Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions YamlDotNet.Analyzers.StaticGenerator/StaticTypeInspectorFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,45 @@ public override void Write(SerializableSyntaxReceiver syntaxReceiver)
Write("return value.ToString();");
UnIndent(); Write("}");

Write("public bool HasParseMethod(Type type)");
Write("{"); Indent();
foreach (var o in syntaxReceiver.Classes)
{
var classObject = o.Value;
if (classObject.ModuleSymbol.GetMembers("Parse").OfType<IMethodSymbol>().Any(m => m.IsStatic && m.Parameters.Length == 1 && m.Parameters[0].Type.SpecialType == SpecialType.System_String))
{
Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}))");
Write("{"); Indent();
Write("return true;");
UnIndent(); Write("}");
}
}
Write("return false;");
UnIndent(); Write("}");

Write("public object Parse(string value, Type expectedType)");
Write("{"); Indent();
foreach (var o in syntaxReceiver.Classes)
{
var classObject = o.Value;
if (classObject.ModuleSymbol
.GetMembers("Parse")
.OfType<IMethodSymbol>()
.Any(m => m.DeclaredAccessibility == Accessibility.Public &&
m.IsStatic &&
m.Parameters.Length == 1 &&
m.Parameters[0].Type.SpecialType == SpecialType.System_String &&
SymbolEqualityComparer.Default.Equals(m.ReturnType, classObject.ModuleSymbol)))
{
Write($"if (expectedType == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}))");
Write("{"); Indent();
Write($"return {classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}.Parse(value);");
UnIndent(); Write("}");
Comment thread
EdwardCooke marked this conversation as resolved.
}
}
Write("throw new InvalidOperationException($\"Type '{expectedType.FullName}' does not have a static Parse method.\");");
UnIndent(); Write("}");

UnIndent(); Write("}");
}

Expand Down
7 changes: 5 additions & 2 deletions YamlDotNet.Core7AoTCompileTest/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
using YamlDotNet.Core7AoTCompileTest.Model;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.Callbacks;
using YamlDotNet.Serialization.NamingConventions;

string yaml = string.Create(CultureInfo.InvariantCulture, $@"MyBool: true
hi: 1
Expand Down Expand Up @@ -106,6 +107,7 @@

var aotContext = new YamlDotNet.Core7AoTCompileTest.StaticContext();
var deserializer = new StaticDeserializerBuilder(aotContext)
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.Build();

var x = deserializer.Deserialize<PrimitiveTypes>(input);
Expand Down Expand Up @@ -189,16 +191,17 @@
Console.WriteLine("Serialized:");

var serializer = new StaticSerializerBuilder(aotContext)
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.Build();

var output = serializer.Serialize(x);
Console.WriteLine(output);
Console.WriteLine("============== Done with the primary object");

yaml = @"- myArray:
yaml = @"- my_array:
- 1
- 2
- myArray:
- my_array:
- 3
- 4
";
Expand Down
134 changes: 75 additions & 59 deletions YamlDotNet.Test/Serialization/MergingParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,53 +162,60 @@ public async Task MergingParserWithMergeKeyBomb_ShouldThrowExceptionWhenTooManyE
// Timebox this test to avoid infinite loops in case of bugs.
// 30 seconds should be more than enough for this test to run even on a slow machine, and if it takes longer than that,
// it's likely that the merging parser is not correctly counting events and enforcing the limit.
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60));
cancellationTokenSource.Token.Register(() =>
{
throw new TimeoutException("The test took too long, likely due to an infinite loop in the merging parser.");
});

await Task.Run(() =>
try
{
var sb = new StringBuilder();

// Base anchor
sb.AppendLine("a0: &a0");
sb.AppendLine(" x: 1");
sb.AppendLine();

// Each level merges the previous anchor TWICE (fanout=2), doubling event count
for (int i = 1; i <= 25; i++)
await Task.Run(() =>
{
sb.AppendLine($"a{i}: &a{i}");
sb.AppendLine($" <<: *a{i - 1}"); // first merge
sb.AppendLine($" <<: *a{i - 1}"); // second merge
var sb = new StringBuilder();

// Base anchor
sb.AppendLine("a0: &a0");
sb.AppendLine(" x: 1");
sb.AppendLine();
}

sb.AppendLine("final:");
sb.AppendLine(" <<: *a25");
// Each level merges the previous anchor TWICE (fanout=2), doubling event count
for (int i = 1; i <= 25; i++)
{
sb.AppendLine($"a{i}: &a{i}");
sb.AppendLine($" <<: *a{i - 1}"); // first merge
sb.AppendLine($" <<: *a{i - 1}"); // second merge
sb.AppendLine();
}

var yaml = sb.ToString();
var parser = new Parser(new StringReader(yaml));
var mergingParser = new MergingParser(parser, 1000);
try
{
while (mergingParser.MoveNext())
sb.AppendLine("final:");
sb.AppendLine(" <<: *a25");

var yaml = sb.ToString();
var parser = new Parser(new StringReader(yaml));
var mergingParser = new MergingParser(parser, 1000);
try
{
//move through everything, we're in a timebox so if this takes too long, the cancellation token will trigger and fail the test
while (mergingParser.MoveNext())
{
//move through everything, we're in a timebox so if this takes too long, the cancellation token will trigger and fail the test
}
}
}
catch (YamlException ex) when (ex.Message.Contains("Too many parsing events"))
{
// Expected exception, test passes
return;
}
catch (Exception ex)
{
throw new Exception($"Unexpected exception", ex);
}
}, cancellationTokenSource.Token);
catch (YamlException ex) when (ex.Message.Contains("Too many parsing events"))
{
// Expected exception, test passes
return;
}
catch (Exception ex)
{
throw new Exception($"Unexpected exception", ex);
}
}, cancellationTokenSource.Token);
}
catch (TimeoutException ex)
{
Assert.Fail($"Test failed due to timeout: {ex.Message}");
}
}

[Fact]
Expand All @@ -217,42 +224,51 @@ public async Task MergingParserWithManySmallMerges_ShouldThrowExceptionWhenCumul
// Timebox this test to avoid infinite loops in case of bugs.
// 30 seconds should be more than enough for this test to run even on a slow machine, and if it takes longer than that,
// it's likely that the merging parser is not correctly counting events and enforcing the limit.
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60));
cancellationTokenSource.Token.Register(() =>
{
throw new TimeoutException("The test took too long, likely due to an infinite loop in the merging parser.");
});

await Task.Run(() =>
try
{
var sb = new StringBuilder();
sb.AppendLine("base: &base");
for (var i = 0; i < 25; i++)
await Task.Run(() =>
{
sb.AppendLine($" k{i}: v{i}");
}
var sb = new StringBuilder();
sb.AppendLine("base: &base");
for (var i = 0; i < 25; i++)
{
sb.AppendLine($" k{i}: v{i}");
}

sb.AppendLine();
for (var i = 0; i < 35; i++)
{
sb.AppendLine($"entry{i}:");
sb.AppendLine(" <<: *base");
sb.AppendLine();
}

var parser = new Parser(new StringReader(sb.ToString()));
var mergingParser = new MergingParser(parser, 1000);

Action parse = () =>
{
while (mergingParser.MoveNext())
for (var i = 0; i < 35; i++)
{
sb.AppendLine($"entry{i}:");
sb.AppendLine(" <<: *base");
sb.AppendLine();
}
};

parse.Should().Throw<YamlException>()
.Where(ex => ex.Message.Contains("Too many parsing events"));
}, cancellationTokenSource.Token);
var parser = new Parser(new StringReader(sb.ToString()));
var mergingParser = new MergingParser(parser, 1000);
var totalParsedEvents = 0;
Action parse = () =>
{
while (mergingParser.MoveNext())
{
totalParsedEvents++;
}
};

parse.Should().Throw<YamlException>()
.Where(ex => ex.Message.Contains("Too many parsing events"));
Console.WriteLine($"Total parsed events before exception: {totalParsedEvents}");
}, cancellationTokenSource.Token);
}
catch (TimeoutException ex)
{
Assert.Fail($"Test failed due to timeout: {ex.Message}");
}
}

[Fact]
Expand Down
14 changes: 14 additions & 0 deletions YamlDotNet/Serialization/ITypeInspector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,19 @@ public interface ITypeInspector
/// <param name="enumValue"></param>
/// <returns></returns>
string GetEnumValue(object enumValue);

/// <summary>
/// Indicates whether the specified type has a static Parse method that can be used to convert a string to an instance of that type.
/// </summary>
/// <param name="type">The type to check for a static Parse method.</param>
/// <returns>True if the type has a static Parse method; otherwise, false.</returns>
bool HasParseMethod(Type type);

/// <summary> Converts a string to an instance of the specified type using a static Parse method.
/// </summary>
/// <param name="value">The string value to convert.</param>
/// <param name="expectedType">The type to convert the string to.</param>
/// <returns>An instance of the specified type.</returns>
object? Parse(string value, Type expectedType);
Comment thread
EdwardCooke marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,17 +147,9 @@ public bool Deserialize(IParser parser, Type expectedType, Func<IParser, Type, o
// TimeSpan, DateTimeOffset, Guid, IPAddress, and others.
// This is especially important for the static/AOT deserialization
// path where the typeConverter is a NullTypeConverter.
var parseMethod = underlyingType.GetPublicStaticMethod("Parse", typeof(string), typeof(IFormatProvider));
if (parseMethod != null)
if (typeInspector.HasParseMethod(underlyingType))
{
try
{
value = parseMethod.Invoke(null, new object[] { scalar.Value, CultureInfo.InvariantCulture });
}
catch (System.Reflection.TargetInvocationException ex) when (ex.InnerException != null)
{
throw ex.InnerException;
}
value = typeInspector.Parse(scalar.Value, underlyingType);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class CachedTypeInspector : TypeInspectorSkeleton
private readonly ConcurrentDictionary<Type, List<IPropertyDescriptor>> cache = new ConcurrentDictionary<Type, List<IPropertyDescriptor>>();
private readonly ConcurrentDictionary<Type, ConcurrentDictionary<string, string>> enumNameCache = new ConcurrentDictionary<Type, ConcurrentDictionary<string, string>>();
private readonly ConcurrentDictionary<object, string> enumValueCache = new ConcurrentDictionary<object, string>();
private readonly ConcurrentDictionary<Type, bool> hasParseMethodCache = new ConcurrentDictionary<Type, bool>();

public CachedTypeInspector(ITypeInspector innerTypeDescriptor)
{
Expand Down Expand Up @@ -74,5 +75,13 @@ public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object
},
(container, innerTypeDescriptor));
}

public override bool HasParseMethod(Type type)
{
return hasParseMethodCache.GetOrAdd(type, static (t, typeDescriptor) =>
typeDescriptor.HasParseMethod(t), innerTypeDescriptor);
}

public override object? Parse(string value, Type expectedType) => innerTypeDescriptor.Parse(value, expectedType);
}
}
17 changes: 17 additions & 0 deletions YamlDotNet/Serialization/TypeInspectors/CompositeTypeInspector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,22 @@ public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object
return typeInspectors
.SelectMany(i => i.GetProperties(type, container));
}

public override bool HasParseMethod(Type type)
{
return typeInspectors.Any(i => i.HasParseMethod(type));
}

public override object? Parse(string value, Type expectedType)
{
var parsableInspector = typeInspectors.FirstOrDefault(i => i.HasParseMethod(expectedType));

if (parsableInspector == null)
{
throw new ArgumentOutOfRangeException(nameof(expectedType), $"No inspector with a Parse method for type {expectedType.FullName} was found.");
}

return parsableInspector.Parse(value, expectedType);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,9 @@ public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object
return new PropertyDescriptor(p) { Name = namingConvention.Apply(p.Name) };
});
}

public override bool HasParseMethod(Type type) => this.innerTypeDescriptor.HasParseMethod(type);

public override object? Parse(string value, Type expectedType) => this.innerTypeDescriptor.Parse(value, expectedType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object
return innerTypeDescriptor.GetProperties(type, container)
.Where(p => p.CanWrite);
}

public override bool HasParseMethod(Type type) => this.innerTypeDescriptor.HasParseMethod(type);

public override object? Parse(string value, Type expectedType) => this.innerTypeDescriptor.Parse(value, expectedType);
}
}
13 changes: 13 additions & 0 deletions YamlDotNet/Serialization/TypeInspectors/ReflectionTypeInspector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,18 @@ public override string GetEnumValue(object enumValue)
return result!;
}

public override bool HasParseMethod(Type type) => type.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static, null, [typeof(string)], null) != null;

public override object? Parse(string value, Type expectedType)
{
var method = expectedType.GetMethod("Parse", [typeof(string)]);

if (method == null)
{
throw new InvalidOperationException($"Type '{expectedType.FullName}' does not have a static Parse method.");
}

return method.Invoke(null, new object[] { value });
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,9 @@ public IPropertyDescriptor GetProperty(Type type, object? container, string name

return property;
}

public abstract bool HasParseMethod(Type type);

public abstract object? Parse(string value, Type expectedType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,5 @@ public class ReflectionTypeConverter : ITypeConverter
{
public object? ChangeType(object? value, Type expectedType, ITypeInspector typeInspector) => ChangeType(value, expectedType, NullNamingConvention.Instance, typeInspector);
public object? ChangeType(object? value, Type expectedType, INamingConvention enumNamingConvention, ITypeInspector typeInspector) => TypeConverter.ChangeType(value, expectedType, enumNamingConvention, typeInspector);

}
}
Loading
Loading