Skip to content

Commit d8f282c

Browse files
committed
Merge remote-tracking branch 'aubry/master' into ec-aotwarnings
2 parents 2b7439b + 65c2aa9 commit d8f282c

File tree

11 files changed

+126
-28
lines changed

11 files changed

+126
-28
lines changed

YamlDotNet.Test/Serialization/DateTimeConverterTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ public void Given_Values_WithFormats_WriteYaml_ShouldReturn_Result_WithFirstForm
569569
public void JsonCompatible_EncaseDateTimesInDoubleQuotes()
570570
{
571571
var serializer = new SerializerBuilder().JsonCompatible().Build();
572-
var testObject = new TestObject { DateTime = new DateTime(2023, 01, 14, 0, 1, 2) };
572+
var testObject = new TestObject { DateTime = new DateTime(2023, 01, 14, 0, 1, 2, DateTimeKind.Utc) };
573573
var actual = serializer.Serialize(testObject);
574574

575575
actual.TrimNewLines().Should().ContainEquivalentOf("{\"DateTime\": \"01/14/2023 00:01:02\"}");

YamlDotNet.Test/Serialization/DeserializerTest.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,14 @@ public void DeserializeWithDuplicateKeyChecking_YamlWithDuplicateKeys_ThrowsYaml
298298
.Build();
299299

300300
Action act = () => sut.Deserialize<Person>(yaml);
301-
act.ShouldThrow<YamlException>("Because there are duplicate name keys");
301+
act.ShouldThrow<YamlException>("Because there are duplicate name keys with concrete class");
302+
act = () => sut.Deserialize<IDictionary<object, object>>(yaml);
303+
act.ShouldThrow<YamlException>("Because there are duplicate name keys with dictionary");
304+
305+
var stream = Yaml.ReaderFrom("backreference.yaml");
306+
var parser = new MergingParser(new Parser(stream));
307+
act = () => sut.Deserialize<Dictionary<string, Dictionary<string, string>>>(parser);
308+
act.ShouldThrow<YamlException>("Because there are duplicate name keys with merging parser");
302309
}
303310

304311
[Fact]
@@ -316,6 +323,13 @@ public void DeserializeWithoutDuplicateKeyChecking_YamlWithDuplicateKeys_DoesNot
316323

317324
Action act = () => sut.Deserialize<Person>(yaml);
318325
act.ShouldNotThrow<YamlException>("Because duplicate key checking is not enabled");
326+
act = () => sut.Deserialize<IDictionary<object, object>>(yaml);
327+
act.ShouldNotThrow<YamlException>("Because duplicate key checking is not enabled");
328+
329+
var stream = Yaml.ReaderFrom("backreference.yaml");
330+
var parser = new MergingParser(new Parser(stream));
331+
act = () => sut.Deserialize<Dictionary<string, Dictionary<string, string>>>(parser);
332+
act.ShouldNotThrow<YamlException>("Because duplicate key checking is not enabled");
319333
}
320334

321335
public class Test

YamlDotNet.Test/Serialization/SerializationTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2300,6 +2300,29 @@ public void StringsThatMatchKeywordsAreQuoted(string input)
23002300
Assert.Equal($"text: \"{input}\"{Environment.NewLine}", yaml);
23012301
}
23022302

2303+
public static IEnumerable<object[]> Yaml1_1SpecialStringsData = new[]
2304+
{
2305+
"-.inf", "-.Inf", "-.INF", "-0", "-0100_200", "-0b101", "-0x30", "-190:20:30", "-23", "-3.14",
2306+
"._", "._14", ".", ".0", ".1_4", ".14", ".3E-1", ".3e+3", ".inf", ".Inf",
2307+
".INF", ".nan", ".NaN", ".NAN", "+.inf", "+.Inf", "+.INF", "+0.3e+3", "+0",
2308+
"+0100_200", "+0b100", "+190:20:30", "+23", "+3.14", "~", "0.0", "0", "00", "001.23",
2309+
"0011", "010", "02_0", "07", "0b0", "0b100_101", "0o0", "0o10", "0o7", "0x0",
2310+
"0x10", "0x2_0", "0x42", "0xa", "100_000", "190:20:30.15", "190:20:30", "23", "3.", "3.14", "3.3e+3",
2311+
"85_230.15", "85.230_15e+03", "false", "False", "FALSE", "n", "N", "no", "No", "NO",
2312+
"null", "Null", "NULL", "off", "Off", "OFF", "on", "On", "ON", "true", "True", "TRUE",
2313+
"y", "Y", "yes", "Yes", "YES"
2314+
}.Select(v => new object[] { v }).ToList();
2315+
2316+
[Theory]
2317+
[MemberData(nameof(Yaml1_1SpecialStringsData))]
2318+
public void StringsThatMatchYaml1_1KeywordsAreQuoted(string input)
2319+
{
2320+
var serializer = new SerializerBuilder().WithQuotingNecessaryStrings(true).Build();
2321+
var o = new { text = input };
2322+
var yaml = serializer.Serialize(o);
2323+
Assert.Equal($"text: \"{input}\"{Environment.NewLine}", yaml);
2324+
}
2325+
23032326
[Fact]
23042327
public void KeysOnConcreteClassDontGetQuoted_TypeStringGetsQuoted()
23052328
{

YamlDotNet/Serialization/DeserializerBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public DeserializerBuilder()
9393
{ typeof(NullNodeDeserializer), _ => new NullNodeDeserializer() },
9494
{ typeof(ScalarNodeDeserializer), _ => new ScalarNodeDeserializer(attemptUnknownTypeDeserialization, typeConverter) },
9595
{ typeof(ArrayNodeDeserializer), _ => new ArrayNodeDeserializer() },
96-
{ typeof(DictionaryNodeDeserializer), _ => new DictionaryNodeDeserializer(objectFactory.Value) },
96+
{ typeof(DictionaryNodeDeserializer), _ => new DictionaryNodeDeserializer(objectFactory.Value, duplicateKeyChecking) },
9797
{ typeof(CollectionNodeDeserializer), _ => new CollectionNodeDeserializer(objectFactory.Value) },
9898
{ typeof(EnumerableNodeDeserializer), _ => new EnumerableNodeDeserializer() },
9999
{ typeof(ObjectNodeDeserializer), _ => new ObjectNodeDeserializer(objectFactory.Value, BuildTypeInspector(), ignoreUnmatched, duplicateKeyChecking, typeConverter) }

YamlDotNet/Serialization/EventEmitters/TypeAssigningEventEmitter.cs

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,64 @@ public sealed class TypeAssigningEventEmitter : ChainedEventEmitter
3232
private readonly bool requireTagWhenStaticAndActualTypesAreDifferent;
3333
private readonly IDictionary<Type, TagName> tagMappings;
3434
private readonly bool quoteNecessaryStrings;
35-
private static readonly string IsSpecialStringValue_Regex =
35+
private readonly Regex? isSpecialStringValue_Regex;
36+
private static readonly string SpecialStrings_Pattern =
3637
@"^("
37-
+ @"null|Null|NULL|\~"
38-
+ @"|true|True|TRUE|false|False|FALSE"
39-
+ @"|[-+]?[0-9]+|0o[0-7]+"
40-
+ @"|0x[0-9a-fA-F]+"
41-
+ @"|[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?"
42-
+ @"|[-+]?(\.inf|\.Inf|\.INF)"
43-
+ @"|\.nan|\.NaN|\.NAN"
38+
+ @"null|Null|NULL|\~"
39+
+ @"|true|True|TRUE|false|False|FALSE"
40+
+ @"|[-+]?[0-9]+" // int base 10
41+
+ @"|0o[0-7]+" // int base 8
42+
+ @"|0x[0-9a-fA-F]+" // int base 16
43+
+ @"|[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?" // float number
44+
+ @"|[-+]?(\.inf|\.Inf|\.INF)"
45+
+ @"|\.nan|\.NaN|\.NAN"
4446
+ @")$";
4547

48+
/// <summary>
49+
/// This pattern matches strings that are special both in YAML 1.1 and 1.2
50+
/// </summary>
51+
private static readonly string CombinedYaml1_1SpecialStrings_Pattern =
52+
@"^("
53+
+ @"null|Null|NULL|\~"
54+
+ @"|true|True|TRUE|false|False|FALSE"
55+
+ @"|y|Y|yes|Yes|YES|n|N|no|No|NO"
56+
+ @"|on|On|ON|off|Off|OFF"
57+
+ @"|[-+]?0b[0-1_]+" // int base 2
58+
+ @"|[-+]?0o?[0-7_]+" // int base 8 both with and without "o"
59+
+ @"|[-+]?(0|[1-9][0-9_]*)" // int base 10
60+
+ @"|[-+]?0x[0-9a-fA-F_]+" // int base 16
61+
+ @"|[-+]?[1-9][0-9_]*(:[0-5]?[0-9])+" // int base 60
62+
+ @"|[-+]?([0-9][0-9_]*)?\.[0-9_]*([eE][-+][0-9]+)?" // float base 10
63+
+ @"|[-+]?[0-9][0-9_]*(:[0-5]?[0-9])+\.[0-9_]*" // float base 60
64+
+ @"|[-+]?\.(inf|Inf|INF)"
65+
+ @"|\.(nan|NaN|NAN)"
66+
+ @")$";
67+
68+
public TypeAssigningEventEmitter(IEventEmitter nextEmitter, bool requireTagWhenStaticAndActualTypesAreDifferent, IDictionary<Type, TagName> tagMappings, bool quoteNecessaryStrings, bool quoteYaml1_1Strings)
69+
: this(nextEmitter, requireTagWhenStaticAndActualTypesAreDifferent, tagMappings)
70+
{
71+
this.quoteNecessaryStrings = quoteNecessaryStrings;
72+
73+
var specialStringValuePattern = quoteYaml1_1Strings
74+
? CombinedYaml1_1SpecialStrings_Pattern
75+
: SpecialStrings_Pattern;
76+
#if NET40
77+
isSpecialStringValue_Regex = new Regex(specialStringValuePattern);
78+
#else
79+
isSpecialStringValue_Regex = new Regex(specialStringValuePattern, RegexOptions.Compiled);
80+
#endif
81+
}
82+
4683
public TypeAssigningEventEmitter(IEventEmitter nextEmitter, bool requireTagWhenStaticAndActualTypesAreDifferent, IDictionary<Type, TagName> tagMappings, bool quoteNecessaryStrings)
4784
: this(nextEmitter, requireTagWhenStaticAndActualTypesAreDifferent, tagMappings)
4885
{
4986
this.quoteNecessaryStrings = quoteNecessaryStrings;
87+
88+
#if NET40
89+
isSpecialStringValue_Regex = new Regex(SpecialStrings_Pattern);
90+
#else
91+
isSpecialStringValue_Regex = new Regex(SpecialStrings_Pattern, RegexOptions.Compiled);
92+
#endif
5093
}
5194

5295
public TypeAssigningEventEmitter(IEventEmitter nextEmitter, bool requireTagWhenStaticAndActualTypesAreDifferent, IDictionary<Type, TagName> tagMappings)
@@ -204,9 +247,7 @@ private bool IsSpecialStringValue(string value)
204247
return true;
205248
}
206249

207-
return Regex.IsMatch(
208-
value,
209-
IsSpecialStringValue_Regex);
250+
return isSpecialStringValue_Regex?.IsMatch(value) ?? false;
210251
}
211252
}
212253
}

YamlDotNet/Serialization/NodeDeserializers/DictionaryDeserializer.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,25 @@ namespace YamlDotNet.Serialization.NodeDeserializers
2828
{
2929
public abstract class DictionaryDeserializer
3030
{
31-
protected void Deserialize(Type tKey, Type tValue, IParser parser, Func<IParser, Type, object?> nestedObjectDeserializer, IDictionary result)
31+
private readonly bool duplicateKeyChecking;
32+
33+
public DictionaryDeserializer(bool duplicateKeyChecking)
34+
{
35+
this.duplicateKeyChecking = duplicateKeyChecking;
36+
}
37+
38+
private void TryAssign(IDictionary result, object key, object value, MappingStart propertyName)
39+
{
40+
if (duplicateKeyChecking && result.Contains(key))
41+
{
42+
throw new YamlException(propertyName.Start, propertyName.End, $"Encountered duplicate key {key}");
43+
}
44+
result[key] = value!;
45+
}
46+
47+
protected virtual void Deserialize(Type tKey, Type tValue, IParser parser, Func<IParser, Type, object?> nestedObjectDeserializer, IDictionary result)
3248
{
33-
parser.Consume<MappingStart>();
49+
var property = parser.Consume<MappingStart>();
3450
while (!parser.TryConsume<MappingEnd>(out var _))
3551
{
3652
var key = nestedObjectDeserializer(parser, tKey);
@@ -53,7 +69,7 @@ protected void Deserialize(Type tKey, Type tValue, IParser parser, Func<IParser,
5369
{
5470
if (hasFirstPart)
5571
{
56-
result[v!] = value!;
72+
TryAssign(result, v!, value!, property);
5773
}
5874
else
5975
{
@@ -66,7 +82,7 @@ protected void Deserialize(Type tKey, Type tValue, IParser parser, Func<IParser,
6682
{
6783
if (hasFirstPart)
6884
{
69-
result[key] = v!;
85+
TryAssign(result, key, v!, property);
7086
}
7187
else
7288
{
@@ -86,7 +102,7 @@ protected void Deserialize(Type tKey, Type tValue, IParser parser, Func<IParser,
86102
if (valuePromise == null)
87103
{
88104
// Happy path: both key and value are known
89-
result[key] = value!;
105+
TryAssign(result, key, value!, property);
90106
}
91107
else
92108
{

YamlDotNet/Serialization/NodeDeserializers/DictionaryNodeDeserializer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public class DictionaryNodeDeserializer : DictionaryDeserializer, INodeDeseriali
3333
{
3434
private readonly IObjectFactory objectFactory;
3535

36-
public DictionaryNodeDeserializer(IObjectFactory objectFactory)
36+
public DictionaryNodeDeserializer(IObjectFactory objectFactory, bool duplicateKeyChecking) :
37+
base(duplicateKeyChecking)
3738
{
3839
this.objectFactory = objectFactory ?? throw new ArgumentNullException(nameof(objectFactory));
3940
}

YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,16 @@ public bool Deserialize(IParser parser, Type expectedType, Func<IParser, Type, o
5757
var implementationType = Nullable.GetUnderlyingType(expectedType) ?? expectedType;
5858

5959
value = objectFactory.Create(implementationType);
60-
var consumedProperties = new List<string>();
60+
var consumedProperties = new HashSet<string>(StringComparer.Ordinal);
6161
while (!parser.TryConsume<MappingEnd>(out var _))
6262
{
6363
var propertyName = parser.Consume<Scalar>();
64-
if (duplicateKeyChecking && consumedProperties.Contains(propertyName.Value))
64+
if (duplicateKeyChecking && !consumedProperties.Add(propertyName.Value))
6565
{
6666
throw new YamlException(propertyName.Start, propertyName.End, $"Encountered duplicate key {propertyName.Value}");
6767
}
6868
try
6969
{
70-
consumedProperties.Add(propertyName.Value);
7170
var property = typeDescriptor.GetProperty(implementationType, null, propertyName.Value, ignoreUnmatched);
7271
if (property == null)
7372
{

YamlDotNet/Serialization/NodeDeserializers/StaticDictionaryNodeDeserializer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ public class StaticDictionaryNodeDeserializer : DictionaryDeserializer, INodeDes
2929
{
3030
private readonly ObjectFactories.StaticObjectFactory _objectFactory;
3131

32-
public StaticDictionaryNodeDeserializer(ObjectFactories.StaticObjectFactory objectFactory)
32+
public StaticDictionaryNodeDeserializer(ObjectFactories.StaticObjectFactory objectFactory, bool duplicateKeyChecking)
33+
: base(duplicateKeyChecking)
3334
{
3435
_objectFactory = objectFactory ?? throw new ArgumentNullException(nameof(objectFactory));
3536
}

YamlDotNet/Serialization/SerializerBuilder.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public sealed class SerializerBuilder : BuilderSkeleton<SerializerBuilder>
6060
private EmitterSettings emitterSettings = EmitterSettings.Default;
6161
private DefaultValuesHandling defaultValuesHandlingConfiguration = DefaultValuesHandling.Preserve;
6262
private bool quoteNecessaryStrings;
63+
private bool quoteYaml1_1Strings;
6364

6465
public SerializerBuilder()
6566
: base(new DynamicTypeResolver())
@@ -96,7 +97,7 @@ public SerializerBuilder()
9697

9798
eventEmitterFactories = new LazyComponentRegistrationList<IEventEmitter, IEventEmitter>
9899
{
99-
{ typeof(TypeAssigningEventEmitter), inner => new TypeAssigningEventEmitter(inner, false, tagMappings, quoteNecessaryStrings) }
100+
{ typeof(TypeAssigningEventEmitter), inner => new TypeAssigningEventEmitter(inner, false, tagMappings, quoteNecessaryStrings, quoteYaml1_1Strings) }
100101
};
101102

102103
objectFactory = new DefaultObjectFactory();
@@ -110,9 +111,11 @@ public SerializerBuilder()
110111
/// <summary>
111112
/// Put double quotes around strings that need it, for example Null, True, False, a number. This should be called before any other "With" methods if you want this feature enabled.
112113
/// </summary>
113-
public SerializerBuilder WithQuotingNecessaryStrings()
114+
/// <param name="quoteYaml1_1Strings">Also quote strings that are valid scalars in the YAML 1.1 specification (which includes boolean Yes/No/On/Off, base 60 numbers and more)</param>
115+
public SerializerBuilder WithQuotingNecessaryStrings(bool quoteYaml1_1Strings = false)
114116
{
115117
quoteNecessaryStrings = true;
118+
this.quoteYaml1_1Strings = quoteYaml1_1Strings;
116119
return this;
117120
}
118121

@@ -269,7 +272,7 @@ public SerializerBuilder EnsureRoundtrip()
269272
settings,
270273
objectFactory
271274
);
272-
WithEventEmitter(inner => new TypeAssigningEventEmitter(inner, true, tagMappings, quoteNecessaryStrings), loc => loc.InsteadOf<TypeAssigningEventEmitter>());
275+
WithEventEmitter(inner => new TypeAssigningEventEmitter(inner, true, tagMappings, quoteNecessaryStrings, quoteYaml1_1Strings), loc => loc.InsteadOf<TypeAssigningEventEmitter>());
273276
return WithTypeInspector(inner => new ReadableAndWritablePropertiesTypeInspector(inner), loc => loc.OnBottom());
274277
}
275278

0 commit comments

Comments
 (0)