Skip to content

Commit ed08af4

Browse files
committed
Ensure missing-key errors include source spans
1 parent 5d9c582 commit ed08af4

File tree

4 files changed

+78
-20
lines changed

4 files changed

+78
-20
lines changed

src/Tomlyn.Tests/NewApiExceptionLocationTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using NUnit.Framework;
22
using Tomlyn.Parsing;
33
using Tomlyn.Serialization;
4+
using System.Text.Json.Serialization;
45

56
namespace Tomlyn.Tests;
67

@@ -62,4 +63,41 @@ public void Parse_InvalidUtf8_IncludesByteOffset()
6263
Assert.That(ex.Column, Is.EqualTo(5));
6364
Assert.That(ex.Message, Does.Contain("utf8.toml("));
6465
}
66+
67+
[Test]
68+
public void Deserialize_RootValueKeyNotFound_IncludesLocation()
69+
{
70+
var options = new TomlSerializerOptions
71+
{
72+
RootValueHandling = TomlRootValueHandling.WrapInRootKey,
73+
RootValueKeyName = "value",
74+
SourceName = "root.toml",
75+
};
76+
77+
var ex = Assert.Throws<TomlException>(() => TomlSerializer.Deserialize<long>("other = 1\n", options));
78+
Assert.That(ex, Is.Not.Null);
79+
Assert.That(ex!.Span.HasValue, Is.True);
80+
Assert.That(ex.Line, Is.GreaterThan(0));
81+
Assert.That(ex.Column, Is.GreaterThan(0));
82+
Assert.That(ex.Message, Does.Contain("root.toml("));
83+
}
84+
85+
public sealed class MissingRequiredModel
86+
{
87+
[JsonRequired]
88+
public string Name { get; set; } = string.Empty;
89+
}
90+
91+
[Test]
92+
public void Deserialize_ReflectionMissingRequiredKey_IncludesLocation()
93+
{
94+
var options = new TomlSerializerOptions { SourceName = "required.toml" };
95+
96+
var ex = Assert.Throws<TomlException>(() => TomlSerializer.Deserialize<MissingRequiredModel>("other = 1\n", options));
97+
Assert.That(ex, Is.Not.Null);
98+
Assert.That(ex!.Span.HasValue, Is.True);
99+
Assert.That(ex.Line, Is.GreaterThan(0));
100+
Assert.That(ex.Column, Is.GreaterThan(0));
101+
Assert.That(ex.Message, Does.Contain("required.toml("));
102+
}
65103
}

src/Tomlyn/Parsing/TomlParser.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,7 @@ private bool ProduceStatement()
646646
CloseImplicitFrames(baseIndex: 0);
647647
CloseExplicitFrames(targetPrefixLength: 0);
648648

649-
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: null, propertyName: null, stringValue: null, data: 0));
649+
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: CurrentSpan(), propertyName: null, stringValue: null, data: 0));
650650
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndDocument, span: null, propertyName: null, stringValue: null, data: 0));
651651
_state = DocumentState.Ended;
652652
return true;
@@ -717,7 +717,7 @@ private bool ProduceArray(ref ContainerFrame frame)
717717
{
718718
Consume(TokenKind.CloseBracket, DetermineLexerStateAfterContainerClose());
719719
_containers.RemoveAt(_containers.Count - 1);
720-
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndArray, span: null, propertyName: null, stringValue: null, data: 0));
720+
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndArray, span: CurrentSpan(), propertyName: null, stringValue: null, data: 0));
721721
return true;
722722
}
723723

@@ -743,7 +743,7 @@ private bool ProduceArray(ref ContainerFrame frame)
743743
{
744744
Consume(TokenKind.CloseBracket, DetermineLexerStateAfterContainerClose());
745745
_containers.RemoveAt(_containers.Count - 1);
746-
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndArray, span: null, propertyName: null, stringValue: null, data: 0));
746+
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndArray, span: CurrentSpan(), propertyName: null, stringValue: null, data: 0));
747747
return true;
748748
}
749749

@@ -756,7 +756,7 @@ private bool ProduceArray(ref ContainerFrame frame)
756756
{
757757
Consume(TokenKind.CloseBracket, DetermineLexerStateAfterContainerClose());
758758
_containers.RemoveAt(_containers.Count - 1);
759-
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndArray, span: null, propertyName: null, stringValue: null, data: 0));
759+
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndArray, span: CurrentSpan(), propertyName: null, stringValue: null, data: 0));
760760
return true;
761761
}
762762

@@ -790,13 +790,13 @@ private bool ProduceInlineTable(ref ContainerFrame frame)
790790
if (_implicitFrames.Count > frame.InlineImplicitBase)
791791
{
792792
_implicitFrames.RemoveAt(_implicitFrames.Count - 1);
793-
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: null, propertyName: null, stringValue: null, data: 0));
793+
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: CurrentSpan(), propertyName: null, stringValue: null, data: 0));
794794
return true;
795795
}
796796

797797
Consume(TokenKind.CloseBrace, DetermineLexerStateAfterContainerClose());
798798
_containers.RemoveAt(_containers.Count - 1);
799-
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: null, propertyName: null, stringValue: null, data: 0));
799+
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: CurrentSpan(), propertyName: null, stringValue: null, data: 0));
800800
return true;
801801
}
802802

@@ -922,7 +922,7 @@ private void CloseImplicitFrames(int baseIndex)
922922
for (var i = _implicitFrames.Count - 1; i >= baseIndex; i--)
923923
{
924924
_implicitFrames.RemoveAt(i);
925-
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: null, propertyName: null, stringValue: null, data: 0));
925+
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: CurrentSpan(), propertyName: null, stringValue: null, data: 0));
926926
}
927927
}
928928

@@ -935,10 +935,10 @@ private void CloseExplicitFrames(int targetPrefixLength)
935935
{
936936
case ExplicitFrameKind.Table:
937937
case ExplicitFrameKind.TableArrayElement:
938-
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: null, propertyName: null, stringValue: null, data: 0));
938+
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: CurrentSpan(), propertyName: null, stringValue: null, data: 0));
939939
break;
940940
case ExplicitFrameKind.Array:
941-
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndArray, span: null, propertyName: null, stringValue: null, data: 0));
941+
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndArray, span: CurrentSpan(), propertyName: null, stringValue: null, data: 0));
942942
break;
943943
}
944944

@@ -1249,7 +1249,7 @@ private void EnsureImplicitPrefix(int baseIndex, List<KeySegment> pathSegments,
12491249
for (var i = currentCount - 1; i >= common; i--)
12501250
{
12511251
_implicitFrames.RemoveAt(baseIndex + i);
1252-
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: null, propertyName: null, stringValue: null, data: 0));
1252+
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: CurrentSpan(), propertyName: null, stringValue: null, data: 0));
12531253
}
12541254

12551255
for (var i = common; i < prefixLength; i++)
@@ -1448,20 +1448,20 @@ private void TerminateStream()
14481448
if (frame.Kind == ContainerKind.InlineTable && _implicitFrames.Count > frame.InlineImplicitBase)
14491449
{
14501450
_implicitFrames.RemoveAt(_implicitFrames.Count - 1);
1451-
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: null, propertyName: null, stringValue: null, data: 0));
1451+
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: CurrentSpan(), propertyName: null, stringValue: null, data: 0));
14521452
continue;
14531453
}
14541454

14551455
_containers.RemoveAt(_containers.Count - 1);
14561456
EnqueueEvent(frame.Kind == ContainerKind.Array
1457-
? new TomlParseEvent(TomlParseEventKind.EndArray, span: null, propertyName: null, stringValue: null, data: 0)
1458-
: new TomlParseEvent(TomlParseEventKind.EndTable, span: null, propertyName: null, stringValue: null, data: 0));
1457+
? new TomlParseEvent(TomlParseEventKind.EndArray, span: CurrentSpan(), propertyName: null, stringValue: null, data: 0)
1458+
: new TomlParseEvent(TomlParseEventKind.EndTable, span: CurrentSpan(), propertyName: null, stringValue: null, data: 0));
14591459
}
14601460

14611461
CloseImplicitFrames(baseIndex: 0);
14621462
CloseExplicitFrames(targetPrefixLength: 0);
14631463

1464-
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: null, propertyName: null, stringValue: null, data: 0));
1464+
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndTable, span: CurrentSpan(), propertyName: null, stringValue: null, data: 0));
14651465
EnqueueEvent(new TomlParseEvent(TomlParseEventKind.EndDocument, span: null, propertyName: null, stringValue: null, data: 0));
14661466
_state = DocumentState.Ended;
14671467
}

src/Tomlyn/Serialization/Internal/TomlReflectionTypeInfoResolver.cs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Tomlyn.Serialization;
1111
using Tomlyn.Serialization.Converters;
1212
using Tomlyn.Syntax;
13+
using Tomlyn.Text;
1314

1415
namespace Tomlyn.Serialization.Internal;
1516

@@ -709,9 +710,11 @@ public override void Write(TomlWriter writer, object? value)
709710
throw reader.CreateException($"Expected {TomlTokenType.StartTable} token but was {reader.TokenType}.");
710711
}
711712

713+
var tableStartSpan = reader.CurrentSpan;
714+
712715
if (_parameters.Length > 0)
713716
{
714-
return ReadWithConstructor(reader);
717+
return ReadWithConstructor(reader, tableStartSpan);
715718
}
716719

717720
var instance = CreateInstance();
@@ -780,11 +783,12 @@ public override void Write(TomlWriter writer, object? value)
780783
reader.Skip();
781784
}
782785

786+
var endTableSpan = reader.CurrentSpan;
783787
reader.Read(); // consume EndTable
784788

785789
if (seen is not null)
786790
{
787-
ValidateRequiredMembers(seen);
791+
ValidateRequiredMembers(seen, endTableSpan ?? tableStartSpan);
788792
}
789793

790794
if (propertiesMetadata is not null)
@@ -874,7 +878,7 @@ private IDictionary CreateExtensionDataDictionary(Type memberType)
874878
return typeInfo.ReadAsObject(reader);
875879
}
876880

877-
private object ReadWithConstructor(TomlReader reader)
881+
private object ReadWithConstructor(TomlReader reader, TomlSourceSpan? tableStartSpan)
878882
{
879883
TomlPropertiesMetadata? propertiesMetadata = null;
880884
if (Options.MetadataStore is not null && !Type.IsValueType)
@@ -971,6 +975,7 @@ private object ReadWithConstructor(TomlReader reader)
971975
reader.Skip();
972976
}
973977

978+
var endTableSpan = reader.CurrentSpan;
974979
reader.Read(); // consume EndTable
975980

976981
for (var i = 0; i < _parameters.Length; i++)
@@ -987,10 +992,20 @@ private object ReadWithConstructor(TomlReader reader)
987992
continue;
988993
}
989994

995+
if (endTableSpan is { } span)
996+
{
997+
throw new TomlException(span, $"Missing required constructor parameter '{binding.KeyName}' when deserializing '{Type.FullName}'.");
998+
}
999+
1000+
if (tableStartSpan is { } startSpan)
1001+
{
1002+
throw new TomlException(startSpan, $"Missing required constructor parameter '{binding.KeyName}' when deserializing '{Type.FullName}'.");
1003+
}
1004+
9901005
throw new TomlException($"Missing required constructor parameter '{binding.KeyName}' when deserializing '{Type.FullName}'.");
9911006
}
9921007

993-
ValidateRequiredMembers(memberSeen);
1008+
ValidateRequiredMembers(memberSeen, endTableSpan ?? tableStartSpan);
9941009

9951010
object instance;
9961011
try
@@ -1112,7 +1127,7 @@ private static TomlPropertyDisplayKind GetDisplayKind(TomlReader reader)
11121127
}
11131128
}
11141129

1115-
private void ValidateRequiredMembers(bool[] seen)
1130+
private void ValidateRequiredMembers(bool[] seen, TomlSourceSpan? span)
11161131
{
11171132
for (var i = 0; i < _members.Count; i++)
11181133
{
@@ -1124,6 +1139,11 @@ private void ValidateRequiredMembers(bool[] seen)
11241139

11251140
if (!seen[i])
11261141
{
1142+
if (span is { } locatedSpan)
1143+
{
1144+
throw new TomlException(locatedSpan, $"Missing required TOML key '{member.SerializedName}' when deserializing '{Type.FullName}'.");
1145+
}
1146+
11271147
throw new TomlException($"Missing required TOML key '{member.SerializedName}' when deserializing '{Type.FullName}'.");
11281148
}
11291149
}

src/Tomlyn/TomlSerializer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ public static bool TryDeserialize(string toml, Type returnType, out object? valu
353353
reader.Skip();
354354
}
355355

356-
throw new TomlException($"The root value key '{options.RootValueKeyName}' was not found.");
356+
throw reader.CreateException($"The root value key '{options.RootValueKeyName}' was not found.");
357357
}
358358

359359
return typeInfo.ReadAsObject(reader);

0 commit comments

Comments
 (0)