Skip to content

Commit

Permalink
Merge remote-tracking branch 'aubry/master' into ec-aotwarnings
Browse files Browse the repository at this point in the history
  • Loading branch information
EdwardCooke committed Jan 28, 2023
2 parents 2b7439b + 65c2aa9 commit d8f282c
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 28 deletions.
2 changes: 1 addition & 1 deletion YamlDotNet.Test/Serialization/DateTimeConverterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ public void Given_Values_WithFormats_WriteYaml_ShouldReturn_Result_WithFirstForm
public void JsonCompatible_EncaseDateTimesInDoubleQuotes()
{
var serializer = new SerializerBuilder().JsonCompatible().Build();
var testObject = new TestObject { DateTime = new DateTime(2023, 01, 14, 0, 1, 2) };
var testObject = new TestObject { DateTime = new DateTime(2023, 01, 14, 0, 1, 2, DateTimeKind.Utc) };
var actual = serializer.Serialize(testObject);

actual.TrimNewLines().Should().ContainEquivalentOf("{\"DateTime\": \"01/14/2023 00:01:02\"}");
Expand Down
16 changes: 15 additions & 1 deletion YamlDotNet.Test/Serialization/DeserializerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,14 @@ public void DeserializeWithDuplicateKeyChecking_YamlWithDuplicateKeys_ThrowsYaml
.Build();

Action act = () => sut.Deserialize<Person>(yaml);
act.ShouldThrow<YamlException>("Because there are duplicate name keys");
act.ShouldThrow<YamlException>("Because there are duplicate name keys with concrete class");
act = () => sut.Deserialize<IDictionary<object, object>>(yaml);
act.ShouldThrow<YamlException>("Because there are duplicate name keys with dictionary");

var stream = Yaml.ReaderFrom("backreference.yaml");
var parser = new MergingParser(new Parser(stream));
act = () => sut.Deserialize<Dictionary<string, Dictionary<string, string>>>(parser);
act.ShouldThrow<YamlException>("Because there are duplicate name keys with merging parser");
}

[Fact]
Expand All @@ -316,6 +323,13 @@ public void DeserializeWithoutDuplicateKeyChecking_YamlWithDuplicateKeys_DoesNot

Action act = () => sut.Deserialize<Person>(yaml);
act.ShouldNotThrow<YamlException>("Because duplicate key checking is not enabled");
act = () => sut.Deserialize<IDictionary<object, object>>(yaml);
act.ShouldNotThrow<YamlException>("Because duplicate key checking is not enabled");

var stream = Yaml.ReaderFrom("backreference.yaml");
var parser = new MergingParser(new Parser(stream));
act = () => sut.Deserialize<Dictionary<string, Dictionary<string, string>>>(parser);
act.ShouldNotThrow<YamlException>("Because duplicate key checking is not enabled");
}

public class Test
Expand Down
23 changes: 23 additions & 0 deletions YamlDotNet.Test/Serialization/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2300,6 +2300,29 @@ public void StringsThatMatchKeywordsAreQuoted(string input)
Assert.Equal($"text: \"{input}\"{Environment.NewLine}", yaml);
}

public static IEnumerable<object[]> Yaml1_1SpecialStringsData = new[]
{
"-.inf", "-.Inf", "-.INF", "-0", "-0100_200", "-0b101", "-0x30", "-190:20:30", "-23", "-3.14",
"._", "._14", ".", ".0", ".1_4", ".14", ".3E-1", ".3e+3", ".inf", ".Inf",
".INF", ".nan", ".NaN", ".NAN", "+.inf", "+.Inf", "+.INF", "+0.3e+3", "+0",
"+0100_200", "+0b100", "+190:20:30", "+23", "+3.14", "~", "0.0", "0", "00", "001.23",
"0011", "010", "02_0", "07", "0b0", "0b100_101", "0o0", "0o10", "0o7", "0x0",
"0x10", "0x2_0", "0x42", "0xa", "100_000", "190:20:30.15", "190:20:30", "23", "3.", "3.14", "3.3e+3",
"85_230.15", "85.230_15e+03", "false", "False", "FALSE", "n", "N", "no", "No", "NO",
"null", "Null", "NULL", "off", "Off", "OFF", "on", "On", "ON", "true", "True", "TRUE",
"y", "Y", "yes", "Yes", "YES"
}.Select(v => new object[] { v }).ToList();

[Theory]
[MemberData(nameof(Yaml1_1SpecialStringsData))]
public void StringsThatMatchYaml1_1KeywordsAreQuoted(string input)
{
var serializer = new SerializerBuilder().WithQuotingNecessaryStrings(true).Build();
var o = new { text = input };
var yaml = serializer.Serialize(o);
Assert.Equal($"text: \"{input}\"{Environment.NewLine}", yaml);
}

[Fact]
public void KeysOnConcreteClassDontGetQuoted_TypeStringGetsQuoted()
{
Expand Down
2 changes: 1 addition & 1 deletion YamlDotNet/Serialization/DeserializerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public DeserializerBuilder()
{ typeof(NullNodeDeserializer), _ => new NullNodeDeserializer() },
{ typeof(ScalarNodeDeserializer), _ => new ScalarNodeDeserializer(attemptUnknownTypeDeserialization, typeConverter) },
{ typeof(ArrayNodeDeserializer), _ => new ArrayNodeDeserializer() },
{ typeof(DictionaryNodeDeserializer), _ => new DictionaryNodeDeserializer(objectFactory.Value) },
{ typeof(DictionaryNodeDeserializer), _ => new DictionaryNodeDeserializer(objectFactory.Value, duplicateKeyChecking) },
{ typeof(CollectionNodeDeserializer), _ => new CollectionNodeDeserializer(objectFactory.Value) },
{ typeof(EnumerableNodeDeserializer), _ => new EnumerableNodeDeserializer() },
{ typeof(ObjectNodeDeserializer), _ => new ObjectNodeDeserializer(objectFactory.Value, BuildTypeInspector(), ignoreUnmatched, duplicateKeyChecking, typeConverter) }
Expand Down
63 changes: 52 additions & 11 deletions YamlDotNet/Serialization/EventEmitters/TypeAssigningEventEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,64 @@ public sealed class TypeAssigningEventEmitter : ChainedEventEmitter
private readonly bool requireTagWhenStaticAndActualTypesAreDifferent;
private readonly IDictionary<Type, TagName> tagMappings;
private readonly bool quoteNecessaryStrings;
private static readonly string IsSpecialStringValue_Regex =
private readonly Regex? isSpecialStringValue_Regex;
private static readonly string SpecialStrings_Pattern =
@"^("
+ @"null|Null|NULL|\~"
+ @"|true|True|TRUE|false|False|FALSE"
+ @"|[-+]?[0-9]+|0o[0-7]+"
+ @"|0x[0-9a-fA-F]+"
+ @"|[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?"
+ @"|[-+]?(\.inf|\.Inf|\.INF)"
+ @"|\.nan|\.NaN|\.NAN"
+ @"null|Null|NULL|\~"
+ @"|true|True|TRUE|false|False|FALSE"
+ @"|[-+]?[0-9]+" // int base 10
+ @"|0o[0-7]+" // int base 8
+ @"|0x[0-9a-fA-F]+" // int base 16
+ @"|[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?" // float number
+ @"|[-+]?(\.inf|\.Inf|\.INF)"
+ @"|\.nan|\.NaN|\.NAN"
+ @")$";

/// <summary>
/// This pattern matches strings that are special both in YAML 1.1 and 1.2
/// </summary>
private static readonly string CombinedYaml1_1SpecialStrings_Pattern =
@"^("
+ @"null|Null|NULL|\~"
+ @"|true|True|TRUE|false|False|FALSE"
+ @"|y|Y|yes|Yes|YES|n|N|no|No|NO"
+ @"|on|On|ON|off|Off|OFF"
+ @"|[-+]?0b[0-1_]+" // int base 2
+ @"|[-+]?0o?[0-7_]+" // int base 8 both with and without "o"
+ @"|[-+]?(0|[1-9][0-9_]*)" // int base 10
+ @"|[-+]?0x[0-9a-fA-F_]+" // int base 16
+ @"|[-+]?[1-9][0-9_]*(:[0-5]?[0-9])+" // int base 60
+ @"|[-+]?([0-9][0-9_]*)?\.[0-9_]*([eE][-+][0-9]+)?" // float base 10
+ @"|[-+]?[0-9][0-9_]*(:[0-5]?[0-9])+\.[0-9_]*" // float base 60
+ @"|[-+]?\.(inf|Inf|INF)"
+ @"|\.(nan|NaN|NAN)"
+ @")$";

public TypeAssigningEventEmitter(IEventEmitter nextEmitter, bool requireTagWhenStaticAndActualTypesAreDifferent, IDictionary<Type, TagName> tagMappings, bool quoteNecessaryStrings, bool quoteYaml1_1Strings)
: this(nextEmitter, requireTagWhenStaticAndActualTypesAreDifferent, tagMappings)
{
this.quoteNecessaryStrings = quoteNecessaryStrings;

var specialStringValuePattern = quoteYaml1_1Strings
? CombinedYaml1_1SpecialStrings_Pattern
: SpecialStrings_Pattern;
#if NET40
isSpecialStringValue_Regex = new Regex(specialStringValuePattern);
#else
isSpecialStringValue_Regex = new Regex(specialStringValuePattern, RegexOptions.Compiled);
#endif
}

public TypeAssigningEventEmitter(IEventEmitter nextEmitter, bool requireTagWhenStaticAndActualTypesAreDifferent, IDictionary<Type, TagName> tagMappings, bool quoteNecessaryStrings)
: this(nextEmitter, requireTagWhenStaticAndActualTypesAreDifferent, tagMappings)
{
this.quoteNecessaryStrings = quoteNecessaryStrings;

#if NET40
isSpecialStringValue_Regex = new Regex(SpecialStrings_Pattern);
#else
isSpecialStringValue_Regex = new Regex(SpecialStrings_Pattern, RegexOptions.Compiled);
#endif
}

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

return Regex.IsMatch(
value,
IsSpecialStringValue_Regex);
return isSpecialStringValue_Regex?.IsMatch(value) ?? false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,25 @@ namespace YamlDotNet.Serialization.NodeDeserializers
{
public abstract class DictionaryDeserializer
{
protected void Deserialize(Type tKey, Type tValue, IParser parser, Func<IParser, Type, object?> nestedObjectDeserializer, IDictionary result)
private readonly bool duplicateKeyChecking;

public DictionaryDeserializer(bool duplicateKeyChecking)
{
this.duplicateKeyChecking = duplicateKeyChecking;
}

private void TryAssign(IDictionary result, object key, object value, MappingStart propertyName)
{
if (duplicateKeyChecking && result.Contains(key))
{
throw new YamlException(propertyName.Start, propertyName.End, $"Encountered duplicate key {key}");
}
result[key] = value!;
}

protected virtual void Deserialize(Type tKey, Type tValue, IParser parser, Func<IParser, Type, object?> nestedObjectDeserializer, IDictionary result)
{
parser.Consume<MappingStart>();
var property = parser.Consume<MappingStart>();
while (!parser.TryConsume<MappingEnd>(out var _))
{
var key = nestedObjectDeserializer(parser, tKey);
Expand All @@ -53,7 +69,7 @@ protected void Deserialize(Type tKey, Type tValue, IParser parser, Func<IParser,
{
if (hasFirstPart)
{
result[v!] = value!;
TryAssign(result, v!, value!, property);
}
else
{
Expand All @@ -66,7 +82,7 @@ protected void Deserialize(Type tKey, Type tValue, IParser parser, Func<IParser,
{
if (hasFirstPart)
{
result[key] = v!;
TryAssign(result, key, v!, property);
}
else
{
Expand All @@ -86,7 +102,7 @@ protected void Deserialize(Type tKey, Type tValue, IParser parser, Func<IParser,
if (valuePromise == null)
{
// Happy path: both key and value are known
result[key] = value!;
TryAssign(result, key, value!, property);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ public class DictionaryNodeDeserializer : DictionaryDeserializer, INodeDeseriali
{
private readonly IObjectFactory objectFactory;

public DictionaryNodeDeserializer(IObjectFactory objectFactory)
public DictionaryNodeDeserializer(IObjectFactory objectFactory, bool duplicateKeyChecking) :
base(duplicateKeyChecking)
{
this.objectFactory = objectFactory ?? throw new ArgumentNullException(nameof(objectFactory));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,16 @@ public bool Deserialize(IParser parser, Type expectedType, Func<IParser, Type, o
var implementationType = Nullable.GetUnderlyingType(expectedType) ?? expectedType;

value = objectFactory.Create(implementationType);
var consumedProperties = new List<string>();
var consumedProperties = new HashSet<string>(StringComparer.Ordinal);
while (!parser.TryConsume<MappingEnd>(out var _))
{
var propertyName = parser.Consume<Scalar>();
if (duplicateKeyChecking && consumedProperties.Contains(propertyName.Value))
if (duplicateKeyChecking && !consumedProperties.Add(propertyName.Value))
{
throw new YamlException(propertyName.Start, propertyName.End, $"Encountered duplicate key {propertyName.Value}");
}
try
{
consumedProperties.Add(propertyName.Value);
var property = typeDescriptor.GetProperty(implementationType, null, propertyName.Value, ignoreUnmatched);
if (property == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public class StaticDictionaryNodeDeserializer : DictionaryDeserializer, INodeDes
{
private readonly ObjectFactories.StaticObjectFactory _objectFactory;

public StaticDictionaryNodeDeserializer(ObjectFactories.StaticObjectFactory objectFactory)
public StaticDictionaryNodeDeserializer(ObjectFactories.StaticObjectFactory objectFactory, bool duplicateKeyChecking)
: base(duplicateKeyChecking)
{
_objectFactory = objectFactory ?? throw new ArgumentNullException(nameof(objectFactory));
}
Expand Down
9 changes: 6 additions & 3 deletions YamlDotNet/Serialization/SerializerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public sealed class SerializerBuilder : BuilderSkeleton<SerializerBuilder>
private EmitterSettings emitterSettings = EmitterSettings.Default;
private DefaultValuesHandling defaultValuesHandlingConfiguration = DefaultValuesHandling.Preserve;
private bool quoteNecessaryStrings;
private bool quoteYaml1_1Strings;

public SerializerBuilder()
: base(new DynamicTypeResolver())
Expand Down Expand Up @@ -96,7 +97,7 @@ public SerializerBuilder()

eventEmitterFactories = new LazyComponentRegistrationList<IEventEmitter, IEventEmitter>
{
{ typeof(TypeAssigningEventEmitter), inner => new TypeAssigningEventEmitter(inner, false, tagMappings, quoteNecessaryStrings) }
{ typeof(TypeAssigningEventEmitter), inner => new TypeAssigningEventEmitter(inner, false, tagMappings, quoteNecessaryStrings, quoteYaml1_1Strings) }
};

objectFactory = new DefaultObjectFactory();
Expand All @@ -110,9 +111,11 @@ public SerializerBuilder()
/// <summary>
/// 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.
/// </summary>
public SerializerBuilder WithQuotingNecessaryStrings()
/// <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>
public SerializerBuilder WithQuotingNecessaryStrings(bool quoteYaml1_1Strings = false)
{
quoteNecessaryStrings = true;
this.quoteYaml1_1Strings = quoteYaml1_1Strings;
return this;
}

Expand Down Expand Up @@ -269,7 +272,7 @@ public SerializerBuilder EnsureRoundtrip()
settings,
objectFactory
);
WithEventEmitter(inner => new TypeAssigningEventEmitter(inner, true, tagMappings, quoteNecessaryStrings), loc => loc.InsteadOf<TypeAssigningEventEmitter>());
WithEventEmitter(inner => new TypeAssigningEventEmitter(inner, true, tagMappings, quoteNecessaryStrings, quoteYaml1_1Strings), loc => loc.InsteadOf<TypeAssigningEventEmitter>());
return WithTypeInspector(inner => new ReadableAndWritablePropertiesTypeInspector(inner), loc => loc.OnBottom());
}

Expand Down
2 changes: 1 addition & 1 deletion YamlDotNet/Serialization/StaticDeserializerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public StaticDeserializerBuilder(StaticContext context)
{ typeof(NullNodeDeserializer), _ => new NullNodeDeserializer() },
{ typeof(ScalarNodeDeserializer), _ => new ScalarNodeDeserializer(attemptUnknownTypeDeserialization, typeConverter) },
{ typeof(StaticArrayNodeDeserializer), _ => new StaticArrayNodeDeserializer(factory) },
{ typeof(StaticDictionaryNodeDeserializer), _ => new StaticDictionaryNodeDeserializer(factory) },
{ typeof(StaticDictionaryNodeDeserializer), _ => new StaticDictionaryNodeDeserializer(factory, duplicateKeyChecking) },
{ typeof(StaticCollectionNodeDeserializer), _ => new StaticCollectionNodeDeserializer(factory) },
{ typeof(ObjectNodeDeserializer), _ => new ObjectNodeDeserializer(factory, BuildTypeInspector(), ignoreUnmatched, duplicateKeyChecking, typeConverter) },
};
Expand Down

0 comments on commit d8f282c

Please sign in to comment.