diff --git a/YamlDotNet.Analyzers.StaticGenerator/StaticTypeInspectorFile.cs b/YamlDotNet.Analyzers.StaticGenerator/StaticTypeInspectorFile.cs index cb9a72d0..29eac0af 100644 --- a/YamlDotNet.Analyzers.StaticGenerator/StaticTypeInspectorFile.cs +++ b/YamlDotNet.Analyzers.StaticGenerator/StaticTypeInspectorFile.cs @@ -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().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() + .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("}"); + } + } + Write("throw new InvalidOperationException($\"Type '{expectedType.FullName}' does not have a static Parse method.\");"); + UnIndent(); Write("}"); + UnIndent(); Write("}"); } diff --git a/YamlDotNet.Core7AoTCompileTest/Program.cs b/YamlDotNet.Core7AoTCompileTest/Program.cs index 56e278f7..ffa8f241 100644 --- a/YamlDotNet.Core7AoTCompileTest/Program.cs +++ b/YamlDotNet.Core7AoTCompileTest/Program.cs @@ -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 @@ -106,6 +107,7 @@ var aotContext = new YamlDotNet.Core7AoTCompileTest.StaticContext(); var deserializer = new StaticDeserializerBuilder(aotContext) + .WithNamingConvention(UnderscoredNamingConvention.Instance) .Build(); var x = deserializer.Deserialize(input); @@ -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 "; diff --git a/YamlDotNet.Test/Serialization/MergingParserTests.cs b/YamlDotNet.Test/Serialization/MergingParserTests.cs index fc84e197..6634910b 100644 --- a/YamlDotNet.Test/Serialization/MergingParserTests.cs +++ b/YamlDotNet.Test/Serialization/MergingParserTests.cs @@ -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] @@ -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() - .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() + .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] diff --git a/YamlDotNet/Serialization/ITypeInspector.cs b/YamlDotNet/Serialization/ITypeInspector.cs index c1603d72..671450d3 100644 --- a/YamlDotNet/Serialization/ITypeInspector.cs +++ b/YamlDotNet/Serialization/ITypeInspector.cs @@ -66,5 +66,19 @@ public interface ITypeInspector /// /// string GetEnumValue(object enumValue); + + /// + /// Indicates whether the specified type has a static Parse method that can be used to convert a string to an instance of that type. + /// + /// The type to check for a static Parse method. + /// True if the type has a static Parse method; otherwise, false. + bool HasParseMethod(Type type); + + /// Converts a string to an instance of the specified type using a static Parse method. + /// + /// The string value to convert. + /// The type to convert the string to. + /// An instance of the specified type. + object? Parse(string value, Type expectedType); } } diff --git a/YamlDotNet/Serialization/NodeDeserializers/ScalarNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/ScalarNodeDeserializer.cs index 5c08e464..5d275a9a 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/ScalarNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/ScalarNodeDeserializer.cs @@ -147,17 +147,9 @@ public bool Deserialize(IParser parser, Type expectedType, Func> cache = new ConcurrentDictionary>(); private readonly ConcurrentDictionary> enumNameCache = new ConcurrentDictionary>(); private readonly ConcurrentDictionary enumValueCache = new ConcurrentDictionary(); + private readonly ConcurrentDictionary hasParseMethodCache = new ConcurrentDictionary(); public CachedTypeInspector(ITypeInspector innerTypeDescriptor) { @@ -74,5 +75,13 @@ public override IEnumerable 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); } } diff --git a/YamlDotNet/Serialization/TypeInspectors/CompositeTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/CompositeTypeInspector.cs index 53c1c80b..6695a75f 100644 --- a/YamlDotNet/Serialization/TypeInspectors/CompositeTypeInspector.cs +++ b/YamlDotNet/Serialization/TypeInspectors/CompositeTypeInspector.cs @@ -86,5 +86,22 @@ public override IEnumerable 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); + } } } diff --git a/YamlDotNet/Serialization/TypeInspectors/NamingConventionTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/NamingConventionTypeInspector.cs index c77bd003..b496ceb5 100644 --- a/YamlDotNet/Serialization/TypeInspectors/NamingConventionTypeInspector.cs +++ b/YamlDotNet/Serialization/TypeInspectors/NamingConventionTypeInspector.cs @@ -57,5 +57,9 @@ public override IEnumerable 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); } } diff --git a/YamlDotNet/Serialization/TypeInspectors/ReadableAndWritablePropertiesTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/ReadableAndWritablePropertiesTypeInspector.cs index 9eac967c..49f3cb57 100644 --- a/YamlDotNet/Serialization/TypeInspectors/ReadableAndWritablePropertiesTypeInspector.cs +++ b/YamlDotNet/Serialization/TypeInspectors/ReadableAndWritablePropertiesTypeInspector.cs @@ -46,5 +46,9 @@ public override IEnumerable 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); } } diff --git a/YamlDotNet/Serialization/TypeInspectors/ReflectionTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/ReflectionTypeInspector.cs index f65c6b53..62a12ff7 100644 --- a/YamlDotNet/Serialization/TypeInspectors/ReflectionTypeInspector.cs +++ b/YamlDotNet/Serialization/TypeInspectors/ReflectionTypeInspector.cs @@ -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 }); + } } } diff --git a/YamlDotNet/Serialization/TypeInspectors/TypeInspectorSkeleton.cs b/YamlDotNet/Serialization/TypeInspectors/TypeInspectorSkeleton.cs index 5f233410..fae5c80b 100644 --- a/YamlDotNet/Serialization/TypeInspectors/TypeInspectorSkeleton.cs +++ b/YamlDotNet/Serialization/TypeInspectors/TypeInspectorSkeleton.cs @@ -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); } } diff --git a/YamlDotNet/Serialization/Utilities/ReflectionTypeConverter.cs b/YamlDotNet/Serialization/Utilities/ReflectionTypeConverter.cs index c42b3f8c..b09aadd6 100644 --- a/YamlDotNet/Serialization/Utilities/ReflectionTypeConverter.cs +++ b/YamlDotNet/Serialization/Utilities/ReflectionTypeConverter.cs @@ -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); - } } diff --git a/YamlDotNet/Serialization/YamlAttributesTypeInspector.cs b/YamlDotNet/Serialization/YamlAttributesTypeInspector.cs index 37df8a90..79b4be26 100644 --- a/YamlDotNet/Serialization/YamlAttributesTypeInspector.cs +++ b/YamlDotNet/Serialization/YamlAttributesTypeInspector.cs @@ -70,5 +70,9 @@ public override IEnumerable GetProperties(Type type, object }) .OrderBy(p => p.Order); } + + public override bool HasParseMethod(Type type) => this.innerTypeDescriptor.HasParseMethod(type); + + public override object? Parse(string value, Type expectedType) => this.innerTypeDescriptor.Parse(value, expectedType); } }