From 25682eb5acb27551b96a45372df53a167b3d9f04 Mon Sep 17 00:00:00 2001 From: Ivan Josipovic <9521987+IvanJosipovic@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:40:23 -0700 Subject: [PATCH 1/7] feat: aot support --- .github/workflows/pr.yaml | 8 +- README.md | 4 +- YamlDotNet.System.Text.Json.slnx | 2 +- .../Examples/BuilderConfigurationExample.cs | 28 -- .../JsonAttributesAndExtensionDataExample.cs | 36 -- samples/Examples/JsonInputExample.cs | 27 -- .../Examples/SerializationOptionsExample.cs | 46 -- samples/Examples/StaticContextExample.cs | 47 -- samples/Program.cs | 41 -- samples/SampleModels.cs | 38 -- ...YamlDotNet.System.Text.Json.Samples.csproj | 18 - .../ExtensionDataPropertyDescriptor.cs | 5 +- .../SystemTextJsonTypeInspector.cs | 103 +++-- .../YamlConverter.cs | 68 ++- .../YamlDotNet.System.Text.Json.csproj | 5 +- .../BuilderExtensionsTests.cs | 17 +- .../DefaultValuesHandlingTests.cs | 25 +- .../EnumTests.cs | 37 +- .../ExtensionDataPropertyDescriptorTests.cs | 29 +- .../ExtensionDataTests.cs | 46 +- .../Fixtures/StaticYaml.cs | 77 ++++ .../Fixtures/SystemTextJsonFixture.cs | 408 ++++++++++++++++++ .../Json2YamlTests.cs | 6 +- .../JsonIgnoreTests.cs | 94 +--- .../JsonPropertyNameTests.cs | 14 +- .../JsonPropertyOrderTests.cs | 20 +- .../StaticContextApiTests.cs | 106 +---- .../SystemTextJsonTypeInspectorTests.cs | 40 +- .../TypeConverterTests.cs | 71 ++- .../Usings.cs | 1 + .../YamlDotNet.System.Text.Json.Tests.csproj | 19 +- .../AnalyzerReleases.Unshipped.md | 7 + .../ClassObject.cs | 71 +++ .../EnumMappings.cs | 39 ++ .../File.cs | 57 +++ .../ObjectAccessorFileGenerator.cs | 110 +++++ .../README.md | 9 + .../SerializableSyntaxReceiver.cs | 230 ++++++++++ .../StaticContextFile.cs | 75 ++++ .../StaticObjectFactoryFile.cs | 273 ++++++++++++ .../StaticPropertyDescriptorFile.cs | 83 ++++ .../StaticTypeInspectorFile.cs | 264 ++++++++++++ .../StaticTypeResolverFile.cs | 51 +++ .../SymbolCollectionExtensions.cs | 36 ++ .../SymbolExtensions.cs | 141 ++++++ .../TypeFactoryGenerator.cs | 294 +++++++++++++ ...amlDotNet.Analyzers.StaticGenerator.csproj | 16 + .../YamlDotNet.LICENSE.txt | 19 + 48 files changed, 2521 insertions(+), 740 deletions(-) delete mode 100644 samples/Examples/BuilderConfigurationExample.cs delete mode 100644 samples/Examples/JsonAttributesAndExtensionDataExample.cs delete mode 100644 samples/Examples/JsonInputExample.cs delete mode 100644 samples/Examples/SerializationOptionsExample.cs delete mode 100644 samples/Examples/StaticContextExample.cs delete mode 100644 samples/Program.cs delete mode 100644 samples/SampleModels.cs delete mode 100644 samples/YamlDotNet.System.Text.Json.Samples.csproj create mode 100644 tests/YamlDotNet.System.Text.Json.Tests/Fixtures/StaticYaml.cs create mode 100644 tests/YamlDotNet.System.Text.Json.Tests/Fixtures/SystemTextJsonFixture.cs create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/AnalyzerReleases.Unshipped.md create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/ClassObject.cs create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/EnumMappings.cs create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/File.cs create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/ObjectAccessorFileGenerator.cs create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/README.md create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/SerializableSyntaxReceiver.cs create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/StaticContextFile.cs create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/StaticObjectFactoryFile.cs create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/StaticPropertyDescriptorFile.cs create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/StaticTypeInspectorFile.cs create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/StaticTypeResolverFile.cs create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/SymbolCollectionExtensions.cs create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/SymbolExtensions.cs create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/TypeFactoryGenerator.cs create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.Analyzers.StaticGenerator.csproj create mode 100644 tools/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.LICENSE.txt diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 5006642..89d0565 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -11,8 +11,8 @@ jobs: contents: read uses: IvanJosipovic/templates/.github/workflows/csharp-build-test.yaml@main - trimmed-consumer: - name: Samples and trimmed consumer smoke test + aot-tests: + name: Native AOT xUnit tests runs-on: ubuntu-latest permissions: contents: read @@ -24,5 +24,5 @@ jobs: with: global-json-file: global.json - - run: dotnet publish samples/YamlDotNet.System.Text.Json.Samples.csproj -f net10.0 -p:TargetFrameworks=net10.0 -r linux-x64 --self-contained true -p:PublishTrimmed=true -p:TrimMode=link -o artifacts/trimming-smoke - - run: ./artifacts/trimming-smoke/YamlDotNet.System.Text.Json.Samples + - run: dotnet publish tests/YamlDotNet.System.Text.Json.Tests/YamlDotNet.System.Text.Json.Tests.csproj -c Aot -f net10.0 -p:TargetFrameworks=net10.0 -r linux-x64 --self-contained true -o artifacts/aot-tests + - run: ./artifacts/aot-tests/YamlDotNet.System.Text.Json.Tests diff --git a/README.md b/README.md index 8bd9320..98f3be7 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,9 @@ The library is trim-analyzed. Its default `YamlConverter` methods and `AddSystem Add the `Vecc.YamlDotNet.Analyzers.StaticGenerator` package to the consuming application, declare a partial `StaticContext` with `[YamlStaticContext]` and `[YamlSerializable(typeof(MyModel))]`, then pass that context to `YamlConverter.Serialize` and `YamlConverter.Deserialize`. Register all application model types used by the context, including enum types. The `AddSystemTextJson` builder extensions also support YamlDotNet's static builder types. -The [`samples`](samples) project demonstrates static-context serialization and deserialization, JSON property attributes, extension data, ordering and default-value options, unmatched-property handling, and direct static-builder configuration. It also runs as the trimmed consumer smoke test in CI. +The test project contains capability-specific shared fixtures and exercises the supported `JsonNode`, `JsonArray`, `JsonObject`, `JsonValue`, `JsonElement`, and `JsonDocument` types, as well as JSON attributes, extension data, naming, ordering, defaults, ignore conditions, enum names, and unmatched properties. CI publishes a focused xUnit Native AOT suite that covers the comprehensive fixture through a generated static context and a source-generated `JsonTypeInfo` converter for included fields and `JsonElement` extension data. + +`SystemTextJsonTypeInspector` reads `System.Text.Json` attributes from model metadata at runtime. A generated YAML static context registers the model descriptors, but trimmed applications must also retain those attributes, for example with `[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(MyModel))]` or by rooting the model assembly. The Native AOT tests apply targeted `DynamicDependency` roots to their registered fixture models. For applications configuring YamlDotNet's static builders directly, register each model type and enum in the generated static context. The context must include public model properties, fields, and constructors, and enum public fields for `JsonStringEnumMemberNameAttribute`. For `IDictionary` extension data, values may be JSON scalars, dictionaries with string keys, sequences, or existing `JsonElement`, `JsonDocument`, or `JsonNode` instances. Arbitrary CLR objects require runtime JSON metadata and are rejected. `IDictionary` extension data retains its values as objects. diff --git a/YamlDotNet.System.Text.Json.slnx b/YamlDotNet.System.Text.Json.slnx index e65733b..8904e8d 100644 --- a/YamlDotNet.System.Text.Json.slnx +++ b/YamlDotNet.System.Text.Json.slnx @@ -5,6 +5,6 @@ - + diff --git a/samples/Examples/BuilderConfigurationExample.cs b/samples/Examples/BuilderConfigurationExample.cs deleted file mode 100644 index a1ff48a..0000000 --- a/samples/Examples/BuilderConfigurationExample.cs +++ /dev/null @@ -1,28 +0,0 @@ -using YamlDotNet.Serialization; -using YamlDotNet.System.Text.Json; - -/// -/// Shows configuring YamlDotNet's static serializer and deserializer builders with this library. -/// POCO properties follow their configured property order; JSON object key sorting is shown in JsonInputExample. -/// -internal static class BuilderConfigurationExample -{ - /// Runs the direct static-builder serialization and deserialization example. - public static void Run() - { - var context = new SamplesYamlContext(); - var model = new ServiceConfiguration { Name = "api", Port = 8080, Mode = ServiceMode.Production }; - var serializer = new StaticSerializerBuilder(context).AddSystemTextJson().Build(); - var deserializer = new StaticDeserializerBuilder(context).AddSystemTextJson().Build(); - - var builderYaml = serializer.Serialize(model); - SampleAssert.YamlEquals(builderYaml, """ - mode: production - display-name: api - port: 8080 - - """, "Static builder configuration should preserve the DTO property order."); - var restored = deserializer.Deserialize(builderYaml); - SampleAssert.That(restored.Name == model.Name, "Direct static-builder round-trip failed."); - } -} diff --git a/samples/Examples/JsonAttributesAndExtensionDataExample.cs b/samples/Examples/JsonAttributesAndExtensionDataExample.cs deleted file mode 100644 index 4db4b6d..0000000 --- a/samples/Examples/JsonAttributesAndExtensionDataExample.cs +++ /dev/null @@ -1,36 +0,0 @@ -using YamlDotNet.System.Text.Json; - -/// -/// Shows how JSON property names, ordering, ignore rules, enum names, and extension data affect YAML output. -/// -internal static class JsonAttributesAndExtensionDataExample -{ - /// Runs examples for JSON property attributes and extension data. - public static void Run() - { - var context = new SamplesYamlContext(); - var model = new ServiceConfiguration - { - Name = "api", - Port = 8080, - Mode = ServiceMode.Production, - ExtensionData = new Dictionary - { - ["owner"] = "platform", - }, - }; - - var yaml = YamlConverter.Serialize(model, context); - SampleAssert.YamlEquals(yaml, """ - mode: production - owner: platform - display-name: api - port: 8080 - - """, "JSON attributes and extension data should produce the expected YAML document."); - - var restored = YamlConverter.Deserialize(yaml, context); - SampleAssert.That(restored.Name == model.Name && restored.Mode == model.Mode, "Convenience API round-trip failed."); - SampleAssert.That(Equals(restored.ExtensionData?["owner"], "platform"), "Extension-data round-trip failed."); - } -} diff --git a/samples/Examples/JsonInputExample.cs b/samples/Examples/JsonInputExample.cs deleted file mode 100644 index 9467ccb..0000000 --- a/samples/Examples/JsonInputExample.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Text.Json; -using YamlDotNet.Serialization; -using YamlDotNet.System.Text.Json; - -/// -/// Shows converting JSON text to YAML with a trim-safe static serializer and JSON parsing options. -/// -internal static class JsonInputExample -{ - /// Runs the JSON document to YAML example using sorted object keys. - public static void Run() - { - using var document = JsonDocument.Parse( - "{\"z\":1,\"a\":2,}", - new JsonDocumentOptions { AllowTrailingCommas = true }); - var serializer = new StaticSerializerBuilder(new SamplesYamlContext()) - .AddSystemTextJson(sortAlphabetically: true) - .Build(); - var yaml = serializer.Serialize(document); - - SampleAssert.YamlEquals(yaml, """ - a: 2 - z: 1 - - """, "The static builder should serialize the parsed JSON document in key order."); - } -} diff --git a/samples/Examples/SerializationOptionsExample.cs b/samples/Examples/SerializationOptionsExample.cs deleted file mode 100644 index daf6393..0000000 --- a/samples/Examples/SerializationOptionsExample.cs +++ /dev/null @@ -1,46 +0,0 @@ -using YamlDotNet.Serialization; -using YamlDotNet.System.Text.Json; - -/// -/// Shows POCO property ordering, default-value handling, and unmatched-property handling. -/// -internal static class SerializationOptionsExample -{ - /// Runs examples for POCO ordering, default omission, and unmatched properties. - public static void Run() - { - var context = new SamplesYamlContext(); - var model = new ServiceConfiguration - { - Name = "api", - Port = 8080, - Mode = ServiceMode.Production, - }; - - var yamlWithJsonKeySortingEnabled = YamlConverter.Serialize(model, context, sortAlphabetically: true); - SampleAssert.YamlEquals(yamlWithJsonKeySortingEnabled, """ - mode: production - display-name: api - port: 8080 - - """, "sortAlphabetically applies to JSON object keys; POCO order follows YamlDotNet defaults and JsonPropertyOrder metadata."); - - var declarationOrderYaml = YamlConverter.Serialize(model, context, ignoreOrder: true); - SampleAssert.YamlEquals(declarationOrderYaml, """ - display-name: api - port: 8080 - mode: production - - """, "Ignoring JsonPropertyOrderAttribute should produce declaration-order YAML."); - - var defaultsYaml = YamlConverter.Serialize(new ServiceConfiguration(), context, defaultValuesHandling: DefaultValuesHandling.OmitDefaults); - SampleAssert.YamlEquals(defaultsYaml, """ - display-name: '' - - """, "OmitDefaults should omit default enum and numeric values."); - - var forwardCompatibleYaml = YamlConverter.Serialize(model, context) + "future-setting: enabled\n"; - var forwardCompatible = YamlConverter.Deserialize(forwardCompatibleYaml, context, ignoreUnmatchedProperties: true); - SampleAssert.That(forwardCompatible.Name == model.Name, "Ignoring unmatched properties failed."); - } -} diff --git a/samples/Examples/StaticContextExample.cs b/samples/Examples/StaticContextExample.cs deleted file mode 100644 index 803b9fb..0000000 --- a/samples/Examples/StaticContextExample.cs +++ /dev/null @@ -1,47 +0,0 @@ -using YamlDotNet.Serialization; -using YamlDotNet.System.Text.Json; - -/// -/// Shows the static-context convenience APIs and their equivalent direct YamlDotNet static-builder configuration. -/// -internal static class StaticContextExample -{ - /// Runs the static-context convenience API and direct-builder examples. - public static void Run() - { - var context = new SamplesYamlContext(); - var model = new ServiceConfiguration - { - Name = "api", - Port = 8080, - Mode = ServiceMode.Production, - ExtensionData = new Dictionary { ["owner"] = "platform" }, - }; - - var yaml = YamlConverter.Serialize(model, context, sortAlphabetically: true); - SampleAssert.YamlEquals(yaml, """ - mode: production - owner: platform - display-name: api - port: 8080 - - """, "Static-context convenience APIs should produce the expected YAML document."); - var restored = YamlConverter.Deserialize(yaml, context); - SampleAssert.That(restored.Name == model.Name, "Static-context round-trip failed."); - SampleAssert.That(Equals(restored.ExtensionData?["owner"], "platform"), "Static-context extension data round-trip failed."); - - var serializer = new StaticSerializerBuilder(context).AddSystemTextJson().Build(); - var deserializer = new StaticDeserializerBuilder(context).AddSystemTextJson().Build(); - var directYaml = serializer.Serialize(model); - SampleAssert.YamlEquals(directYaml, """ - mode: production - owner: platform - display-name: api - port: 8080 - - """, "Direct static builder configuration should produce the expected YAML document."); - var directRoundTrip = deserializer.Deserialize(directYaml); - SampleAssert.That(directRoundTrip.Name == model.Name, "Direct static-builder round-trip failed."); - - } -} diff --git a/samples/Program.cs b/samples/Program.cs deleted file mode 100644 index 47cc98c..0000000 --- a/samples/Program.cs +++ /dev/null @@ -1,41 +0,0 @@ -internal static class Program -{ - private static int Main() - { - JsonAttributesAndExtensionDataExample.Run(); - SerializationOptionsExample.Run(); - JsonInputExample.Run(); - BuilderConfigurationExample.Run(); - StaticContextExample.Run(); - Console.WriteLine("All library samples passed."); - return 0; - } -} - -internal static class SampleAssert -{ - /// Compares complete YAML documents after normalizing line endings. - /// The YAML produced by the sample. - /// The complete expected YAML document. - /// The failure message to display. - public static void YamlEquals(string actual, string expected, string message) - { - var normalizedActual = actual.ReplaceLineEndings("\n"); - var normalizedExpected = expected.ReplaceLineEndings("\n"); - if (!string.Equals(normalizedActual, normalizedExpected, StringComparison.Ordinal)) - { - throw new InvalidOperationException($"{message}\nExpected:\n{normalizedExpected}\nActual:\n{normalizedActual}"); - } - } - - /// Throws when a sample condition is false. - /// The condition that must be true. - /// The failure message to display. - public static void That(bool condition, string message) - { - if (!condition) - { - throw new InvalidOperationException(message); - } - } -} diff --git a/samples/SampleModels.cs b/samples/SampleModels.cs deleted file mode 100644 index d6deee4..0000000 --- a/samples/SampleModels.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Text.Json.Serialization; -using YamlDotNet.Serialization; - -internal sealed class ServiceConfiguration -{ - [JsonPropertyOrder(1)] - [JsonPropertyName("display-name")] - public string Name { get; set; } = string.Empty; - - [JsonPropertyOrder(2)] - [JsonPropertyName("port")] - public int Port { get; set; } - - [JsonPropertyName("mode")] - public ServiceMode Mode { get; set; } - - [JsonIgnore] - public string InternalDescription { get; set; } = "not serialized"; - - [JsonExtensionData] - public Dictionary? ExtensionData { get; set; } -} - -internal enum ServiceMode -{ - [JsonStringEnumMemberName("production")] - Production, - - [JsonStringEnumMemberName("development")] - Development, -} - -[YamlStaticContext] -[YamlSerializable(typeof(ServiceConfiguration))] -[YamlSerializable(typeof(ServiceMode))] -public partial class SamplesYamlContext : StaticContext -{ -} diff --git a/samples/YamlDotNet.System.Text.Json.Samples.csproj b/samples/YamlDotNet.System.Text.Json.Samples.csproj deleted file mode 100644 index 04b3169..0000000 --- a/samples/YamlDotNet.System.Text.Json.Samples.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - Exe - net10.0 - enable - enable - true - true - false - - - - - - - - diff --git a/src/YamlDotNet.System.Text.Json/ExtensionDataPropertyDescriptor.cs b/src/YamlDotNet.System.Text.Json/ExtensionDataPropertyDescriptor.cs index 4b1514e..9a4eede 100644 --- a/src/YamlDotNet.System.Text.Json/ExtensionDataPropertyDescriptor.cs +++ b/src/YamlDotNet.System.Text.Json/ExtensionDataPropertyDescriptor.cs @@ -24,7 +24,10 @@ public ExtensionDataPropertyDescriptor(IPropertyDescriptor baseDescriptor) public bool Required => false; - public Type Type => typeof(object); +#if !NETSTANDARD2_0 + [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "IPropertyDescriptor.Type cannot express interface preservation. StaticContext registrations root the concrete extension-data dictionary type.")] +#endif + public Type Type => GetIDictionaryKVTypes(_baseDescriptor.Type)?.val ?? typeof(object); public Type? TypeOverride { get; set; } diff --git a/src/YamlDotNet.System.Text.Json/SystemTextJsonTypeInspector.cs b/src/YamlDotNet.System.Text.Json/SystemTextJsonTypeInspector.cs index f5a2bc5..440bcf3 100644 --- a/src/YamlDotNet.System.Text.Json/SystemTextJsonTypeInspector.cs +++ b/src/YamlDotNet.System.Text.Json/SystemTextJsonTypeInspector.cs @@ -1,6 +1,7 @@ using System.Reflection; using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; +using System.Runtime.Serialization; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; using YamlDotNet.Core; @@ -136,11 +137,11 @@ private IEnumerable GetPropertyDescriptors(IReadOnlyList(inspectedProperty) == null) - { - var enumDefault = Array.CreateInstance(property.Type, 1).GetValue(0)!; - return [new EnumDefaultValuePropertyDescriptor(descriptor, new global::System.ComponentModel.DefaultValueAttribute(enumDefault))]; - } + if (property.Type.IsEnum && GetCustomAttribute(inspectedProperty) == null) + { + var enumDefault = Enum.ToObject(property.Type, 0); + return [new EnumDefaultValuePropertyDescriptor(descriptor, new global::System.ComponentModel.DefaultValueAttribute(enumDefault))]; + } return [descriptor]; } @@ -155,10 +156,16 @@ private IEnumerable GetPropertyDescriptors(IReadOnlyList p.Order); } - private static bool ShouldIncludeProperty(InspectedProperty inspectedProperty, object? container) - { - var property = inspectedProperty.Descriptor; - var ignore = GetCustomAttribute(inspectedProperty); + private static bool ShouldIncludeProperty(InspectedProperty inspectedProperty, object? container) + { + var property = inspectedProperty.Descriptor; + if (inspectedProperty.ReflectedMember is FieldInfo + && GetCustomAttribute(inspectedProperty) == null) + { + return false; + } + + var ignore = GetCustomAttribute(inspectedProperty); if (ignore == null) { return true; @@ -176,11 +183,29 @@ private static bool ShouldIncludeProperty(InspectedProperty inspectedProperty, o }; } - private static bool IsDefaultValue(IPropertyDescriptor property, object container) - { - var value = property.Read(container).Value; - return value == null || property.Type.IsValueType && value.Equals(Array.CreateInstance(property.Type, 1).GetValue(0)); - } +#if !NETSTANDARD2_0 + [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Generated StaticContext registrations root declared model property types; reflection-based descriptor graphs require callers to preserve their types.")] +#endif + private static bool IsDefaultValue(IPropertyDescriptor property, object container) + { + var value = property.Read(container).Value; + if (value == null) + { + return true; + } + + if (!property.Type.IsValueType || Nullable.GetUnderlyingType(property.Type) != null) + { + return false; + } + +#if NETSTANDARD2_0 + var defaultValue = Array.CreateInstance(property.Type, 1).GetValue(0); +#else + var defaultValue = RuntimeHelpers.GetUninitializedObject(property.Type); +#endif + return value.Equals(defaultValue); + } /// public IPropertyDescriptor GetProperty(Type type, object? container, string name, bool ignoreUnmatched, bool caseInsensitivePropertyMatching) @@ -263,28 +288,36 @@ public bool HasParseMethod(Type type) [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "StaticContext registrations root public DTO properties, but ITypeInspector cannot express that requirement on its Type parameter.")] [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "StaticContext registrations root public DTO properties, but ITypeInspector cannot express that requirement on its Type parameter.")] #endif - private IReadOnlyList GetInspectedProperties(Type type, object? container) - { - return _innerTypeDescriptor.GetProperties(type, container) - .Select(property => new InspectedProperty(property, FindMostDerivedProperty(type, property.Name))) - .ToArray(); + private IReadOnlyList GetInspectedProperties(Type type, object? container) + { + return _innerTypeDescriptor.GetProperties(type, container) + .Select(property => new InspectedProperty(property, FindMostDerivedMember(type, property.Name))) + .ToArray(); } #if !NETSTANDARD2_0 [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "ITypeInspector does not propagate DTO property preservation requirements. StaticContext registrations root public DTO properties.")] [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "ITypeInspector does not propagate DTO property preservation requirements. StaticContext registrations root public DTO properties.")] #endif - private static PropertyInfo? FindMostDerivedProperty(Type type, string name) - { - for (var currentType = type; currentType != null; currentType = currentType.BaseType) - { + private static MemberInfo? FindMostDerivedMember(Type type, string name) + { + for (var currentType = type; currentType != null; currentType = currentType.BaseType) + { var property = currentType .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) .FirstOrDefault(candidate => candidate.Name == name && candidate.GetIndexParameters().Length == 0); - if (property != null) - { - return property; - } + if (property != null) + { + return property; + } + + var field = currentType + .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) + .FirstOrDefault(candidate => candidate.Name == name); + if (field != null) + { + return field; + } } return null; @@ -293,21 +326,21 @@ private IReadOnlyList GetInspectedProperties(Type type, objec private static TAttribute? GetCustomAttribute(InspectedProperty inspectedProperty) where TAttribute : Attribute { - return inspectedProperty.Descriptor.GetCustomAttribute() - ?? inspectedProperty.ReflectedProperty?.GetCustomAttribute(); + return inspectedProperty.Descriptor.GetCustomAttribute() + ?? inspectedProperty.ReflectedMember?.GetCustomAttribute(); } private sealed class InspectedProperty { - public InspectedProperty(IPropertyDescriptor descriptor, PropertyInfo? reflectedProperty) - { - Descriptor = descriptor; - ReflectedProperty = reflectedProperty; + public InspectedProperty(IPropertyDescriptor descriptor, MemberInfo? reflectedMember) + { + Descriptor = descriptor; + ReflectedMember = reflectedMember; } public IPropertyDescriptor Descriptor { get; } - public PropertyInfo? ReflectedProperty { get; } + public MemberInfo? ReflectedMember { get; } } private sealed class EnumDefaultValuePropertyDescriptor : IPropertyDescriptor diff --git a/src/YamlDotNet.System.Text.Json/YamlConverter.cs b/src/YamlDotNet.System.Text.Json/YamlConverter.cs index 3314e51..ae0442f 100644 --- a/src/YamlDotNet.System.Text.Json/YamlConverter.cs +++ b/src/YamlDotNet.System.Text.Json/YamlConverter.cs @@ -14,7 +14,8 @@ namespace YamlDotNet.System.Text.Json; /// All methods are thread-safe and do not maintain any internal state. public static class YamlConverter { - [RequiresUnreferencedCode("YamlDotNet's reflection based serializer is not trim safe.")] + [RequiresUnreferencedCode("YamlDotNet's reflection based serializer is not trim safe.")] + [RequiresDynamicCode("YamlDotNet's reflection based serializer uses dynamic code. Use a generated StaticContext for Native AOT support.")] private static ISerializer GetSerializer(bool sortAlphabetically = false, bool ignoreOrder = false, DefaultValuesHandling defaultValuesHandling = DefaultValuesHandling.Preserve) { return new SerializerBuilder() @@ -23,7 +24,8 @@ private static ISerializer GetSerializer(bool sortAlphabetically = false, bool i .Build(); } - [RequiresUnreferencedCode("YamlDotNet's reflection based deserializer is not trim safe.")] + [RequiresUnreferencedCode("YamlDotNet's reflection based deserializer is not trim safe.")] + [RequiresDynamicCode("YamlDotNet's reflection based deserializer uses dynamic code. Use a generated StaticContext for Native AOT support.")] private static IDeserializer GetDeserializer(bool ignoreUnmatchedProperties = false) { var builder = new DeserializerBuilder() @@ -46,7 +48,8 @@ private static IDeserializer GetDeserializer(bool ignoreUnmatchedProperties = fa /// Specifies how default values are handled during serialization. Determines whether default values are preserved /// or omitted. /// A string containing the serialized representation of the object. - [RequiresUnreferencedCode("YamlDotNet's default serializer uses runtime reflection. Use the overload that accepts a generated StaticContext for trimming support.")] + [RequiresUnreferencedCode("YamlDotNet's default serializer uses runtime reflection. Use the overload that accepts a generated StaticContext for trimming support.")] + [RequiresDynamicCode("YamlDotNet's default serializer uses dynamic code. Use the overload that accepts a generated StaticContext for Native AOT support.")] public static string Serialize(object obj, bool sortAlphabetically = false, bool ignoreOrder = false, DefaultValuesHandling defaultValuesHandling = DefaultValuesHandling.Preserve) { return SerializeCore(obj, sortAlphabetically, ignoreOrder, defaultValuesHandling); @@ -61,7 +64,8 @@ public static string Serialize(object obj, bool sortAlphabetically = false, bool /// Whether to ignore . /// How default values are handled. /// The serialized YAML. - [RequiresUnreferencedCode("YamlDotNet's default serializer uses runtime reflection. Use the overload that accepts a generated StaticContext for trimming support.")] + [RequiresUnreferencedCode("YamlDotNet's default serializer uses runtime reflection. Use the overload that accepts a generated StaticContext for trimming support.")] + [RequiresDynamicCode("YamlDotNet's default serializer uses dynamic code. Use the overload that accepts a generated StaticContext for Native AOT support.")] public static string Serialize(T obj, bool sortAlphabetically = false, bool ignoreOrder = false, DefaultValuesHandling defaultValuesHandling = DefaultValuesHandling.Preserve) { return SerializeCore(obj, sortAlphabetically, ignoreOrder, defaultValuesHandling); @@ -94,7 +98,8 @@ public static string Serialize(T obj, StaticContext context, bool sortAlphabe return serializer.Serialize(obj); } - [RequiresUnreferencedCode("YamlDotNet's reflection based serializer is not trim safe.")] + [RequiresUnreferencedCode("YamlDotNet's reflection based serializer is not trim safe.")] + [RequiresDynamicCode("YamlDotNet's reflection based serializer uses dynamic code. Use a generated StaticContext for Native AOT support.")] private static string SerializeCore(object? obj, bool sortAlphabetically, bool ignoreOrder, DefaultValuesHandling defaultValuesHandling) { var serializer = GetSerializer(sortAlphabetically, ignoreOrder, defaultValuesHandling); @@ -112,10 +117,11 @@ private static string SerializeCore(object? obj, bool sortAlphabetically, bool i /// Specifies how default values are handled during serialization. Use DefaultValuesHandling.Preserve to retain /// default values, or other options to modify this behavior. /// A string containing the serialized representation of the input JSON. - [RequiresUnreferencedCode("YamlDotNet's default serializer uses runtime reflection. Use YamlDotNet's static builder APIs with a generated StaticContext for trimming support.")] - public static string SerializeJson(string json, JsonSerializerOptions? jsonSerializerOptions = null, bool sortAlphabetically = false, bool ignoreOrder = false, DefaultValuesHandling defaultValuesHandling = DefaultValuesHandling.Preserve) - { - var serializer = GetSerializer(sortAlphabetically, ignoreOrder, defaultValuesHandling); + [RequiresUnreferencedCode("YamlDotNet's default serializer uses runtime reflection. Use YamlDotNet's static builder APIs with a generated StaticContext for trimming support.")] + [RequiresDynamicCode("YamlDotNet's default serializer uses dynamic code. Use YamlDotNet's static builder APIs with a generated StaticContext for Native AOT support.")] + public static string SerializeJson(string json, JsonSerializerOptions? jsonSerializerOptions = null, bool sortAlphabetically = false, bool ignoreOrder = false, DefaultValuesHandling defaultValuesHandling = DefaultValuesHandling.Preserve) + { + var serializer = GetSerializer(sortAlphabetically, ignoreOrder, defaultValuesHandling); var documentOptions = jsonSerializerOptions is null ? default : new JsonDocumentOptions @@ -125,8 +131,45 @@ public static string SerializeJson(string json, JsonSerializerOptions? jsonSeria MaxDepth = jsonSerializerOptions.MaxDepth, }; - return serializer.Serialize(JsonDocument.Parse(json, documentOptions)); - } + return serializer.Serialize(JsonDocument.Parse(json, documentOptions)); + } + + /// + /// Serializes JSON to YAML using a generated YamlDotNet static context. + /// + /// The JSON text to serialize. + /// The generated context containing the static YAML metadata. + /// Optional settings used to parse the JSON document. + /// Whether to sort object keys alphabetically. + /// Whether to ignore JSON property order metadata. + /// How default values are handled. + /// The YAML representation of the JSON document. + public static string SerializeJson(string json, StaticContext context, JsonSerializerOptions? jsonSerializerOptions = null, bool sortAlphabetically = false, bool ignoreOrder = false, DefaultValuesHandling defaultValuesHandling = DefaultValuesHandling.Preserve) + { +#if NETSTANDARD2_0 + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } +#else + ArgumentNullException.ThrowIfNull(context); +#endif + var serializer = new StaticSerializerBuilder(context) + .ConfigureDefaultValuesHandling(defaultValuesHandling) + .AddSystemTextJson(sortAlphabetically, ignoreOrder) + .Build(); + var documentOptions = jsonSerializerOptions is null + ? default + : new JsonDocumentOptions + { + AllowTrailingCommas = jsonSerializerOptions.AllowTrailingCommas, + CommentHandling = jsonSerializerOptions.ReadCommentHandling, + MaxDepth = jsonSerializerOptions.MaxDepth, + }; + + using var document = JsonDocument.Parse(json, documentOptions); + return serializer.Serialize(document); + } /// /// Deserializes a YAML string into an object of the specified type. @@ -137,7 +180,8 @@ public static string SerializeJson(string json, JsonSerializerOptions? jsonSeria /// The YAML string to deserialize. Cannot be null. /// Instructs the deserializer to ignore unmatched properties instead of throwing an exception. /// An instance of type T populated with data from the YAML string. - [RequiresUnreferencedCode("YamlDotNet's default deserializer uses runtime reflection. Use the overload that accepts a generated StaticContext for trimming support.")] + [RequiresUnreferencedCode("YamlDotNet's default deserializer uses runtime reflection. Use the overload that accepts a generated StaticContext for trimming support.")] + [RequiresDynamicCode("YamlDotNet's default deserializer uses dynamic code. Use the overload that accepts a generated StaticContext for Native AOT support.")] public static T Deserialize(string yaml, bool ignoreUnmatchedProperties = false) { var deserializer = GetDeserializer(ignoreUnmatchedProperties); diff --git a/src/YamlDotNet.System.Text.Json/YamlDotNet.System.Text.Json.csproj b/src/YamlDotNet.System.Text.Json/YamlDotNet.System.Text.Json.csproj index 44e4aee..71694ba 100644 --- a/src/YamlDotNet.System.Text.Json/YamlDotNet.System.Text.Json.csproj +++ b/src/YamlDotNet.System.Text.Json/YamlDotNet.System.Text.Json.csproj @@ -18,8 +18,9 @@ snupkg true true - true - true + true + true + true false true false diff --git a/tests/YamlDotNet.System.Text.Json.Tests/BuilderExtensionsTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/BuilderExtensionsTests.cs index 6d9dfed..e2b0498 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/BuilderExtensionsTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/BuilderExtensionsTests.cs @@ -1,3 +1,4 @@ +using TestFixtures; using YamlDotNet.Serialization; namespace YamlDotNet.System.Text.Json.Tests; @@ -5,9 +6,9 @@ namespace YamlDotNet.System.Text.Json.Tests; public class BuilderExtensionsTests { [Fact] - public void AddSystemTextJson_OnSerializerBuilder_ReturnsSameInstance() + public void AddSystemTextJson_OnStaticSerializerBuilder_ReturnsSameInstance() { - var builder = new SerializerBuilder(); + var builder = new StaticSerializerBuilder(new SharedYamlContext()); var result = builder.AddSystemTextJson(); @@ -18,9 +19,9 @@ public void AddSystemTextJson_OnSerializerBuilder_ReturnsSameInstance() } [Fact] - public void AddSystemTextJson_OnDeserializerBuilder_ReturnsSameInstance() + public void AddSystemTextJson_OnStaticDeserializerBuilder_ReturnsSameInstance() { - var builder = new DeserializerBuilder(); + var builder = new StaticDeserializerBuilder(new SharedYamlContext()); var result = builder.AddSystemTextJson(); @@ -31,17 +32,17 @@ public void AddSystemTextJson_OnDeserializerBuilder_ReturnsSameInstance() } [Fact] - public void AddSystemTextJson_SerializerBuilderNull_ThrowsArgumentNullException() + public void AddSystemTextJson_StaticSerializerBuilderNull_ThrowsArgumentNullException() { - var exception = Should.Throw(() => BuilderExtensions.AddSystemTextJson((SerializerBuilder)null!)); + var exception = Should.Throw(() => BuilderExtensions.AddSystemTextJson((StaticSerializerBuilder)null!)); exception.ParamName.ShouldBe("builder"); } [Fact] - public void AddSystemTextJson_DeserializerBuilderNull_ThrowsArgumentNullException() + public void AddSystemTextJson_StaticDeserializerBuilderNull_ThrowsArgumentNullException() { - var exception = Should.Throw(() => BuilderExtensions.AddSystemTextJson((DeserializerBuilder)null!)); + var exception = Should.Throw(() => BuilderExtensions.AddSystemTextJson((StaticDeserializerBuilder)null!)); exception.ParamName.ShouldBe("builder"); } diff --git a/tests/YamlDotNet.System.Text.Json.Tests/DefaultValuesHandlingTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/DefaultValuesHandlingTests.cs index d11df47..b8f7eb5 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/DefaultValuesHandlingTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/DefaultValuesHandlingTests.cs @@ -4,26 +4,17 @@ namespace YamlDotNet.System.Text.Json.Tests; public class DefaultValuesHandlingTests { - private sealed class DefaultValuesModel - { - public string? NullableString { get; set; } - - public int Number { get; set; } - - public List Numbers { get; set; } = new(); - } - [Fact] public void Serialize_PreservesDefaultValuesByDefault() { - var model = new DefaultValuesModel + var model = new FixtureModels.DefaultValues.Model { NullableString = null, Number = 0, Numbers = new List() }; - var yaml = YamlConverter.Serialize(model); + var yaml = StaticYaml.Serialize(model); yaml.ShouldContain("NullableString"); yaml.ShouldContain("Number: 0"); @@ -33,14 +24,14 @@ public void Serialize_PreservesDefaultValuesByDefault() [Fact] public void Serialize_OmitsNullValuesWhenConfigured() { - var model = new DefaultValuesModel + var model = new FixtureModels.DefaultValues.Model { NullableString = null, Number = 5, Numbers = new List { 1 } }; - var yaml = YamlConverter.Serialize(model, defaultValuesHandling: DefaultValuesHandling.OmitNull); + var yaml = StaticYaml.Serialize(model, defaultValuesHandling: DefaultValuesHandling.OmitNull); yaml.ShouldNotContain("NullableString"); yaml.ShouldContain("Number: 5"); @@ -50,14 +41,14 @@ public void Serialize_OmitsNullValuesWhenConfigured() [Fact] public void Serialize_OmitsDefaultValuesWhenConfigured() { - var model = new DefaultValuesModel + var model = new FixtureModels.DefaultValues.Model { NullableString = "value", Number = 0, Numbers = new List { 1 } }; - var yaml = YamlConverter.Serialize(model, defaultValuesHandling: DefaultValuesHandling.OmitDefaults); + var yaml = StaticYaml.Serialize(model, defaultValuesHandling: DefaultValuesHandling.OmitDefaults); yaml.ShouldContain("NullableString: value"); yaml.ShouldNotContain("Number:"); @@ -67,14 +58,14 @@ public void Serialize_OmitsDefaultValuesWhenConfigured() [Fact] public void Serialize_OmitsEmptyCollectionsWhenConfigured() { - var model = new DefaultValuesModel + var model = new FixtureModels.DefaultValues.Model { NullableString = "value", Number = 5, Numbers = new List() }; - var yaml = YamlConverter.Serialize(model, defaultValuesHandling: DefaultValuesHandling.OmitEmptyCollections); + var yaml = StaticYaml.Serialize(model, defaultValuesHandling: DefaultValuesHandling.OmitEmptyCollections); yaml.ShouldContain("NullableString: value"); yaml.ShouldContain("Number: 5"); diff --git a/tests/YamlDotNet.System.Text.Json.Tests/EnumTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/EnumTests.cs index dad852a..ce63610 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/EnumTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/EnumTests.cs @@ -4,39 +4,20 @@ namespace YamlDotNet.System.Text.Json.Tests; public class EnumTests { - public class TestModel - { - public TestEnum? Enum { get; set; } - - public TestEnum? EnumName { get; set; } - - public TestEnum? EnumNull { get; set; } - - public IList? EnumList { get; set; } - } - - public enum TestEnum - { - First, - [JsonStringEnumMemberName("val2")] - Second, - Third - } - [Fact] public void EnumSerialize() { - var model = new TestModel() + var model = new FixtureModels.Enums.EnumModel() { - Enum = TestEnum.Third, - EnumName = TestEnum.Second, + Enum = FixtureModels.Enums.TestEnum.Third, + EnumName = FixtureModels.Enums.TestEnum.Second, EnumList = [ - TestEnum.Second + FixtureModels.Enums.TestEnum.Second ] }; - var yaml = YamlConverter.Serialize(model); + var yaml = StaticYaml.Serialize(model); var expected = """ Enum: Third @@ -61,12 +42,12 @@ public void EnumNameDeserialize() """; - var model = YamlConverter.Deserialize(yaml); + var model = StaticYaml.Deserialize(yaml); - model.Enum.ShouldBe(TestEnum.Third); - model.EnumName.ShouldBe(TestEnum.Second); + model.Enum.ShouldBe(FixtureModels.Enums.TestEnum.Third); + model.EnumName.ShouldBe(FixtureModels.Enums.TestEnum.Second); model.EnumNull.ShouldBeNull(); model.EnumList!.Count.ShouldBe(1); - model.EnumList[0].ShouldBe(TestEnum.Second); + model.EnumList[0].ShouldBe(FixtureModels.Enums.TestEnum.Second); } } diff --git a/tests/YamlDotNet.System.Text.Json.Tests/ExtensionDataPropertyDescriptorTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/ExtensionDataPropertyDescriptorTests.cs index 84efe0d..d080af2 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/ExtensionDataPropertyDescriptorTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/ExtensionDataPropertyDescriptorTests.cs @@ -23,7 +23,7 @@ public void PropertiesExposeMutableState() descriptor.AllowNulls.ShouldBeTrue(); descriptor.Name.ShouldBe("ExtensionData"); descriptor.Required.ShouldBeFalse(); - descriptor.Type.ShouldBe(typeof(object)); + descriptor.Type.ShouldBe(typeof(JsonElement)); descriptor.TypeOverride.ShouldBe(typeof(int)); descriptor.ConverterType.ShouldBe(typeof(string)); descriptor.Order.ShouldBe(7); @@ -63,6 +63,7 @@ public void WriteCreatesDictionaryForObjectInterface() var stored = (IDictionary)baseDescriptor.Read(target).Value!; stored.ShouldContainKey("ExtensionData"); stored["ExtensionData"].ShouldBe(42); + descriptor.Type.ShouldBe(typeof(object)); } [Fact] @@ -160,8 +161,8 @@ public void WriteConvertsSupportedJsonScalarsToJsonElement(object? value, string { new DateTime(2020, 1, 2, 3, 4, 5, DateTimeKind.Utc), "\"2020-01-02T03:04:05Z\"" }, { new DateTimeOffset(2020, 1, 2, 3, 4, 5, TimeSpan.Zero), "\"2020-01-02T03:04:05+00:00\"" }, { Guid.Parse("00112233-4455-6677-8899-aabbccddeeff"), "\"00112233-4455-6677-8899-aabbccddeeff\"" }, - { SignedEnum.Negative, "-1" }, - { UnsignedEnum.Maximum, "18446744073709551615" }, + { FixtureModels.ExtensionData.DescriptorEdgeCases.SignedEnum.Negative, "-1" }, + { FixtureModels.ExtensionData.DescriptorEdgeCases.UnsignedEnum.Maximum, "18446744073709551615" }, }; [Fact] @@ -223,7 +224,7 @@ public void WriteUsesExistingExtensionDataDictionary() [Fact] public void WriteRequiresCustomDictionaryToBeInitialized() { - var descriptor = new ExtensionDataPropertyDescriptor(new TestPropertyDescriptor("value", typeof(CustomDictionary))); + var descriptor = new ExtensionDataPropertyDescriptor(new TestPropertyDescriptor("value", typeof(FixtureModels.ExtensionData.DescriptorEdgeCases.CustomDictionary))); Should.Throw(() => descriptor.Write(new object(), 2)) .Message.ShouldContain("Initialize the property before deserialization"); @@ -232,7 +233,7 @@ public void WriteRequiresCustomDictionaryToBeInitialized() [Fact] public void WriteRequiresCustomJsonElementDictionaryToBeInitialized() { - var descriptor = new ExtensionDataPropertyDescriptor(new TestPropertyDescriptor("value", typeof(CustomJsonElementDictionary))); + var descriptor = new ExtensionDataPropertyDescriptor(new TestPropertyDescriptor("value", typeof(FixtureModels.ExtensionData.DescriptorEdgeCases.CustomJsonElementDictionary))); Should.Throw(() => descriptor.Write(new object(), JsonDocument.Parse("null").RootElement)) .Message.ShouldContain("Initialize the property before deserialization"); @@ -248,22 +249,4 @@ public void GetCustomAttributeForwardsToBaseDescriptor() descriptor.GetCustomAttribute().ShouldBeSameAs(attribute); descriptor.GetCustomAttribute().ShouldBeNull(); } - - private enum SignedEnum : long - { - Negative = -1, - } - - private enum UnsignedEnum : ulong - { - Maximum = ulong.MaxValue, - } - - private sealed class CustomDictionary : Dictionary - { - } - - private sealed class CustomJsonElementDictionary : Dictionary - { - } } diff --git a/tests/YamlDotNet.System.Text.Json.Tests/ExtensionDataTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/ExtensionDataTests.cs index 7583915..799cf70 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/ExtensionDataTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/ExtensionDataTests.cs @@ -5,40 +5,18 @@ namespace YamlDotNet.System.Text.Json.Tests; public class ExtensionDataTests { - public class TestJsonExtensionDataModel - { - [JsonExtensionData] - public Dictionary? ExtensionData { get; set; } - } - - public class TestJsonExtensionDataModelObject - { - [JsonExtensionData] - public Dictionary? ExtensionData { get; set; } - } - - public class TestJsonExtensionDataModelMixed - { - public string? before { get; set; } - - [JsonExtensionData] - public Dictionary? ExtensionData { get; set; } - - public string? after { get; set; } - } - [Fact] public void SerializeExtensionData() { - var model = new TestJsonExtensionDataModel() + var model = new FixtureModels.ExtensionData.JsonElementModel() { ExtensionData = new() { - { "test", JsonSerializer.SerializeToElement("test-value") } + { "test", SharedJson.ParseElement("\"test-value\"") } } }; - var yaml = YamlConverter.Serialize(model); + var yaml = StaticYaml.Serialize(model); var expected = """ test: test-value @@ -50,7 +28,7 @@ public void SerializeExtensionData() [Fact] public void SerializeExtensionDataObject() { - var model = new TestJsonExtensionDataModelObject() + var model = new FixtureModels.ExtensionData.ObjectModel() { ExtensionData = new() { @@ -58,7 +36,7 @@ public void SerializeExtensionDataObject() } }; - var yaml = YamlConverter.Serialize(model); + var yaml = StaticYaml.Serialize(model); var expected = """ test: test-value @@ -70,19 +48,19 @@ public void SerializeExtensionDataObject() [Fact] public void SerializeExtensionDataMixed() { - var model = new TestJsonExtensionDataModelMixed() + var model = new FixtureModels.ExtensionData.MixedModel() { before = "test1", ExtensionData = new() { - { "test", JsonSerializer.SerializeToElement("test-value") } + { "test", SharedJson.ParseElement("\"test-value\"") } }, after = "test2" }; - var yaml = YamlConverter.Serialize(model); + var yaml = StaticYaml.Serialize(model); var expected = """ before: test1 @@ -101,7 +79,7 @@ public void DeserializeExtensionData() """; - var model = YamlConverter.Deserialize(yaml); + var model = StaticYaml.Deserialize(yaml); model.ExtensionData!.Count.ShouldBe(1); model.ExtensionData["test"].GetString().ShouldBe("test-value"); @@ -119,7 +97,7 @@ public void DeserializeNestedExtensionData() """; - var model = YamlConverter.Deserialize(yaml); + var model = StaticYaml.Deserialize(yaml); var nested = model.ExtensionData!["nested"]; nested.GetProperty("test").GetString().ShouldBe("test-value"); @@ -134,7 +112,7 @@ public void DeserializeExtensionDataObject() """; - var model = YamlConverter.Deserialize(yaml); + var model = StaticYaml.Deserialize(yaml); model.ExtensionData!.Count.ShouldBe(1); model.ExtensionData["test"].ShouldBe("test-value"); @@ -150,7 +128,7 @@ public void DeserializeExtensionDataMixed() """; - var model = YamlConverter.Deserialize(yaml); + var model = StaticYaml.Deserialize(yaml); model.before.ShouldBe("test1"); model.after.ShouldBe("test2"); diff --git a/tests/YamlDotNet.System.Text.Json.Tests/Fixtures/StaticYaml.cs b/tests/YamlDotNet.System.Text.Json.Tests/Fixtures/StaticYaml.cs new file mode 100644 index 0000000..33a0dbc --- /dev/null +++ b/tests/YamlDotNet.System.Text.Json.Tests/Fixtures/StaticYaml.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using System.Diagnostics.CodeAnalysis; +using TestFixtures; +using YamlDotNet.Serialization; + +namespace YamlDotNet.System.Text.Json.Tests; + +internal static class StaticYaml +{ + private static readonly SharedYamlContext Context = new(); + + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(ComprehensiveConfiguration))] + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicFields, typeof(FixtureMode))] + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(StaticApiProbe))] + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicFields, typeof(StaticApiProbeMode))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.ConfigurationAndAttributes.PropertyNameModel))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.ConfigurationAndAttributes.PropertyOrderModel))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.IgnoreConditions.ConditionsModel))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.IgnoreConditions.DirectionalModel))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.Enums.EnumModel))] + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicFields, typeof(FixtureModels.Enums.TestEnum))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.ExtensionData.JsonElementModel))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.ExtensionData.ObjectModel))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.ExtensionData.MixedModel))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.ExtensionData.ContainerWithJsonElementExtensionData))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.ExtensionData.DescriptorEdgeCases.CustomDictionary))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.ExtensionData.DescriptorEdgeCases.CustomJsonElementDictionary))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.DefaultValues.Model))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.TypeInspectorInheritance.BaseHiddenPropertyModel))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.TypeInspectorInheritance.DerivedHiddenPropertyModel))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.TypeInspectorInheritance.BaseUnannotatedPropertyModel))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.TypeInspectorInheritance.DerivedUnannotatedPropertyModel))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.UnmatchedProperties.V1ObjectMeta))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.UnmatchedProperties.V1CustomResourceDefinition))] + static StaticYaml() + { + } + + public static string Serialize( + T value, + bool sortAlphabetically = false, + bool ignoreOrder = false, + DefaultValuesHandling defaultValuesHandling = DefaultValuesHandling.Preserve) + { + return YamlConverter.Serialize(value, Context, sortAlphabetically, ignoreOrder, defaultValuesHandling); + } + + public static T Deserialize(string yaml, bool ignoreUnmatchedProperties = false) + { + return YamlConverter.Deserialize(yaml, Context, ignoreUnmatchedProperties); + } + + public static string SerializeJson( + string json, + JsonSerializerOptions? jsonSerializerOptions = null, + bool sortAlphabetically = false, + bool ignoreOrder = false, + DefaultValuesHandling defaultValuesHandling = DefaultValuesHandling.Preserve) + { + return YamlConverter.SerializeJson( + json, + Context, + jsonSerializerOptions, + sortAlphabetically, + ignoreOrder, + defaultValuesHandling); + } +} + +internal static class SharedJson +{ + public static JsonElement ParseElement(string json) + { + using var document = JsonDocument.Parse(json); + return document.RootElement.Clone(); + } +} diff --git a/tests/YamlDotNet.System.Text.Json.Tests/Fixtures/SystemTextJsonFixture.cs b/tests/YamlDotNet.System.Text.Json.Tests/Fixtures/SystemTextJsonFixture.cs new file mode 100644 index 0000000..131455b --- /dev/null +++ b/tests/YamlDotNet.System.Text.Json.Tests/Fixtures/SystemTextJsonFixture.cs @@ -0,0 +1,408 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace TestFixtures; + +/// Rich trim-safe model shared by integration and Native AOT tests. +public sealed class ComprehensiveConfiguration +{ + [JsonPropertyOrder(1)] + [JsonPropertyName("display-name")] + public string Name { get; set; } = "api"; + + [JsonPropertyOrder(2)] + [JsonPropertyName("port")] + public int Port { get; set; } = 8080; + + [JsonPropertyName("mode")] + public FixtureMode Mode { get; set; } = FixtureMode.Production; + + [JsonIgnore] + public string InternalDescription { get; set; } = "internal"; + + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public string AlwaysIgnored { get; set; } = "hidden"; + + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string NeverIgnored { get; set; } = "visible"; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? OptionalLabel { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int RetryLimit { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWriting)] + public string InputOnly { get; set; } = "input-default"; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenReading)] + public string OutputOnly { get; set; } = "output-value"; + + [JsonExtensionData] + public Dictionary? ExtensionData { get; set; } = new() + { + ["owner"] = "platform", + ["future-setting"] = "enabled", + }; +} + +public enum FixtureMode +{ + [JsonStringEnumMemberName("production")] + Production, + + [JsonStringEnumMemberName("development")] + Development, +} + +/// Registered model used by tests for unmatched-property behavior. +public sealed class StaticApiProbe +{ + [JsonPropertyName("display-name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("mode")] + public StaticApiProbeMode Mode { get; set; } +} + +public enum StaticApiProbeMode +{ + [JsonStringEnumMemberName("production")] + Production, +} + +/// Test-only model fixtures organized by the capability they exercise. +public static class FixtureModels +{ + public static class ConfigurationAndAttributes + { + public sealed class PropertyNameModel + { + [JsonPropertyName("MyNewPropName")] + public string MyProp { get; set; } = nameof(MyProp); + + public string MyProp2 { get; set; } = nameof(MyProp2); + } + + public sealed class PropertyOrderModel + { + [JsonPropertyOrder(3)] + public string MyProp { get; set; } = nameof(MyProp); + + [JsonPropertyOrder(2)] + public string MyProp2 { get; set; } = nameof(MyProp2); + + [JsonPropertyOrder(1)] + public string MyProp3 { get; set; } = nameof(MyProp3); + } + } + + public static class IgnoreConditions + { + public sealed class ConditionsModel + { + public string MyProp { get; set; } = nameof(MyProp); + + [JsonIgnore] + public string Hide { get; set; } = nameof(Hide); + + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public string Hide2 { get; set; } = nameof(Hide2); + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Show { get; set; } = nameof(Show); + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Show2 { get; set; } = nameof(Show2); + + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string Show3 { get; set; } = nameof(Show3); + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? HideWhenNull { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string ShowWhenNotNull { get; set; } = nameof(ShowWhenNotNull); + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int HideWhenDefault { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int ShowWhenNotDefault { get; set; } = 42; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List ShowWhenEmpty { get; set; } = []; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int? HideNullableWhenDefault { get; set; } = 0; + } + + public sealed class DirectionalModel + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWriting)] + public string WriteOnly { get; set; } = nameof(WriteOnly); + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenReading)] + public string ReadOnly { get; set; } = nameof(ReadOnly); + + [JsonInclude] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWriting)] + public string IncludedWriteOnlyField = nameof(IncludedWriteOnlyField); + + [JsonInclude] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenReading)] + public string IncludedReadOnlyField = nameof(IncludedReadOnlyField); + + [JsonInclude] + [JsonPropertyName("aliasedField")] + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public string IgnoredAliasedField = nameof(IgnoredAliasedField); + + [JsonInclude] + [JsonPropertyName("extensionKey")] + public string ExtensionKey = nameof(ExtensionKey); + + [JsonInclude] + [JsonExtensionData] + public Dictionary ExtensionData = []; + } + } + + public static class Enums + { + public sealed class EnumModel + { + public TestEnum? Enum { get; set; } + + public TestEnum? EnumName { get; set; } + + public TestEnum? EnumNull { get; set; } + + public IList? EnumList { get; set; } + } + + public enum TestEnum + { + First, + + [JsonStringEnumMemberName("val2")] + Second, + + Third, + } + + } + + public static class ExtensionData + { + public sealed class JsonElementModel + { +#pragma warning disable YDNG001 // JsonElement values are handled by SystemTextJsonYamlTypeConverter. + [JsonExtensionData] + public Dictionary? ExtensionData { get; set; } +#pragma warning restore YDNG001 + } + + public sealed class ObjectModel + { + [JsonExtensionData] + public Dictionary? ExtensionData { get; set; } + } + + public sealed class MixedModel + { + public string? before { get; set; } + +#pragma warning disable YDNG001 // JsonElement values are handled by SystemTextJsonYamlTypeConverter. + [JsonExtensionData] + public Dictionary? ExtensionData { get; set; } +#pragma warning restore YDNG001 + + public string? after { get; set; } + } + + public sealed class ContainerWithJsonElementExtensionData + { +#pragma warning disable YDNG001 // JsonElement values are handled by SystemTextJsonYamlTypeConverter. + public IDictionary ExtensionData { get; set; } = new Dictionary(); +#pragma warning restore YDNG001 + } + + public static class DescriptorEdgeCases + { + public enum SignedEnum : long + { + Negative = -1, + } + + public enum UnsignedEnum : ulong + { + Maximum = ulong.MaxValue, + } + + public sealed class CustomDictionary : Dictionary + { + } + + public sealed class CustomJsonElementDictionary : Dictionary + { + } + } + } + + public static class DefaultValues + { + public sealed class Model + { + public string? NullableString { get; set; } + + public int Number { get; set; } + + public List Numbers { get; set; } = new(); + } + } + + public static class TypeInspectorInheritance + { + public class BaseHiddenPropertyModel + { + [JsonPropertyName("base-value")] + public object Value { get; set; } = "base"; + } + + public sealed class DerivedHiddenPropertyModel : BaseHiddenPropertyModel + { + [JsonPropertyName("derived-value")] + public new string Value { get; set; } = "derived"; + } + + public class BaseUnannotatedPropertyModel + { + public int Id { get; set; } = 1; + } + + public sealed class DerivedUnannotatedPropertyModel : BaseUnannotatedPropertyModel + { + public new string Id { get; set; } = "derived"; + } + } + + public static class UnmatchedProperties + { + public sealed class V1ObjectMeta + { + [JsonPropertyName("name")] + public string Name { get; set; } = default!; + } + + public sealed class V1CustomResourceDefinition + { + [JsonPropertyName("apiVersion")] + public string ApiVersion { get; set; } = default!; + + [JsonPropertyName("kind")] + public string Kind { get; set; } = default!; + + [JsonPropertyName("metadata")] + public V1ObjectMeta Metadata { get; set; } = default!; + } + } +} + +/// Builds and owns the disposable JSON document used by the canonical fixture. +public sealed class FixtureData : IDisposable +{ + public ComprehensiveConfiguration Model { get; } + + public JsonNode Node { get; } + + public JsonArray Array { get; } + + public JsonObject Object { get; } + + public JsonValue Value { get; } + + public JsonElement Element { get; } + + public JsonDocument Document { get; } + + public FixtureData() + { + Node = JsonNode.Parse("""{"enabled":true,"region":"west"}""")!; + Array = JsonNode.Parse("""["blue",2,false]""")!.AsArray(); + Object = JsonNode.Parse("""{"owner":"platform","retries":3}""")!.AsObject(); + Value = JsonValue.Create("ready")!; + Element = ParseElement("""{"endpoint":"https://example.test","healthy":true}"""); + Document = JsonDocument.Parse("""{"version":1,"items":["alpha","beta"]}"""); + Model = new ComprehensiveConfiguration(); + } + + public void Dispose() + { + Document.Dispose(); + } + + private static JsonElement ParseElement(string json) + { + using var document = JsonDocument.Parse(json); + return document.RootElement.Clone(); + } +} + +[YamlStaticContext] +[YamlSerializable(typeof(ComprehensiveConfiguration))] +[YamlSerializable(typeof(FixtureMode))] +[YamlSerializable(typeof(StaticApiProbe))] +[YamlSerializable(typeof(StaticApiProbeMode))] +[YamlSerializable(typeof(FixtureModels.ConfigurationAndAttributes.PropertyNameModel))] +[YamlSerializable(typeof(FixtureModels.ConfigurationAndAttributes.PropertyOrderModel))] +[YamlSerializable(typeof(FixtureModels.IgnoreConditions.ConditionsModel))] +[YamlSerializable(typeof(FixtureModels.IgnoreConditions.DirectionalModel))] +[YamlSerializable(typeof(FixtureModels.Enums.EnumModel))] +[YamlSerializable(typeof(FixtureModels.Enums.TestEnum))] +[YamlSerializable(typeof(FixtureModels.ExtensionData.JsonElementModel))] +[YamlSerializable(typeof(FixtureModels.ExtensionData.ObjectModel))] +[YamlSerializable(typeof(FixtureModels.ExtensionData.MixedModel))] +[YamlSerializable(typeof(FixtureModels.ExtensionData.ContainerWithJsonElementExtensionData))] +[YamlSerializable(typeof(FixtureModels.ExtensionData.DescriptorEdgeCases.SignedEnum))] +[YamlSerializable(typeof(FixtureModels.ExtensionData.DescriptorEdgeCases.UnsignedEnum))] +[YamlSerializable(typeof(FixtureModels.DefaultValues.Model))] +[YamlSerializable(typeof(FixtureModels.TypeInspectorInheritance.BaseHiddenPropertyModel))] +[YamlSerializable(typeof(FixtureModels.TypeInspectorInheritance.DerivedHiddenPropertyModel))] +[YamlSerializable(typeof(FixtureModels.TypeInspectorInheritance.BaseUnannotatedPropertyModel))] +[YamlSerializable(typeof(FixtureModels.TypeInspectorInheritance.DerivedUnannotatedPropertyModel))] +[YamlSerializable(typeof(FixtureModels.UnmatchedProperties.V1ObjectMeta))] +[YamlSerializable(typeof(FixtureModels.UnmatchedProperties.V1CustomResourceDefinition))] +public partial class SharedYamlContext : StaticContext +{ +} + +/// Shared semantic assertions for sample and test round-trips. +public static class FixtureAssertions +{ + public static void Equivalent( + ComprehensiveConfiguration expected, + ComprehensiveConfiguration actual) + { + Assert(expected.Name == actual.Name, "The renamed property should round-trip."); + Assert(expected.Port == actual.Port, "The ordered numeric property should round-trip."); + Assert(expected.Mode == actual.Mode, "The enum and custom member name should round-trip."); + Assert(actual.InternalDescription == "internal", "Always-ignored properties should retain their initializer."); + Assert(actual.AlwaysIgnored == "hidden", "Always-ignored properties should retain their initializer."); + Assert(actual.NeverIgnored == expected.NeverIgnored, "Never-ignored properties should round-trip."); + Assert(actual.InputOnly == expected.InputOnly, "Write-only properties should remain readable from YAML."); + Assert(actual.OutputOnly == "output-value", "Read-only properties should ignore YAML input."); + Assert(actual.ExtensionData?["owner"] as string == "platform", "Object extension data should round-trip."); + Assert(actual.ExtensionData?["future-setting"] as string == "enabled", "Additional object extension data should round-trip."); + } + + private static void Assert(bool condition, string message) + { + if (!condition) + { + throw new InvalidOperationException(message); + } + } +} diff --git a/tests/YamlDotNet.System.Text.Json.Tests/Json2YamlTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/Json2YamlTests.cs index 16f964f..792ce14 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/Json2YamlTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/Json2YamlTests.cs @@ -14,7 +14,7 @@ public void Test1() } """; - var yaml = YamlConverter.SerializeJson(json); + var yaml = StaticYaml.SerializeJson(json); var expected = """ Temperature: '25' @@ -33,7 +33,7 @@ public void Test2() } """; - var yaml = YamlConverter.SerializeJson(json, sortAlphabetically: true); + var yaml = StaticYaml.SerializeJson(json, sortAlphabetically: true); var expected = """ a: '1' @@ -51,7 +51,7 @@ public void SerializeJson_UsesJsonDocumentParsingOptions() AllowTrailingCommas = true, }; - var yaml = YamlConverter.SerializeJson("{\"value\":\"text\",}", options); + var yaml = StaticYaml.SerializeJson("{\"value\":\"text\",}", options); yaml.ShouldContain("value: text"); } diff --git a/tests/YamlDotNet.System.Text.Json.Tests/JsonIgnoreTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/JsonIgnoreTests.cs index 947a62e..871f68f 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/JsonIgnoreTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/JsonIgnoreTests.cs @@ -4,80 +4,12 @@ namespace YamlDotNet.System.Text.Json.Tests; public partial class JsonIgnoreTests { - public class TestModel - { - public string MyProp { get; set; } = nameof(TestModel.MyProp); - - [JsonIgnore()] - public string Hide { get; set; } = nameof(TestModel.Hide); - - [JsonIgnore(Condition = JsonIgnoreCondition.Always)] - public string Hide2 { get; set; } = nameof(TestModel.Hide2); - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Show { get; set; } = nameof(TestModel.Show); - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string Show2 { get; set; } = nameof(TestModel.Show2); - - [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string Show3 { get; set; } = nameof(TestModel.Show3); - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? HideWhenNull { get; set; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string ShowWhenNotNull { get; set; } = nameof(ShowWhenNotNull); - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public int HideWhenDefault { get; set; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public int ShowWhenNotDefault { get; set; } = 42; - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public List ShowWhenEmpty { get; set; } = []; - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public int? HideNullableWhenDefault { get; set; } = 0; - } - - public class DirectionalTestModel - { - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWriting)] - public string WriteOnly { get; set; } = nameof(WriteOnly); - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenReading)] - public string ReadOnly { get; set; } = nameof(ReadOnly); - - [JsonInclude] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWriting)] - public string IncludedWriteOnlyField = nameof(IncludedWriteOnlyField); - - [JsonInclude] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenReading)] - public string IncludedReadOnlyField = nameof(IncludedReadOnlyField); - - [JsonInclude] - [JsonPropertyName("aliasedField")] - [JsonIgnore(Condition = JsonIgnoreCondition.Always)] - public string IgnoredAliasedField = nameof(IgnoredAliasedField); - - [JsonInclude] - [JsonPropertyName("extensionKey")] - public string ExtensionKey = nameof(ExtensionKey); - - [JsonInclude] - [JsonExtensionData] - public Dictionary ExtensionData = []; - } - [Fact] public void Serialize() { - var model = new TestModel(); + var model = new FixtureModels.IgnoreConditions.ConditionsModel(); - var yaml = YamlConverter.Serialize(model); + var yaml = StaticYaml.Serialize(model); var expected = """ MyProp: MyProp @@ -104,7 +36,7 @@ public void Deserialize() """; - var model = YamlConverter.Deserialize(yaml); + var model = StaticYaml.Deserialize(yaml); model.MyProp.ShouldBe("test"); model.Show.ShouldBe("test5"); @@ -115,9 +47,9 @@ public void Deserialize() [Fact] public void Serialize_RespectsConditionalIgnoreConditions() { - var model = new TestModel(); + var model = new FixtureModels.IgnoreConditions.ConditionsModel(); - var yaml = YamlConverter.Serialize(model); + var yaml = StaticYaml.Serialize(model); yaml.ShouldNotContain("HideWhenNull"); yaml.ShouldContain("ShowWhenNotNull: ShowWhenNotNull"); @@ -142,21 +74,21 @@ public void Deserialize_ConditionalIgnoreConditionsStillReadValues() """; - var model = YamlConverter.Deserialize(yaml); + var model = StaticYaml.Deserialize(yaml); model.HideWhenNull.ShouldBe("supplied"); model.ShowWhenNotNull.ShouldBe("supplied2"); model.HideWhenDefault.ShouldBe(7); model.ShowWhenNotDefault.ShouldBe(8); model.ShowWhenEmpty.ShouldBe([1]); - model.Hide.ShouldBe(nameof(TestModel.Hide)); - model.Hide2.ShouldBe(nameof(TestModel.Hide2)); + model.Hide.ShouldBe(nameof(FixtureModels.IgnoreConditions.ConditionsModel.Hide)); + model.Hide2.ShouldBe(nameof(FixtureModels.IgnoreConditions.ConditionsModel.Hide2)); } [Fact] public void Serialize_RespectsDirectionalIgnoreConditions() { - var yaml = YamlConverter.Serialize(new DirectionalTestModel()); + var yaml = StaticYaml.Serialize(new FixtureModels.IgnoreConditions.DirectionalModel()); yaml.ShouldNotContain("WriteOnly"); yaml.ShouldContain("ReadOnly: ReadOnly"); @@ -179,13 +111,13 @@ public void Deserialize_RespectsDirectionalIgnoreConditions() """; - var model = YamlConverter.Deserialize(yaml); + var model = StaticYaml.Deserialize(yaml); model.WriteOnly.ShouldBe("supplied"); - model.ReadOnly.ShouldBe(nameof(DirectionalTestModel.ReadOnly)); + model.ReadOnly.ShouldBe(nameof(FixtureModels.IgnoreConditions.DirectionalModel.ReadOnly)); model.IncludedWriteOnlyField.ShouldBe("supplied-field"); - model.IncludedReadOnlyField.ShouldBe(nameof(DirectionalTestModel.IncludedReadOnlyField)); - model.IgnoredAliasedField.ShouldBe(nameof(DirectionalTestModel.IgnoredAliasedField)); + model.IncludedReadOnlyField.ShouldBe(nameof(FixtureModels.IgnoreConditions.DirectionalModel.IncludedReadOnlyField)); + model.IgnoredAliasedField.ShouldBe(nameof(FixtureModels.IgnoreConditions.DirectionalModel.IgnoredAliasedField)); model.ExtensionKey.ShouldBe("extension-value"); model.ExtensionData.ShouldContainKeyAndValue("extraKey", "extra-value"); } diff --git a/tests/YamlDotNet.System.Text.Json.Tests/JsonPropertyNameTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/JsonPropertyNameTests.cs index 4d61bb8..eab7100 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/JsonPropertyNameTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/JsonPropertyNameTests.cs @@ -4,20 +4,12 @@ namespace YamlDotNet.System.Text.Json.Tests; public partial class JsonPropertyNameTests { - public class TestModel - { - [JsonPropertyName("MyNewPropName")] - public string MyProp { get; set; } = nameof(TestModel.MyProp); - - public string MyProp2 { get; set; } = nameof(TestModel.MyProp2); - } - [Fact] public void Serialize() { - var model = new TestModel(); + var model = new FixtureModels.ConfigurationAndAttributes.PropertyNameModel(); - var yaml = YamlConverter.Serialize(model); + var yaml = StaticYaml.Serialize(model); var expected = """ MyNewPropName: MyProp @@ -36,7 +28,7 @@ public void Deserialize() """; - var model = YamlConverter.Deserialize(yaml); + var model = StaticYaml.Deserialize(yaml); model.MyProp.ShouldBe("test"); model.MyProp2.ShouldBe("test2"); diff --git a/tests/YamlDotNet.System.Text.Json.Tests/JsonPropertyOrderTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/JsonPropertyOrderTests.cs index 86e0c96..db6b3f6 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/JsonPropertyOrderTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/JsonPropertyOrderTests.cs @@ -4,24 +4,12 @@ namespace YamlDotNet.System.Text.Json.Tests; public class JsonPropertyOrderTests { - public class TestModel2 - { - [JsonPropertyOrder(3)] - public string MyProp { get; set; } = nameof(MyProp); - - [JsonPropertyOrder(2)] - public string MyProp2 { get; set; } = nameof(MyProp2); - - [JsonPropertyOrder(1)] - public string MyProp3 { get; set; } = nameof(MyProp3); - } - [Fact] public void PropertyOrder() { - var model = new TestModel2(); + var model = new FixtureModels.ConfigurationAndAttributes.PropertyOrderModel(); - var yaml = YamlConverter.Serialize(model); + var yaml = StaticYaml.Serialize(model); var expected = """ MyProp3: MyProp3 @@ -35,9 +23,9 @@ public void PropertyOrder() [Fact] public void DisablePropertyOrder() { - var model = new TestModel2(); + var model = new FixtureModels.ConfigurationAndAttributes.PropertyOrderModel(); - var yaml = YamlConverter.Serialize(model, ignoreOrder: true); + var yaml = StaticYaml.Serialize(model, ignoreOrder: true); var expected = """ MyProp: MyProp diff --git a/tests/YamlDotNet.System.Text.Json.Tests/StaticContextApiTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/StaticContextApiTests.cs index 574ffd6..617d69a 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/StaticContextApiTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/StaticContextApiTests.cs @@ -1,51 +1,10 @@ using System.Text.Json; -using System.Text.Json.Serialization; -using YamlDotNet.Serialization; -using YamlDotNet.System.Text.Json; +using TestFixtures; + +namespace YamlDotNet.System.Text.Json.Tests; public class StaticContextApiTests { - [Fact] - public void YamlConverterStaticContextOverloadsRoundTripDtoAndExtensionData() - { - var context = new TestYamlContext(); - var value = new StaticContextModel - { - Name = "api", - Mode = StaticContextMode.Production, - ExtensionData = new Dictionary - { - ["owner"] = "platform", - }, - }; - - var yaml = YamlConverter.Serialize(value, context, sortAlphabetically: true); - - yaml.ReplaceLineEndings().ShouldBe(""" - display-name: api - mode: production - owner: platform - - """.ReplaceLineEndings()); - var restored = YamlConverter.Deserialize(yaml, context); - restored.Name.ShouldBe("api"); - restored.Mode.ShouldBe(StaticContextMode.Production); - restored.ExtensionData!["owner"].ShouldBe("platform"); - } - - [Fact] - public void BuilderExtensionsConfigureStaticBuilders() - { - var context = new TestYamlContext(); - var serializerBuilder = new StaticSerializerBuilder(context); - serializerBuilder.AddSystemTextJson().ShouldBeSameAs(serializerBuilder); - serializerBuilder.Build().ShouldNotBeNull(); - - var deserializerBuilder = new StaticDeserializerBuilder(context); - deserializerBuilder.AddSystemTextJson().ShouldBeSameAs(deserializerBuilder); - deserializerBuilder.Build().ShouldNotBeNull(); - } - [Fact] public void SerializeJsonUsesSuppliedDocumentOptions() { @@ -60,67 +19,14 @@ public void SerializeJsonUsesSuppliedDocumentOptions() { // accepted by the supplied options "name": "api", } - """, options).ShouldBe("name: api" + Environment.NewLine); + """, new SharedYamlContext(), options).ShouldBe("name: api" + Environment.NewLine); } [Fact] - public void ReflectionConvenienceOverloadRemainsAvailable() + public void SerializeUsesSuppliedStaticContext() { - var yaml = YamlConverter.Serialize((object)new StaticContextModel { Name = "api" }); + var yaml = YamlConverter.Serialize(new ComprehensiveConfiguration { Name = "api" }, new SharedYamlContext()); yaml.ShouldContain("display-name: api"); } - - [Fact] - public void StaticDeserializerCanIgnoreUnmatchedProperties() - { - var model = YamlConverter.Deserialize("display-name: api\nunknown: value\n", new TestYamlContext(), ignoreUnmatchedProperties: true); - - model.Name.ShouldBe("api"); - } - - [Fact] - public void OmitDefaultsOmitsDefaultEnumValue() - { - var context = new TestYamlContext(); - var yaml = YamlConverter.Serialize( - new StaticContextModel(), - context, - defaultValuesHandling: DefaultValuesHandling.OmitDefaults); - - yaml.ShouldNotContain("mode:"); - - var directYaml = new StaticSerializerBuilder(context) - .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) - .AddSystemTextJson() - .Build() - .Serialize(new StaticContextModel()); - directYaml.ShouldNotContain("mode:"); - } - -} - -public sealed class StaticContextModel -{ - [JsonPropertyName("display-name")] - public string Name { get; set; } = string.Empty; - - [JsonPropertyName("mode")] - public StaticContextMode Mode { get; set; } - - [JsonExtensionData] - public Dictionary? ExtensionData { get; set; } -} - -public enum StaticContextMode -{ - [JsonStringEnumMemberName("production")] - Production, -} - -[YamlStaticContext] -[YamlSerializable(typeof(StaticContextModel))] -[YamlSerializable(typeof(StaticContextMode))] -public sealed partial class TestYamlContext : StaticContext -{ } diff --git a/tests/YamlDotNet.System.Text.Json.Tests/SystemTextJsonTypeInspectorTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/SystemTextJsonTypeInspectorTests.cs index 2617e9a..e4d6422 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/SystemTextJsonTypeInspectorTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/SystemTextJsonTypeInspectorTests.cs @@ -26,8 +26,8 @@ public void GetProperty_UsesCaseInsensitiveMatching() [Fact] public void GetProperty_ReturnsExtensionDescriptorWhenMissing() { - var container = new ContainerWithExtensionData(); - container.ExtensionData["dynamic"] = JsonSerializer.SerializeToElement("value"); + var container = new FixtureModels.ExtensionData.ContainerWithJsonElementExtensionData(); + container.ExtensionData["dynamic"] = SharedJson.ParseElement("\"value\""); var attributes = new Attribute[] { new JsonExtensionDataAttribute() }; var extensionDescriptor = new TestPropertyDescriptor( @@ -45,7 +45,7 @@ public void GetProperty_ReturnsExtensionDescriptorWhenMissing() var inspector = new SystemTextJsonTypeInspector(new StubTypeInspector(new[] { extensionDescriptor })); - var result = inspector.GetProperty(typeof(ContainerWithExtensionData), container, "newKey", ignoreUnmatched: true, caseInsensitivePropertyMatching: false); + var result = inspector.GetProperty(typeof(FixtureModels.ExtensionData.ContainerWithJsonElementExtensionData), container, "newKey", ignoreUnmatched: true, caseInsensitivePropertyMatching: false); result.ShouldBeOfType(); result.Name.ShouldBe("newKey"); @@ -92,7 +92,9 @@ public void GetPropertiesUsesTheMostDerivedHiddenPropertyAttribute() var descriptor = new TestPropertyDescriptor("Value", typeof(string)); var inspector = new SystemTextJsonTypeInspector(new StubTypeInspector(new[] { descriptor })); - var property = inspector.GetProperties(typeof(DerivedHiddenPropertyModel), new DerivedHiddenPropertyModel()).Single(); + var property = inspector.GetProperties( + typeof(FixtureModels.TypeInspectorInheritance.DerivedHiddenPropertyModel), + new FixtureModels.TypeInspectorInheritance.DerivedHiddenPropertyModel()).Single(); property.Name.ShouldBe("derived-value"); } @@ -100,10 +102,10 @@ public void GetPropertiesUsesTheMostDerivedHiddenPropertyAttribute() [Fact] public void SerializerHandlesUnannotatedHiddenProperties() { - var yaml = YamlConverter.Serialize(new DerivedHiddenPropertyModel()); + var yaml = StaticYaml.Serialize(new FixtureModels.TypeInspectorInheritance.DerivedHiddenPropertyModel()); yaml.ShouldContain("derived-value: derived"); - Should.NotThrow(() => YamlConverter.Serialize(new DerivedUnannotatedPropertyModel())); + Should.NotThrow(() => StaticYaml.Serialize(new FixtureModels.TypeInspectorInheritance.DerivedUnannotatedPropertyModel())); } private sealed class StubTypeInspector : ITypeInspector @@ -142,30 +144,4 @@ public bool HasParseMethod(Type type) } } - private sealed class ContainerWithExtensionData - { - public IDictionary ExtensionData { get; set; } = new Dictionary(); - } - - public class BaseHiddenPropertyModel - { - [JsonPropertyName("base-value")] - public object Value { get; set; } = "base"; - } - - public sealed class DerivedHiddenPropertyModel : BaseHiddenPropertyModel - { - [JsonPropertyName("derived-value")] - public new string Value { get; set; } = "derived"; - } - - public class BaseUnannotatedPropertyModel - { - public int Id { get; set; } = 1; - } - - public sealed class DerivedUnannotatedPropertyModel : BaseUnannotatedPropertyModel - { - public new string Id { get; set; } = "derived"; - } } diff --git a/tests/YamlDotNet.System.Text.Json.Tests/TypeConverterTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/TypeConverterTests.cs index 7fd045d..80f4d5f 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/TypeConverterTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/TypeConverterTests.cs @@ -3,6 +3,7 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; using YamlDotNet.Core; +using TestFixtures; namespace YamlDotNet.System.Text.Json.Tests; @@ -75,11 +76,11 @@ public static IEnumerable GetArrayTests() [MemberData(nameof(GetValueTests))] public void JsonValueTests(string val) { - var input = JsonSerializer.Deserialize(val, JsonSerializerOptions); + var input = JsonNode.Parse(val)!.AsValue(); - var yaml = YamlConverter.Serialize(input!); + var yaml = StaticYaml.Serialize(input!); - var output = YamlConverter.Deserialize(yaml); + var output = StaticYaml.Deserialize(yaml); Assert.Equal(val, output.ToJsonString(JsonSerializerOptions)); } @@ -88,11 +89,11 @@ public void JsonValueTests(string val) [MemberData(nameof(GetArrayTests))] public void JsonArrayTests(string val) { - var input = JsonSerializer.Deserialize(val, JsonSerializerOptions); + var input = JsonNode.Parse(val)!.AsArray(); - var yaml = YamlConverter.Serialize(input!); + var yaml = StaticYaml.Serialize(input!); - var output = YamlConverter.Deserialize(yaml); + var output = StaticYaml.Deserialize(yaml); Assert.Equal(val, output.ToJsonString(JsonSerializerOptions)); } @@ -101,11 +102,11 @@ public void JsonArrayTests(string val) [MemberData(nameof(GetObjectTests))] public void JsonObjectTests(string val) { - var input = JsonSerializer.Deserialize(val, JsonSerializerOptions); + var input = JsonNode.Parse(val)!.AsObject(); - var yaml = YamlConverter.Serialize(input!); + var yaml = StaticYaml.Serialize(input!); - var output = YamlConverter.Deserialize(yaml); + var output = StaticYaml.Deserialize(yaml); Assert.Equal(val, output.ToJsonString(JsonSerializerOptions)); } @@ -116,11 +117,11 @@ public void JsonObjectTests(string val) [MemberData(nameof(GetArrayTests))] public void JsonNodeTests(string val) { - var input = JsonSerializer.Deserialize(val, JsonSerializerOptions); + var input = JsonNode.Parse(val); - var yaml = YamlConverter.Serialize(input!); + var yaml = StaticYaml.Serialize(input!); - var output = YamlConverter.Deserialize(yaml); + var output = StaticYaml.Deserialize(yaml); Assert.Equal(val, output.ToJsonString(JsonSerializerOptions)); } @@ -131,13 +132,13 @@ public void JsonNodeTests(string val) [MemberData(nameof(GetArrayTests))] public void JsonElementTests(string val) { - var input = JsonSerializer.Deserialize(val, JsonSerializerOptions); + var input = SharedJson.ParseElement(val); - var yaml = YamlConverter.Serialize(input); + var yaml = StaticYaml.Serialize(input); - var output = YamlConverter.Deserialize(yaml); + var output = StaticYaml.Deserialize(yaml); - Assert.Equal(val, JsonSerializer.Serialize(output, JsonSerializerOptions)); + Assert.True(JsonElement.DeepEquals(input, output)); } [Theory] @@ -146,13 +147,13 @@ public void JsonElementTests(string val) [MemberData(nameof(GetArrayTests))] public void JsonDocumentTests(string val) { - var input = JsonSerializer.Deserialize(val, JsonSerializerOptions); + var input = JsonDocument.Parse(val); - var yaml = YamlConverter.Serialize(input!); + var yaml = StaticYaml.Serialize(input!); - var output = YamlConverter.Deserialize(yaml); + var output = StaticYaml.Deserialize(yaml); - Assert.Equal(val, JsonSerializer.Serialize(output, JsonSerializerOptions)); + Assert.True(JsonElement.DeepEquals(input.RootElement, output.RootElement)); } public static IEnumerable GetObjectSortTests() @@ -169,33 +170,15 @@ public static IEnumerable GetObjectSortTests() [MemberData(nameof(GetObjectSortTests))] public void JsonNodeSortTests(string inputVal, string outputVal) { - var input = JsonSerializer.Deserialize(inputVal, JsonSerializerOptions); + var input = JsonNode.Parse(inputVal); - var yaml = YamlConverter.Serialize(input!, sortAlphabetically: true); + var yaml = StaticYaml.Serialize(input!, sortAlphabetically: true); - var output = YamlConverter.Deserialize(yaml); + var output = StaticYaml.Deserialize(yaml); Assert.Equal(outputVal, output.ToJsonString(JsonSerializerOptions)); } - public class V1ObjectMeta - { - [JsonPropertyName("name")] - public string Name { get; set; } = default!; - } - - public class V1CustomResourceDefinition - { - [JsonPropertyName("apiVersion")] - public string ApiVersion { get; set; } = default!; - - [JsonPropertyName("kind")] - public string Kind { get; set; } = default!; - - [JsonPropertyName("metadata")] - public V1ObjectMeta Metadata { get; set; } = default!; - } - [Fact] public void DeserializeUnmatched() { @@ -208,9 +191,9 @@ public void DeserializeUnmatched() test: value """; - var output = YamlConverter.Deserialize(yaml, true); + var output = StaticYaml.Deserialize(yaml, true); - var yamlOutput = YamlConverter.Serialize(output); + var yamlOutput = StaticYaml.Serialize(output); var yamlExpected = """ apiVersion: 1.2.3 @@ -235,6 +218,6 @@ public void DeserializeUnmatchedException() test: value """; - Assert.Throws(() => YamlConverter.Deserialize(yaml)); + Assert.Throws(() => StaticYaml.Deserialize(yaml)); } } diff --git a/tests/YamlDotNet.System.Text.Json.Tests/Usings.cs b/tests/YamlDotNet.System.Text.Json.Tests/Usings.cs index 3a6ad15..f2b99e3 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/Usings.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/Usings.cs @@ -1,2 +1,3 @@ global using Shouldly; global using Xunit; +global using FixtureModels = TestFixtures.FixtureModels; diff --git a/tests/YamlDotNet.System.Text.Json.Tests/YamlDotNet.System.Text.Json.Tests.csproj b/tests/YamlDotNet.System.Text.Json.Tests/YamlDotNet.System.Text.Json.Tests.csproj index e8f4d15..9b930dc 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/YamlDotNet.System.Text.Json.Tests.csproj +++ b/tests/YamlDotNet.System.Text.Json.Tests/YamlDotNet.System.Text.Json.Tests.csproj @@ -1,4 +1,4 @@ - + net10.0;net11.0 @@ -8,18 +8,25 @@ preview false true + true + full + false - - - - - + + + + + + + + + diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/AnalyzerReleases.Unshipped.md b/tools/YamlDotNet.Analyzers.StaticGenerator/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..5b2af37 --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/AnalyzerReleases.Unshipped.md @@ -0,0 +1,7 @@ +## Unshipped + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +YDNG001 | YamlDotNet.StaticGenerator | Warning | Requires registering types used by a static context. diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/ClassObject.cs b/tools/YamlDotNet.Analyzers.StaticGenerator/ClassObject.cs new file mode 100644 index 0000000..207be17 --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/ClassObject.cs @@ -0,0 +1,71 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace YamlDotNet.Analyzers.StaticGenerator +{ + public class ClassObject + { + public List FieldSymbols { get; } + public string FullName { get; } + public string GuidSuffix { get; } + public bool IsArray { get; } + public bool IsDictionary { get; } + public bool IsDictionaryOverride { get; } + public bool IsList { get; } + public bool IsListOverride { get; } + public ITypeSymbol ModuleSymbol { get; } + public List OnDeserializedMethods { get; } + public List OnDeserializingMethods { get; } + public List OnSerializedMethods { get; } + public List OnSerializingMethods { get; } + public List PropertySymbols { get; } + public string SanitizedClassName { get; } + + public ClassObject(string sanitizedClassName, + ITypeSymbol moduleSymbol, + bool isDictionary = false, + bool isList = false, + bool isArray = false, + bool isListOverride = false, + bool isDictionaryOverride = false) + { + FieldSymbols = new List(); + PropertySymbols = new List(); + FullName = moduleSymbol.GetFullName() ?? string.Empty; + GuidSuffix = Guid.NewGuid().ToString("N"); + IsDictionary = isDictionary; + IsList = isList; + IsArray = isArray; + IsListOverride = isListOverride; + ModuleSymbol = moduleSymbol; + OnDeserializedMethods = new List(); + OnDeserializingMethods = new List(); + OnSerializedMethods = new List(); + OnSerializingMethods = new List(); + SanitizedClassName = sanitizedClassName; + IsDictionaryOverride = isDictionaryOverride; + } + } +} diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/EnumMappings.cs b/tools/YamlDotNet.Analyzers.StaticGenerator/EnumMappings.cs new file mode 100644 index 0000000..93a1fc9 --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/EnumMappings.cs @@ -0,0 +1,39 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using Microsoft.CodeAnalysis; + +namespace YamlDotNet.Analyzers.StaticGenerator +{ + public class EnumMappings + { + public ITypeSymbol Type { get; set; } + public string ActualName { get; set; } + public string EnumMemberValue { get; set; } + + public EnumMappings(ITypeSymbol type, string actualName, string enumMemberValue) + { + ActualName = actualName; + EnumMemberValue = enumMemberValue; + Type = type; + } + } +} diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/File.cs b/tools/YamlDotNet.Analyzers.StaticGenerator/File.cs new file mode 100644 index 0000000..1d1d2c0 --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/File.cs @@ -0,0 +1,57 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using Microsoft.CodeAnalysis; + +namespace YamlDotNet.Analyzers.StaticGenerator +{ + public abstract class File + { + private readonly Action _write; + private readonly Action _indent; + private readonly Action _unindent; + + public File(Action write, Action indent, Action unindent, GeneratorExecutionContext context) + { + _write = write; + _indent = indent; + _unindent = unindent; + } + + public void Write(string text, bool newLine = true) + { + _write(text, newLine); + } + + public void Indent() + { + _indent(); + } + + public void UnIndent() + { + _unindent(); + } + + public abstract void Write(SerializableSyntaxReceiver classSyntaxReceiver); + } +} diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/ObjectAccessorFileGenerator.cs b/tools/YamlDotNet.Analyzers.StaticGenerator/ObjectAccessorFileGenerator.cs new file mode 100644 index 0000000..320dcd0 --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/ObjectAccessorFileGenerator.cs @@ -0,0 +1,110 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace YamlDotNet.Analyzers.StaticGenerator +{ + public class ObjectAccessorFileGenerator : File + { + public ObjectAccessorFileGenerator(Action Write, Action indent, Action unindent, GeneratorExecutionContext context) : base(Write, indent, unindent, context) + { + } + + public override void Write(SerializableSyntaxReceiver syntaxReceiver) + { + foreach (var o in syntaxReceiver.Classes) + { + var classObject = o.Value; + Write($"class {classObject.SanitizedClassName}_{classObject.GuidSuffix} : YamlDotNet.Serialization.IObjectAccessor"); + Write("{"); Indent(); + + Write("public void Set(string propertyName, object target, object value)"); + Write("{"); Indent(); + var typeName = classObject.FullName.Replace("?", string.Empty); + if (classObject.FieldSymbols.Count > 0 || classObject.PropertySymbols.Count > 0) + { + if (classObject.ModuleSymbol.TypeKind is TypeKind.Struct) + { + Write($"ref var v = ref System.Runtime.CompilerServices.Unsafe.Unbox<{typeName}>(target);"); + } + else + { + Write($"var v = ({typeName})target;"); + } + Write("switch (propertyName)"); + Write("{"); Indent(); + foreach (var field in classObject.FieldSymbols) + { + if (!field.IsReadOnly) + { + Write(GetSetter(field.Name, field.Type)); + } + } + foreach (var property in classObject.PropertySymbols) + { + if (property.SetMethod != null) + { + Write(GetSetter(property.Name, property.Type)); + } + } + Write("default: throw new ArgumentOutOfRangeException(\"propertyName\", $\"{propertyName} does not exist or is not settable\");"); + UnIndent(); Write("}"); + } + UnIndent(); Write("}"); + + Write("public object Read(string propertyName, object target)"); + Write("{"); Indent(); + Write($"var v = ({typeName})target;"); + if (classObject.FieldSymbols.Count > 0 || classObject.PropertySymbols.Count > 0) + { + Write("switch (propertyName)"); + Write("{"); Indent(); + foreach (var field in classObject.FieldSymbols) + { + Write(GetReader(field.Name)); + } + foreach (var property in classObject.PropertySymbols) + { + Write(GetReader(property.Name)); + } + UnIndent(); Write("}"); + } + Write("return null;"); + UnIndent(); Write("}"); + UnIndent(); Write("}"); + } + } + + private string GetSetter(string name, ITypeSymbol type) + { + return $"case \"{name}\": v.{name} = ({type.GetFullName().Replace("?", string.Empty)})value; return;"; + } + + private string GetReader(string name) + { + return $"case \"{name}\": return v.{name};"; + } + } +} diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/README.md b/tools/YamlDotNet.Analyzers.StaticGenerator/README.md new file mode 100644 index 0000000..7a6f1a3 --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/README.md @@ -0,0 +1,9 @@ +# Temporarily vendored YamlDotNet static generator + +These generator sources are copied from +[IvanJosipovic/YamlDotNet](https://github.com/IvanJosipovic/YamlDotNet) at +commit `00500082a13cbe9a00a2411fc93157b7d32c9348` to test the nested-type-name +fix before it is available through the generator package. + +The source is MIT licensed; see [YamlDotNet.LICENSE.txt](YamlDotNet.LICENSE.txt). +Keep this copy in sync only as needed for this temporary test dependency. diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/SerializableSyntaxReceiver.cs b/tools/YamlDotNet.Analyzers.StaticGenerator/SerializableSyntaxReceiver.cs new file mode 100644 index 0000000..4ae3add --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/SerializableSyntaxReceiver.cs @@ -0,0 +1,230 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace YamlDotNet.Analyzers.StaticGenerator +{ + public class SerializableSyntaxReceiver : ISyntaxContextReceiver + { + public List Log { get; } = new(); + public Dictionary Classes { get; } = new Dictionary(); + public Dictionary> EnumMappings { get; } = new Dictionary>(SymbolEqualityComparer.Default); + public INamedTypeSymbol? YamlStaticContextType { get; set; } + + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + if (context.Node is EnumDeclarationSyntax enumDeclarationSyntax) + { + var enumSymbol = context.SemanticModel.GetDeclaredSymbol(enumDeclarationSyntax)!; + if (enumSymbol.GetAttributes().Any(attribute => attribute.AttributeClass?.ToDisplayString() == "YamlDotNet.Serialization.YamlSerializableAttribute")) + { + HandleEnum(enumSymbol); + } + } + else if (context.Node is BaseTypeDeclarationSyntax classDeclarationSyntax) + { + var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax)!; + if (classSymbol.GetAttributes().Any()) + { + var attributes = classSymbol.GetAttributes(); + if (attributes.Any(attribute => attribute.AttributeClass?.ToDisplayString() == "YamlDotNet.Serialization.YamlStaticContextAttribute")) + { + YamlStaticContextType = classSymbol; + + var types = + attributes.Where(attribute => attribute.AttributeClass?.ToDisplayString() == "YamlDotNet.Serialization.YamlSerializableAttribute" + && attribute.ConstructorArguments.Any(argument => argument.Type?.ToDisplayString() == "System.Type")) + .Select(attribute => attribute.ConstructorArguments.First().Value) + .ToArray(); + + foreach (var type in types.OfType()) + { + if (type.TypeKind is TypeKind.Class or TypeKind.Struct) + { + AddSerializableClass(type); + } + } + } + + if (classSymbol.GetAttributes().Any(attribute => attribute.AttributeClass?.ToDisplayString() == "YamlDotNet.Serialization.YamlSerializableAttribute" + && attribute.ConstructorArguments.Length == 0)) + { + AddSerializableClass(classSymbol); + } + } + } + } + + private void AddSerializableClass(INamedTypeSymbol? classSymbol) + { + ClassObject classObject; + var className = SanitizeName(classSymbol!.GetFullName()); + if (Classes.ContainsKey(className)) + { + classObject = Classes[className]; + } + else + { + classObject = new ClassObject(className, classSymbol!); + Classes[className] = classObject; + } + while (classSymbol != null) + { + var members = classSymbol.GetMembers(); + foreach (var member in members) + { + if (member.IsStatic || + (member.DeclaredAccessibility != Accessibility.Public && + member.DeclaredAccessibility != Accessibility.Internal) || + member.GetAttributes().Any(x => x.AttributeClass!.ToDisplayString() == "YamlDotNet.Serialization.YamlIgnoreAttribute")) + { + continue; + } + + if (member is IPropertySymbol propertySymbol) + { + if (!classObject.PropertySymbols.ContainsName(propertySymbol)) + { + classObject.PropertySymbols.Add(propertySymbol); + CheckForSupportedGeneric(propertySymbol.Type); + HandleEnum(propertySymbol.Type); + } + } + else if (member is IFieldSymbol fieldSymbol) + { + if (!classObject.FieldSymbols.ContainsName(fieldSymbol)) + { + classObject.FieldSymbols.Add(fieldSymbol); + CheckForSupportedGeneric(fieldSymbol.Type); + HandleEnum(fieldSymbol.Type); + } + } + else if (member is IMethodSymbol methodSymbol) + { + var methodAttributes = methodSymbol.GetAttributes(); + if (methodAttributes.Any(x => x.AttributeClass!.ToDisplayString() == "YamlDotNet.Serialization.Callbacks.OnDeserializedAttribute") + && !classObject.OnDeserializedMethods.ContainsName(methodSymbol)) + { + classObject.OnDeserializedMethods.Add(methodSymbol); + } + if (methodAttributes.Any(x => x.AttributeClass!.ToDisplayString() == "YamlDotNet.Serialization.Callbacks.OnDeserializingAttribute") + && !classObject.OnDeserializingMethods.ContainsName(methodSymbol)) + { + classObject.OnDeserializingMethods.Add(methodSymbol); + } + if (methodAttributes.Any(x => x.AttributeClass!.ToDisplayString() == "YamlDotNet.Serialization.Callbacks.OnSerializedAttribute") + && !classObject.OnSerializedMethods.ContainsName(methodSymbol)) + { + classObject.OnSerializedMethods.Add(methodSymbol); + } + if (methodAttributes.Any(x => x.AttributeClass!.ToDisplayString() == "YamlDotNet.Serialization.Callbacks.OnSerializingAttribute") + && !classObject.OnSerializingMethods.ContainsName(methodSymbol)) + { + classObject.OnSerializingMethods.Add(methodSymbol); + } + } + } + classSymbol = classSymbol.BaseType; + } + } + + private string SanitizeName(string name) => new string(name.Select(c => char.IsLetterOrDigit(c) ? c : '_').ToArray()); + + private void CheckForSupportedGeneric(ITypeSymbol type) + { + var typeName = type.GetFullName(); + var sanitizedTypeName = SanitizeName(typeName); + + if (Classes.ContainsKey(sanitizedTypeName)) + { + return; + } + + if (type.Kind == SymbolKind.ArrayType) + { + Classes.Add(sanitizedTypeName, new ClassObject(sanitizedTypeName, (IArrayTypeSymbol)type, isArray: true)); + } + else if (typeName.StartsWith("System.Collections.Generic.Dictionary")) + { + Classes.Add(sanitizedTypeName, new ClassObject(sanitizedTypeName, (INamedTypeSymbol)type, true)); + CheckForSupportedGeneric(((INamedTypeSymbol)type).TypeArguments[1]); + } + else if (typeName.StartsWith("System.Collections.Generic.OrderedDictionary")) + { + Classes.Add(sanitizedTypeName, new ClassObject(sanitizedTypeName, (INamedTypeSymbol)type, true)); + CheckForSupportedGeneric(((INamedTypeSymbol)type).TypeArguments[1]); + } + else if (typeName.StartsWith("System.Collections.Generic.List")) + { + Classes.Add(sanitizedTypeName, new ClassObject(sanitizedTypeName, (INamedTypeSymbol)type, isList: true)); + CheckForSupportedGeneric(((INamedTypeSymbol)type).TypeArguments[0]); + } + // Overrides for lists + else if (typeName.StartsWith("System.Collections.Generic.ICollection") || + typeName.StartsWith("System.Collections.Generic.IEnumerable") || + typeName.StartsWith("System.Collections.Generic.IList") || + typeName.StartsWith("System.Collections.Generic.IReadOnlyCollection") || + typeName.StartsWith("System.Collections.Generic.IReadOnlyList")) + { + Classes.Add(sanitizedTypeName, new ClassObject(sanitizedTypeName, (INamedTypeSymbol)type, isListOverride: true)); + CheckForSupportedGeneric(((INamedTypeSymbol)type).TypeArguments[0]); + } + else if (typeName.StartsWith("System.Collections.Generic.IReadOnlyDictionary")) + { + Classes.Add(sanitizedTypeName, new ClassObject(sanitizedTypeName, (INamedTypeSymbol)type, isDictionaryOverride: true)); + CheckForSupportedGeneric(((INamedTypeSymbol)type).TypeArguments[1]); + } + } + + private void HandleEnum(ITypeSymbol type) + { + if (type.TypeKind == TypeKind.Enum) + { + var enumMembers = type.GetMembers(); + var mappings = new List(); + foreach (var member in enumMembers) + { + if (member.Kind == SymbolKind.Field) + { + var enumMember = member.GetAttributes().FirstOrDefault(x => x.AttributeClass!.ToDisplayString() == "System.Runtime.Serialization.EnumMemberAttribute"); + var memberName = member.Name!; + var memberValue = memberName; + if (enumMember != null) + { + var argument = enumMember.NamedArguments.FirstOrDefault(x => x.Key == "Value"); + if ((argument.Value.Value as string) != null) + { + memberValue = (string)argument.Value.Value!; + } + } + mappings.Add(new EnumMappings(type, memberName, memberValue)); + } + } + this.EnumMappings[type] = mappings; + } + } + } +} diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/StaticContextFile.cs b/tools/YamlDotNet.Analyzers.StaticGenerator/StaticContextFile.cs new file mode 100644 index 0000000..1eba1f7 --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/StaticContextFile.cs @@ -0,0 +1,75 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using Microsoft.CodeAnalysis; + +namespace YamlDotNet.Analyzers.StaticGenerator +{ + public class StaticContextFile : File + { + public StaticContextFile(Action write, Action indent, Action unindent, GeneratorExecutionContext context) : base(write, indent, unindent, context) + { + } + + public override void Write(SerializableSyntaxReceiver syntaxReceiver) + { + Write($"public partial class {syntaxReceiver.YamlStaticContextType?.Name ?? "StaticContext"} : YamlDotNet.Serialization.StaticContext"); + Write("{"); Indent(); + Write("private readonly YamlDotNet.Serialization.ObjectFactories.StaticObjectFactory _objectFactory;"); + Write("private readonly YamlDotNet.Serialization.ITypeResolver _typeResolver;"); + Write("private readonly YamlDotNet.Serialization.ITypeInspector _typeInspector;"); + Write($"public {syntaxReceiver.YamlStaticContextType?.Name ?? "StaticContext"}()"); + Write("{"); Indent(); + Write("_objectFactory = new StaticObjectFactory();"); + Write("_typeResolver = new StaticTypeResolver(this);"); + Write("_typeInspector = new StaticTypeInspector(_typeResolver);"); + UnIndent(); Write("}"); + Write("public override YamlDotNet.Serialization.ObjectFactories.StaticObjectFactory GetFactory() => _objectFactory;"); + Write("public override YamlDotNet.Serialization.ITypeInspector GetTypeInspector() => _typeInspector;"); + Write("public override YamlDotNet.Serialization.ITypeResolver GetTypeResolver() => _typeResolver;"); + Write("public override bool IsKnownType(Type type)"); + Write("{"); Indent(); + foreach (var o in syntaxReceiver.Classes) + { + var classObject = o.Value; + if (classObject.IsArray || classObject.IsList || classObject.IsDictionary || classObject.IsDictionaryOverride || classObject.IsListOverride) + { + Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return true;"); + } + else + { + Write($"if (type == typeof({o.Value.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return true;"); + //always support an array, ienumerable, list and dictionary of the type + Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}[])) return true;"); + Write($"if (type == typeof(System.Collections.Generic.IEnumerable<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>)) return true;"); + Write($"if (type == typeof(System.Collections.Generic.List<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>)) return true;"); + Write($"if (type == typeof(System.Collections.Generic.Dictionary)) return true;"); + } + } + // always support dictionary object + Write("if (type == typeof(System.Collections.Generic.Dictionary)) return true;"); + Write("return false;"); + UnIndent(); Write("}"); + UnIndent(); Write("}"); + } + } +} diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/StaticObjectFactoryFile.cs b/tools/YamlDotNet.Analyzers.StaticGenerator/StaticObjectFactoryFile.cs new file mode 100644 index 0000000..bb36793 --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/StaticObjectFactoryFile.cs @@ -0,0 +1,273 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace YamlDotNet.Analyzers.StaticGenerator +{ + public class StaticObjectFactoryFile : File + { + public StaticObjectFactoryFile(Action write, Action indent, Action unindent, GeneratorExecutionContext context) : base(write, indent, unindent, context) + { + } + + public override void Write(SerializableSyntaxReceiver syntaxReceiver) + { + Write($"class StaticObjectFactory : YamlDotNet.Serialization.ObjectFactories.StaticObjectFactory"); + Write("{"); Indent(); + + Write("public override object Create(Type type)"); + Write("{"); Indent(); + foreach (var o in syntaxReceiver.Classes.Where(c => !c.Value.IsArray)) + { + var classObject = o.Value; + if (o.Value.IsListOverride) + { + Write($"if (type == typeof({classObject.ModuleSymbol.GetNamespace()}.{classObject.ModuleSymbol.Name}<{((INamedTypeSymbol)classObject.ModuleSymbol).TypeArguments[0].GetFullName().Replace("?", string.Empty)}>)) return new System.Collections.Generic.List<{((INamedTypeSymbol)classObject.ModuleSymbol).TypeArguments[0].GetFullName().Replace("?", string.Empty)}>();"); + } + else if (o.Value.IsDictionaryOverride) + { + var keyType = ((INamedTypeSymbol)classObject.ModuleSymbol).TypeArguments[0].GetFullName().Replace("?", string.Empty); + var valueType = ((INamedTypeSymbol)classObject.ModuleSymbol).TypeArguments[1].GetFullName().Replace("?", string.Empty); + //Write("/* this is a dictionary override: "); + //Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return new System.Collections.Dictionary<{keyType}, {valueType}>();"); + //Write("*/"); + Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return new System.Collections.Generic.Dictionary<{keyType}, {valueType}>();"); + } + else + { + var fullName = classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty); + var requiredMembers = new System.Collections.Generic.List(); + foreach (var prop in classObject.PropertySymbols) + { + if (prop.IsRequired) + { + requiredMembers.Add($"{prop.Name} = default!"); + } + } + foreach (var field in classObject.FieldSymbols) + { + if (field.IsRequired) + { + requiredMembers.Add($"{field.Name} = default!"); + } + } + if (requiredMembers.Count > 0) + { + var initializer = string.Join(", ", requiredMembers); + Write($"if (type == typeof({fullName})) return new {fullName}() {{ {initializer} }};"); + } + else + { + Write($"if (type == typeof({fullName})) return new {fullName}();"); + } + } + //always support a list and dictionary of the type + Write($"if (type == typeof(System.Collections.Generic.List<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>)) return new System.Collections.Generic.List<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>();"); + Write($"if (type == typeof(System.Collections.Generic.Dictionary)) return new System.Collections.Generic.Dictionary();"); + } + // always support dictionary when deserializing object + Write("if (type == typeof(System.Collections.Generic.Dictionary)) return new System.Collections.Generic.Dictionary();"); + Write($"throw new InvalidOperationException($\"Type '{{type.FullName}}' is not registered in the YamlDotNet static context. Add [YamlSerializable(typeof({{type.Name}}))] to your static context class.\");"); + UnIndent(); Write("}"); + + Write("public override Array CreateArray(Type type, int count)"); + Write("{"); Indent(); + foreach (var o in syntaxReceiver.Classes) + { + var classObject = o.Value; + if (classObject.IsArray) + { + Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return new {classObject.ModuleSymbol.GetFullName(false).Replace("?", string.Empty)}[count];"); + } + else + { + Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}[])) return new {classObject.ModuleSymbol.GetFullName(false).Replace("?", string.Empty)}[count];"); + } + } + Write($"throw new InvalidOperationException($\"Type '{{type.FullName}}' is not registered in the YamlDotNet static context. Add [YamlSerializable(typeof({{type.Name}}))] to your static context class.\");"); + UnIndent(); Write("}"); + + Write("public override bool IsDictionary(Type type)"); + Write("{"); Indent(); + foreach (var o in syntaxReceiver.Classes) + { + var classObject = o.Value; + if (classObject.IsDictionary || classObject.IsDictionaryOverride) + { + Write($"if (type == typeof({o.Value.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return true;"); + } + else + { + //always support a dictionary of the type + Write($"if (type == typeof(System.Collections.Generic.Dictionary)) return true;"); + } + } + // always support dictionary object + Write("if (type == typeof(System.Collections.Generic.Dictionary)) return true;"); + Write("return false;"); + UnIndent(); Write("}"); + + Write("public override bool IsArray(Type type)"); + Write("{"); Indent(); + foreach (var o in syntaxReceiver.Classes) + { + var classObject = o.Value; + if (classObject.IsArray) + { + Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return true;"); + } + else + { + //always support an array of the type + Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}[])) return true;"); + } + } + Write("return false;"); + UnIndent(); Write("}"); + + Write("public override bool IsList(Type type)"); + Write("{"); Indent(); + foreach (var o in syntaxReceiver.Classes) + { + var classObject = o.Value; + if (classObject.IsList || classObject.IsListOverride) + { + Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return true;"); + } + else + { + //always support a list of the type + Write($"if (type == typeof(System.Collections.Generic.List<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>)) return true;"); + + //we'll make ienumerables lists. + Write($"if (type == typeof(System.Collections.Generic.IEnumerable<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>)) return true;"); + } + } + Write("return false;"); + UnIndent(); Write("}"); + + Write("public override Type GetKeyType(Type type)"); + Write("{"); Indent(); + foreach (var o in syntaxReceiver.Classes) + { + var classObject = o.Value; + if (classObject.IsDictionary || classObject.IsDictionaryOverride) + { + var keyType = "object"; + var type = (INamedTypeSymbol)classObject.ModuleSymbol; + + if (type.IsGenericType) + { + keyType = type.TypeArguments[0].GetFullName().Replace("?", string.Empty); + } + + Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return typeof({keyType});"); + } + else if (!classObject.IsArray && !classObject.IsList && !classObject.IsListOverride) + { + //always support a dictionary of the type + Write($"if (type == typeof(System.Collections.Generic.Dictionary)) return typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)});"); + } + } + + // always support dictionary object + Write("if (type == typeof(System.Collections.Generic.Dictionary)) return typeof(object);"); + Write($"throw new InvalidOperationException($\"Type '{{type.FullName}}' is not registered in the YamlDotNet static context. Add [YamlSerializable(typeof({{type.Name}}))] to your static context class.\");"); + UnIndent(); Write("}"); + + Write("public override Type GetValueType(Type type)"); + Write("{"); Indent(); + foreach (var o in syntaxReceiver.Classes) + { + var classObject = o.Value; + if (!(classObject.IsList || classObject.IsDictionary || classObject.IsDictionaryOverride || classObject.IsArray || classObject.IsListOverride)) + { + continue; + } + + string valueType; + if (classObject.IsDictionary || classObject.IsDictionaryOverride) + { + valueType = ((INamedTypeSymbol)classObject.ModuleSymbol).TypeArguments[1].GetFullName().Replace("?", string.Empty); + } + else if (classObject.IsList || classObject.IsListOverride) + { + valueType = ((INamedTypeSymbol)classObject.ModuleSymbol).TypeArguments[0].GetFullName().Replace("?", string.Empty); + } + else + { + valueType = ((IArrayTypeSymbol)(classObject.ModuleSymbol)).ElementType.GetFullName().Replace("?", string.Empty); + } + + Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return typeof({valueType});"); + } + + //always support array, list, dictionary and Ienumerables of all types + foreach (var o in syntaxReceiver.Classes) + { + var classObject = o.Value; + Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}[])) return typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)});"); + Write($"if (type == typeof(System.Collections.Generic.IEnumerable<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>)) return typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)});"); + Write($"if (type == typeof(System.Collections.Generic.List<{classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>)) return typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)});"); + Write($"if (type == typeof(System.Collections.Generic.Dictionary)) return typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)});"); + } + + Write("if (type == typeof(System.Collections.Generic.Dictionary)) return typeof(object);"); + Write($"throw new InvalidOperationException($\"Type '{{type.FullName}}' is not registered in the YamlDotNet static context. Add [YamlSerializable(typeof({{type.Name}}))] to your static context class.\");"); + UnIndent(); Write("}"); + WriteExecuteMethod(syntaxReceiver, "ExecuteOnDeserializing", (c) => c.OnDeserializingMethods); + WriteExecuteMethod(syntaxReceiver, "ExecuteOnDeserialized", (c) => c.OnDeserializedMethods); + WriteExecuteMethod(syntaxReceiver, "ExecuteOnSerializing", (c) => c.OnSerializingMethods); + WriteExecuteMethod(syntaxReceiver, "ExecuteOnSerialized", (c) => c.OnSerializedMethods); + UnIndent(); Write("}"); + } + + private void WriteExecuteMethod(SerializableSyntaxReceiver syntaxReceiver, string methodName, Func> selector) + { + Write($"public override void {methodName}(object value)"); + Write("{"); Indent(); + Write("if (value == null) return;"); + Write("var type = value.GetType();"); + foreach (var o in syntaxReceiver.Classes) + { + var classObject = o.Value; + var methods = selector(classObject); + if (methods.Any()) + { + var className = classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty); + Write($"if (type == typeof({className}))"); + Write("{"); Indent(); + foreach (var m in methods) + { + Write($"(({className})value).{m.Name}();"); + } + Write("return;"); + UnIndent(); Write("}"); + } + } + UnIndent(); Write("}"); + } + } +} diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/StaticPropertyDescriptorFile.cs b/tools/YamlDotNet.Analyzers.StaticGenerator/StaticPropertyDescriptorFile.cs new file mode 100644 index 0000000..3df3f24 --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/StaticPropertyDescriptorFile.cs @@ -0,0 +1,83 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using Microsoft.CodeAnalysis; + +namespace YamlDotNet.Analyzers.StaticGenerator +{ + public class StaticPropertyDescriptorFile : File + { + public StaticPropertyDescriptorFile(Action write, Action indent, Action unindent, GeneratorExecutionContext context) : base(write, indent, unindent, context) + { + } + + public override void Write(SerializableSyntaxReceiver syntaxReceiver) + { + Write("class StaticPropertyDescriptor : YamlDotNet.Serialization.IPropertyDescriptor"); + Write("{"); Indent(); + Write("private readonly YamlDotNet.Serialization.ITypeResolver _typeResolver;"); + Write("private YamlDotNet.Serialization.IObjectAccessor _accessor;"); + Write("private readonly Attribute[] _attributes;"); + Write("public string Name { get; }"); + Write("public bool CanWrite { get; }"); + Write("public Type Type { get; }"); + Write("public Type ConverterType { get; }"); + Write("public Type TypeOverride { get; set; }"); + Write("public bool AllowNulls { get; set; }"); + Write("public bool Required { get; }"); + Write("public int Order { get; set; }"); + Write("public YamlDotNet.Core.ScalarStyle ScalarStyle { get; set; }"); + Write("public T GetCustomAttribute() where T : Attribute"); + Write("{"); Indent(); + Write("foreach (var attribute in this._attributes)"); + Write("{"); Indent(); + Write("if (attribute.GetType() == typeof(T)) { return (T)attribute; }"); + UnIndent(); Write("}"); + Write("return null;"); + UnIndent(); Write("}"); + Write("public YamlDotNet.Serialization.IObjectDescriptor Read(object target)"); + Write("{"); Indent(); + Write("var propertyValue = _accessor.Read(Name, target);"); + Write("var actualType = _typeResolver.Resolve(Type, propertyValue);"); + Write("return new YamlDotNet.Serialization.ObjectDescriptor(propertyValue, actualType, Type, this.ScalarStyle);"); + UnIndent(); Write("}"); + Write("public void Write(object target, object value)"); + Write("{"); Indent(); + Write("_accessor.Set(Name, target, value);"); + UnIndent(); Write("}"); + Write("public StaticPropertyDescriptor(YamlDotNet.Serialization.ITypeResolver typeResolver, YamlDotNet.Serialization.IObjectAccessor accessor, string name, bool canWrite, Type type, Attribute[] attributes, bool allowNulls, bool isRequired, Type converterType)"); + Write("{"); Indent(); + Write("this._typeResolver = typeResolver;"); + Write("this._accessor = accessor;"); + Write("this._attributes = attributes;"); + Write("this.Name = name;"); + Write("this.CanWrite = canWrite;"); + Write("this.Type = type;"); + Write("this.ConverterType = converterType;"); + Write("this.ScalarStyle = YamlDotNet.Core.ScalarStyle.Any;"); + Write("this.AllowNulls = allowNulls;"); + Write("this.Required = isRequired;"); + UnIndent(); Write("}"); + UnIndent(); Write("}"); + } + } +} diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/StaticTypeInspectorFile.cs b/tools/YamlDotNet.Analyzers.StaticGenerator/StaticTypeInspectorFile.cs new file mode 100644 index 0000000..29eac0a --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/StaticTypeInspectorFile.cs @@ -0,0 +1,264 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace YamlDotNet.Analyzers.StaticGenerator +{ + public class StaticTypeInspectorFile : File + { + private readonly GeneratorExecutionContext context; + + public StaticTypeInspectorFile(Action Write, Action indent, Action unindent, GeneratorExecutionContext context) : base(Write, indent, unindent, context) + { + this.context = context; + } + + public override void Write(SerializableSyntaxReceiver syntaxReceiver) + { + Write("public class StaticTypeInspector : YamlDotNet.Serialization.ITypeInspector"); + Write("{"); Indent(); + + Write("private readonly YamlDotNet.Serialization.ITypeResolver _typeResolver;"); + Write("public StaticTypeInspector(YamlDotNet.Serialization.ITypeResolver typeResolver)"); + Write("{"); Indent(); + Write("_typeResolver = typeResolver;"); + UnIndent(); Write("}"); + + #region GetProperties + Write("public IEnumerable GetProperties(Type type, object container)"); + Write("{"); Indent(); + foreach (var o in syntaxReceiver.Classes) + { + var classObject = o.Value; + Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}))"); + Write("{"); Indent(); + Write($"var accessor = new {classObject.SanitizedClassName}_{classObject.GuidSuffix}();"); + Write("return new YamlDotNet.Serialization.IPropertyDescriptor[]"); + Write("{"); Indent(); + foreach (var field in classObject.FieldSymbols) + { + WritePropertyDescriptor(field.Name, field.Type, !field.IsReadOnly, field.GetAttributes(), field.IsRequired, ','); + } + foreach (var property in classObject.PropertySymbols) + { + WritePropertyDescriptor(property.Name, property.Type, property.SetMethod == null, property.GetAttributes(), property.IsRequired, ','); + } + UnIndent(); Write("};"); + UnIndent(); Write("}"); + } + Write("return new List();"); + UnIndent(); Write("}"); + #endregion + + #region GetProperty + Write("public YamlDotNet.Serialization.IPropertyDescriptor GetProperty(Type type, object container, string name, bool ignoreUnmatched, bool caseInsensitivePropertyMatching)"); + Write("{"); Indent(); + foreach (var o in syntaxReceiver.Classes) + { + var classObject = o.Value; + Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}))"); + Write("{"); Indent(); + Write($"var accessor = new {classObject.SanitizedClassName}_{classObject.GuidSuffix}();"); + foreach (var field in classObject.FieldSymbols) + { + Write($"if (name == \"{field.Name}\") return "); + WritePropertyDescriptor(field.Name, field.Type, field.IsReadOnly, field.GetAttributes(), field.IsRequired, ';'); + } + foreach (var property in classObject.PropertySymbols) + { + Write($"if (name == \"{property.Name}\") return ", false); + WritePropertyDescriptor(property.Name, property.Type, property.SetMethod == null, property.GetAttributes(), property.IsRequired, ';'); + } + Write("if (caseInsensitivePropertyMatching)"); + Write("{"); Indent(); + foreach (var field in classObject.FieldSymbols) + { + Write($"if (name.Equals(\"{field.Name}\", System.StringComparison.OrdinalIgnoreCase)) return "); + WritePropertyDescriptor(field.Name, field.Type, field.IsReadOnly, field.GetAttributes(), field.IsRequired, ';'); + } + foreach (var property in classObject.PropertySymbols) + { + Write($"if (name.Equals(\"{property.Name}\", System.StringComparison.OrdinalIgnoreCase)) return "); + WritePropertyDescriptor(property.Name, property.Type, property.SetMethod == null, property.GetAttributes(), property.IsRequired, ';'); + } + + UnIndent(); Write("}"); + + UnIndent(); Write("}"); + + } + Write("if (ignoreUnmatched) return null;"); + Write("throw new ArgumentOutOfRangeException(\"Field or property not valid: \" + name);"); + UnIndent(); Write("}"); + #endregion + + Write("public string GetEnumName(Type type, string name)"); + Write("{"); Indent(); + var prefix = string.Empty; + foreach (var map in syntaxReceiver.EnumMappings) + { + Write($"{prefix}if (type == typeof({map.Key.GetFullName()}))"); + Write("{"); Indent(); + + foreach (var mapping in map.Value) + { + Write($"if (name == \"{mapping.EnumMemberValue.Replace("\"", "\\\"")}\") return \"{mapping.ActualName}\";"); + } + + UnIndent(); Write("}"); + prefix = "else "; + } + Write("return name;"); + UnIndent(); Write("}"); + + prefix = string.Empty; + Write("public string GetEnumValue(object value)"); + Write("{"); Indent(); + Write("var type = value.GetType();"); + foreach (var map in syntaxReceiver.EnumMappings) + { + Write($"{prefix}if (type == typeof({map.Key.GetFullName()}))"); + Write("{"); Indent(); + + foreach (var mapping in map.Value) + { + Write($"if (({mapping.Type.GetFullName()})value == {mapping.Type.GetFullName()}.{mapping.ActualName}) return \"{mapping.EnumMemberValue.Replace("\"", "\\\"")}\";"); + } + + UnIndent(); Write("}"); + prefix = "else "; + } + 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("}"); + } + + private void WritePropertyDescriptor(string name, ITypeSymbol type, bool isReadonly, ImmutableArray attributes, bool isRequired, char finalChar) + { + var allowNulls = type.NullableAnnotation.HasFlag(NullableAnnotation.Annotated) && context.Compilation.Options.NullableContextOptions.AnnotationsEnabled(); + AttributeData? yamlConverterAttribute = null; + Write($"new StaticPropertyDescriptor(_typeResolver, accessor, \"{name}\", {(!isReadonly).ToString().ToLower()}, typeof({type.GetFullName().Replace("?", string.Empty)}), new Attribute[] {{"); + foreach (var attribute in attributes) + { + switch (attribute.AttributeClass?.ToDisplayString()) + { + case "YamlDotNet.Serialization.YamlConverterAttribute()": + yamlConverterAttribute = attribute; + break; + case "YamlDotNet.Serialization.YamlIgnore": + Write("new YamlDotNet.Serialization.YamlIgnoreAttribute(),"); + break; + case "YamlDotNet.Serialization.YamlMemberAttribute": + Write("new YamlDotNet.Serialization.YamlMemberAttribute() {"); + for (var index = 0; index < attribute.NamedArguments.Length; index++) + { + if (index > 0) + { + Write(", "); + } + var attributeData = attribute.NamedArguments[index]; + switch (attributeData.Key) + { + case "Description": + Indent(); Write($"Description = {attributeData.Value.ToCSharpString()}"); UnIndent(); + break; + case "Order": + Indent(); Write($"Order = {attributeData.Value.Value}"); UnIndent(); + break; + case "Alias": + Indent(); Write($"Alias = {attributeData.Value.ToCSharpString()}"); UnIndent(); + break; + case "ApplyNamingConventions": + Indent(); Write($"ApplyNamingConventions = {attributeData.Value.ToCSharpString()}"); UnIndent(); + break; + case "DefaultValuesHandling": + Indent(); Write($"DefaultValuesHandling = (YamlDotNet.Serialization.DefaultValuesHandling){attributeData.Value.Value}"); UnIndent(); + break; + case "ScalarStyle": + Indent(); Write($"ScalarStyle = (YamlDotNet.Core.ScalarStyle){attributeData.Value.Value}"); UnIndent(); + break; + } + } + Write($"}},"); + break; + } + } + Write($"}}, {allowNulls.ToString().ToLower()}, {isRequired.ToString().ToLower()}, ", false); + if (yamlConverterAttribute == null) + { + Write("null", false); + } + else + { + var argument = yamlConverterAttribute.ConstructorArguments[0]; + var converterType = (Type)argument.Value!; + Write($"typeof({converterType.FullName})", false); + } + Write($"){finalChar}"); //TODO: replace null with the class for the typeconverter. + } + } +} diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/StaticTypeResolverFile.cs b/tools/YamlDotNet.Analyzers.StaticGenerator/StaticTypeResolverFile.cs new file mode 100644 index 0000000..7ceee4f --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/StaticTypeResolverFile.cs @@ -0,0 +1,51 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using Microsoft.CodeAnalysis; + +namespace YamlDotNet.Analyzers.StaticGenerator +{ + public class StaticTypeResolverFile : File + { + public StaticTypeResolverFile(Action write, Action indent, Action unindent, GeneratorExecutionContext context) : base(write, indent, unindent, context) + { + } + + public override void Write(SerializableSyntaxReceiver syntaxReceiver) + { + Write($"class StaticTypeResolver : YamlDotNet.Serialization.TypeResolvers.StaticTypeResolver"); + Write("{"); Indent(); + Write("private readonly YamlDotNet.Serialization.StaticContext _context;"); + Write($"public StaticTypeResolver(YamlDotNet.Serialization.StaticContext context)"); + Write("{"); Indent(); + Write("_context = context;"); + UnIndent(); Write("}"); + Write("public override Type Resolve(Type staticType, object actualValue)"); + Write("{"); Indent(); + Write("var result = base.Resolve(staticType, actualValue);"); + Write("if (result == staticType && actualValue != null && _context.IsKnownType(actualValue.GetType())) result = actualValue.GetType();"); + Write("return result;"); + UnIndent(); Write("}"); + UnIndent(); Write("}"); + } + } +} diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/SymbolCollectionExtensions.cs b/tools/YamlDotNet.Analyzers.StaticGenerator/SymbolCollectionExtensions.cs new file mode 100644 index 0000000..390fb94 --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/SymbolCollectionExtensions.cs @@ -0,0 +1,36 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace YamlDotNet.Analyzers.StaticGenerator +{ + static class SymbolCollectionExtensions + { + public static bool ContainsName(this IEnumerable enumerable, T symbol) + where T : ISymbol + { + return enumerable.Any(t => t.Name == symbol.Name); + } + } +} diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/SymbolExtensions.cs b/tools/YamlDotNet.Analyzers.StaticGenerator/SymbolExtensions.cs new file mode 100644 index 0000000..d334342 --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/SymbolExtensions.cs @@ -0,0 +1,141 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace YamlDotNet.Analyzers.StaticGenerator +{ + static class SymbolExtensions + { + public static string GetFullName(this ITypeSymbol symbol, bool includeArrayDimensions = true) + { + if (symbol is IArrayTypeSymbol arrayTypeSymbol) + { + if (includeArrayDimensions) + { + return $"{arrayTypeSymbol.ElementType.GetFullName()}[{(new string(',', arrayTypeSymbol.Rank - 1))}]"; + } + return arrayTypeSymbol.ElementType.GetFullName(); + } + + if (symbol is INamedTypeSymbol namedTypeSymbol) + { + return GetFullName(namedTypeSymbol); + } + + return symbol.Name; + } + + public static string GetNamespace(this ISymbol symbol) + { + var parts = new Stack(); + var currentNamespace = symbol.ContainingNamespace; + + while (currentNamespace != null) + { + if (!string.IsNullOrEmpty(currentNamespace.Name)) + { + parts.Push(currentNamespace.Name); + currentNamespace = currentNamespace.ContainingNamespace; + } + else + { + break; + } + } + + return string.Join(".", parts); + } + + static string GetNullable(this ITypeSymbol symbol) + { + if (symbol.IsValueType || symbol.NullableAnnotation != NullableAnnotation.Annotated) + { + return string.Empty; + } + + return "?"; + } + + static string GetFullName(this INamedTypeSymbol symbol) + { + var space = GetNamespace(symbol); + var suffix = GetGenericTypes(symbol.TypeArguments); ; + + var containingTypes = new Stack(); + var containingSymbol = symbol.ContainingType; + while (containingSymbol != null) + { + containingTypes.Push(containingSymbol.Name); + containingSymbol = containingSymbol.ContainingType; + } + + var containingTypePath = string.Join(".", containingTypes); + if (!string.IsNullOrWhiteSpace(containingTypePath)) + { + space = string.IsNullOrWhiteSpace(space) + ? containingTypePath + : $"{space}.{containingTypePath}"; + } + + if (!string.IsNullOrWhiteSpace(space)) + { + return space + "." + symbol.Name + suffix + symbol.GetNullable(); + } + else + { + return symbol.Name + suffix + symbol.GetNullable(); + } + } + + static string GetGenericTypes(IReadOnlyList typeArguments) + { + var output = new List(); + + foreach (var argument in typeArguments) + { + switch (argument) + { + case INamedTypeSymbol namedTypeSymbol: + output.Add(GetFullName(namedTypeSymbol)); + break; + case ITypeParameterSymbol typeParameterSymbol: + output.Add(typeParameterSymbol.Name + typeParameterSymbol.GetNullable()); + break; + case IArrayTypeSymbol arrayTypeSymbol: + output.Add(GetFullName(arrayTypeSymbol.ElementType) + "[]"); + break; + default: + throw new NotSupportedException($"Cannot generate type name from type argument {argument.GetType().FullName}"); + } + } + + if (output.Count > 0) + { + return $"<{string.Join(", ", typeArguments)}>"; + } + + return string.Empty; + } + } +} diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/TypeFactoryGenerator.cs b/tools/YamlDotNet.Analyzers.StaticGenerator/TypeFactoryGenerator.cs new file mode 100644 index 0000000..710b1d5 --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/TypeFactoryGenerator.cs @@ -0,0 +1,294 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace YamlDotNet.Analyzers.StaticGenerator +{ + [Generator] + public class TypeFactoryGenerator : ISourceGenerator + { + private GeneratorExecutionContext _context; + + private static readonly DiagnosticDescriptor UnregisteredTypeDescriptor = new DiagnosticDescriptor( + id: "YDNG001", + title: "Unregistered type in YamlDotNet static context", + messageFormat: "Property '{0}' on type '{1}' uses type '{2}' which is not registered in the YamlDotNet static context. Add [YamlSerializable(typeof({3}))] to your static context class.", + category: "YamlDotNet.StaticGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public void Execute(GeneratorExecutionContext context) + { + if (!(context.SyntaxContextReceiver is SerializableSyntaxReceiver receiver)) + { + return; + } + + if (receiver.Classes.Count == 0) + { + return; + } + + _context = context; + + foreach (var log in receiver.Log) + { + context.ReportDiagnostic(Diagnostic.Create("1", typeof(TypeFactoryGenerator).FullName, log, DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, true, 1)); + } + + context.AddSource("YamlDotNetAutoGraph.g.cs", GenerateSource(receiver)); + + ReportUnregisteredTypes(context, receiver); + } + + public void Initialize(GeneratorInitializationContext context) + { + var syntaxReceiver = new SerializableSyntaxReceiver(); + context.RegisterForSyntaxNotifications(() => syntaxReceiver); + } + + private string GenerateSource(SerializableSyntaxReceiver syntaxReceiver) + { + var result = new StringBuilder(); + var indentation = 0; + var indent = new Action(() => indentation++); + var unindent = new Action(() => indentation--); + + var write = new Action((string line, bool includeNewLine) => + { + if (indentation > 0) + { + result.Append(new string(' ', indentation * 4)); + } + result.Append(line); + if (includeNewLine) + { + result.AppendLine(); + } + }); + + try + { + write("#pragma warning disable CS8767 // Nullability of reference types", true); + write("#pragma warning disable CS8767 // Nullability of reference types", true); + write("#pragma warning disable CS8603 // Possible null reference return", true); + write("#pragma warning disable CS8604 // Possible null reference argument", true); + write("#pragma warning disable CS8766 // Nullability of reference types", true); + + write("using System;", true); + write("using System.Collections.Generic;", true); + + var namespaceName = syntaxReceiver.YamlStaticContextType?.GetNamespace(); + var isGlobalNamespace = string.IsNullOrEmpty(namespaceName); + + if (!isGlobalNamespace) + { + write($"namespace {namespaceName}", true); + write("{", true); indent(); + } + + new StaticContextFile(write, indent, unindent, _context).Write(syntaxReceiver); + new StaticObjectFactoryFile(write, indent, unindent, _context).Write(syntaxReceiver); + new StaticTypeResolverFile(write, indent, unindent, _context).Write(syntaxReceiver); + new StaticPropertyDescriptorFile(write, indent, unindent, _context).Write(syntaxReceiver); + new StaticTypeInspectorFile(write, indent, unindent, _context).Write(syntaxReceiver); + new ObjectAccessorFileGenerator(write, indent, unindent, _context).Write(syntaxReceiver); + + if (!isGlobalNamespace) + { + unindent(); write("}", true); + } + } + catch (Exception exception) + { + write("/*", true); + var e = exception; + while (e != null) + { + write(e.Message, true); + write(e.StackTrace, true); + write("======", true); + e = e.InnerException; + } + write("*/", true); + } + return result.ToString(); + } + + private static void ReportUnregisteredTypes(GeneratorExecutionContext context, SerializableSyntaxReceiver receiver) + { + // Build a set of all registered type full names (including collections auto-registered) + var registeredTypeNames = new HashSet(receiver.Classes.Keys); + + // Walk each registered class's properties and fields + foreach (var entry in receiver.Classes) + { + var classObject = entry.Value; + + // Skip collection/array entries — they are auto-registered wrappers, not user-defined types + if (classObject.IsArray || classObject.IsList || classObject.IsDictionary + || classObject.IsListOverride || classObject.IsDictionaryOverride) + { + continue; + } + + foreach (var property in classObject.PropertySymbols) + { + CheckTypeRegistration(context, receiver, registeredTypeNames, + property.Type, property.Name, classObject.FullName, property.Locations); + } + + foreach (var field in classObject.FieldSymbols) + { + CheckTypeRegistration(context, receiver, registeredTypeNames, + field.Type, field.Name, classObject.FullName, field.Locations); + } + } + } + + private static void CheckTypeRegistration( + GeneratorExecutionContext context, + SerializableSyntaxReceiver receiver, + HashSet registeredTypeNames, + ITypeSymbol type, + string memberName, + string containingTypeName, + System.Collections.Immutable.ImmutableArray locations) + { + // Unwrap nullable value types + if (type is INamedTypeSymbol namedType + && namedType.IsGenericType + && namedType.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T) + { + type = namedType.TypeArguments[0]; + } + + // Skip types that don't need registration + if (IsWellKnownType(type)) + { + return; + } + + // Check generic collection types — verify their element types + var fullName = type.GetFullName().TrimEnd('?'); + if (fullName.StartsWith("System.Collections.Generic.")) + { + if (type is INamedTypeSymbol genericType && genericType.IsGenericType) + { + foreach (var typeArg in genericType.TypeArguments) + { + CheckTypeRegistration(context, receiver, registeredTypeNames, + typeArg, memberName, containingTypeName, locations); + } + } + return; + } + + // Check array element types + if (type is IArrayTypeSymbol arrayType) + { + CheckTypeRegistration(context, receiver, registeredTypeNames, + arrayType.ElementType, memberName, containingTypeName, locations); + return; + } + + // Check if the type is registered + var sanitizedName = SanitizeName(fullName.TrimEnd('?')); + if (!registeredTypeNames.Contains(sanitizedName)) + { + var shortName = type.Name; + var location = locations.FirstOrDefault() ?? Location.None; + context.ReportDiagnostic(Diagnostic.Create( + UnregisteredTypeDescriptor, + location, + memberName, + containingTypeName, + fullName, + shortName)); + } + } + + private static bool IsWellKnownType(ITypeSymbol type) + { + // Enums are handled by the enum mapping system + if (type.TypeKind == TypeKind.Enum) + { + return true; + } + + // Check for special types that don't need registration + switch (type.SpecialType) + { + case SpecialType.System_Boolean: + case SpecialType.System_Byte: + case SpecialType.System_SByte: + case SpecialType.System_Int16: + case SpecialType.System_UInt16: + case SpecialType.System_Int32: + case SpecialType.System_UInt32: + case SpecialType.System_Int64: + case SpecialType.System_UInt64: + case SpecialType.System_Single: + case SpecialType.System_Double: + case SpecialType.System_Decimal: + case SpecialType.System_Char: + case SpecialType.System_String: + case SpecialType.System_Object: + case SpecialType.System_DateTime: + return true; + } + + // Check for other well-known BCL types that are handled + // by built-in converters or the scalar deserializer + var fullName = type.GetFullName().TrimEnd('?'); + switch (fullName) + { + case "System.Guid": + case "System.TimeSpan": + case "System.DateTimeOffset": + case "System.Uri": + case "System.Type": +#if NET6_0_OR_GREATER + case "System.DateOnly": + case "System.TimeOnly": +#endif + return true; + } + + // DateOnly and TimeOnly may not be caught by the #if above since the + // source generator itself targets netstandard2.0, so check by string + if (fullName == "System.DateOnly" || fullName == "System.TimeOnly") + { + return true; + } + + return false; + } + + private static string SanitizeName(string name) => + new string(name.Select(c => char.IsLetterOrDigit(c) ? c : '_').ToArray()); + } +} diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.Analyzers.StaticGenerator.csproj b/tools/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.Analyzers.StaticGenerator.csproj new file mode 100644 index 0000000..db4eff3 --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.Analyzers.StaticGenerator.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + enable + 12.0 + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.LICENSE.txt b/tools/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.LICENSE.txt new file mode 100644 index 0000000..d4f2924 --- /dev/null +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014 Antoine Aubry and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From c756964609b339e4cb35043841cf01171615a75a Mon Sep 17 00:00:00 2001 From: Ivan Josipovic <9521987+IvanJosipovic@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:41:59 -0700 Subject: [PATCH 2/7] wip --- .../YamlDotNet.Analyzers.StaticGenerator.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.Analyzers.StaticGenerator.csproj b/tools/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.Analyzers.StaticGenerator.csproj index db4eff3..be37fd4 100644 --- a/tools/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.Analyzers.StaticGenerator.csproj +++ b/tools/YamlDotNet.Analyzers.StaticGenerator/YamlDotNet.Analyzers.StaticGenerator.csproj @@ -4,6 +4,7 @@ netstandard2.0 enable 12.0 + false From d12bc7169a6979e0b2f4436dcf67d18ba0c83d62 Mon Sep 17 00:00:00 2001 From: Ivan Josipovic <9521987+IvanJosipovic@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:49:10 -0700 Subject: [PATCH 3/7] fix: remove unnecessary TargetFrameworks property from AOT publish command --- .github/workflows/pr.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 89d0565..dfc6942 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -24,5 +24,5 @@ jobs: with: global-json-file: global.json - - run: dotnet publish tests/YamlDotNet.System.Text.Json.Tests/YamlDotNet.System.Text.Json.Tests.csproj -c Aot -f net10.0 -p:TargetFrameworks=net10.0 -r linux-x64 --self-contained true -o artifacts/aot-tests + - run: dotnet publish tests/YamlDotNet.System.Text.Json.Tests/YamlDotNet.System.Text.Json.Tests.csproj -c Aot -f net10.0 -r linux-x64 --self-contained true -o artifacts/aot-tests - run: ./artifacts/aot-tests/YamlDotNet.System.Text.Json.Tests From be8ed832fcd92af3df73b8becd8f0eab5d89a2f9 Mon Sep 17 00:00:00 2001 From: Ivan Josipovic <9521987+IvanJosipovic@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:30:51 -0700 Subject: [PATCH 4/7] fix: adjust AOT build steps to ensure xUnit apphost pack is available --- .github/workflows/pr.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index dfc6942..3170611 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -24,5 +24,7 @@ jobs: with: global-json-file: global.json - - run: dotnet publish tests/YamlDotNet.System.Text.Json.Tests/YamlDotNet.System.Text.Json.Tests.csproj -c Aot -f net10.0 -r linux-x64 --self-contained true -o artifacts/aot-tests + # Restore outside the AOT configuration so the xUnit-required apphost pack is available to the AOT build. + - run: dotnet restore tests/YamlDotNet.System.Text.Json.Tests/YamlDotNet.System.Text.Json.Tests.csproj -r linux-x64 + - run: dotnet publish tests/YamlDotNet.System.Text.Json.Tests/YamlDotNet.System.Text.Json.Tests.csproj -c Aot -f net10.0 -r linux-x64 --self-contained true --no-restore -o artifacts/aot-tests - run: ./artifacts/aot-tests/YamlDotNet.System.Text.Json.Tests From b887266f3bb28f36ba0ec3443e6265f9434af46e Mon Sep 17 00:00:00 2001 From: Ivan Josipovic <9521987+IvanJosipovic@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:55:50 -0700 Subject: [PATCH 5/7] test: add reflection API tests and enhance existing test coverage --- .../ExtensionDataPropertyDescriptorTests.cs | 8 +++ .../Fixtures/StaticYaml.cs | 1 + .../Fixtures/SystemTextJsonFixture.cs | 12 ++++ .../ReflectionApiTests.cs | 59 +++++++++++++++++++ .../StaticContextApiTests.cs | 11 ++++ .../SystemTextJsonTypeInspectorTests.cs | 17 ++++++ .../SystemTextJsonYamlTypeConverterTests.cs | 4 ++ 7 files changed, 112 insertions(+) create mode 100644 tests/YamlDotNet.System.Text.Json.Tests/ReflectionApiTests.cs diff --git a/tests/YamlDotNet.System.Text.Json.Tests/ExtensionDataPropertyDescriptorTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/ExtensionDataPropertyDescriptorTests.cs index d080af2..3748ae3 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/ExtensionDataPropertyDescriptorTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/ExtensionDataPropertyDescriptorTests.cs @@ -127,6 +127,14 @@ public void WriteThrowsWhenTypeIsNotDictionary() Should.Throw(() => descriptor.Write(target, "value")); } + [Fact] + public void TypeDefaultsToObjectForNonDictionaryProperty() + { + var descriptor = new ExtensionDataPropertyDescriptor(new TestPropertyDescriptor("value", typeof(string))); + + descriptor.Type.ShouldBe(typeof(object)); + } + [Theory] [MemberData(nameof(JsonScalarValues))] public void WriteConvertsSupportedJsonScalarsToJsonElement(object? value, string expectedJson) diff --git a/tests/YamlDotNet.System.Text.Json.Tests/Fixtures/StaticYaml.cs b/tests/YamlDotNet.System.Text.Json.Tests/Fixtures/StaticYaml.cs index 33a0dbc..1140641 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/Fixtures/StaticYaml.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/Fixtures/StaticYaml.cs @@ -15,6 +15,7 @@ internal static class StaticYaml [DynamicDependency(DynamicallyAccessedMemberTypes.PublicFields, typeof(StaticApiProbeMode))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.ConfigurationAndAttributes.PropertyNameModel))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.ConfigurationAndAttributes.PropertyOrderModel))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.ConfigurationAndAttributes.FieldModel))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.IgnoreConditions.ConditionsModel))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.IgnoreConditions.DirectionalModel))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FixtureModels.Enums.EnumModel))] diff --git a/tests/YamlDotNet.System.Text.Json.Tests/Fixtures/SystemTextJsonFixture.cs b/tests/YamlDotNet.System.Text.Json.Tests/Fixtures/SystemTextJsonFixture.cs index 131455b..715d6ef 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/Fixtures/SystemTextJsonFixture.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/Fixtures/SystemTextJsonFixture.cs @@ -97,6 +97,15 @@ public sealed class PropertyOrderModel [JsonPropertyOrder(1)] public string MyProp3 { get; set; } = nameof(MyProp3); } + + public sealed class FieldModel + { + public string UnannotatedField = "hidden"; + + [JsonInclude] + [JsonPropertyName("included-field")] + public string IncludedField = "included"; + } } public static class IgnoreConditions @@ -137,6 +146,9 @@ public sealed class ConditionsModel [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int? HideNullableWhenDefault { get; set; } = 0; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? HideNullWhenDefault { get; set; } } public sealed class DirectionalModel diff --git a/tests/YamlDotNet.System.Text.Json.Tests/ReflectionApiTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/ReflectionApiTests.cs new file mode 100644 index 0000000..94f2328 --- /dev/null +++ b/tests/YamlDotNet.System.Text.Json.Tests/ReflectionApiTests.cs @@ -0,0 +1,59 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text.Json; +using TestFixtures; +using YamlDotNet.Serialization; + +namespace YamlDotNet.System.Text.Json.Tests; + +[CollectionDefinition("Reflection API tests", DisableParallelization = true)] +public sealed class ReflectionApiCollection +{ + public const string Name = "Reflection API tests"; +} + +[Collection(ReflectionApiCollection.Name)] +public class ReflectionApiTests +{ + [Fact] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The reflection APIs are explicitly trim-unsafe and this test runs only when dynamic code is supported.")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "The reflection APIs are explicitly unsupported for Native AOT and this test runs only when dynamic code is supported.")] + public void ReflectionConvenienceApisWorkWhenDynamicCodeIsSupported() + { + if (!RuntimeFeature.IsDynamicCodeSupported) + { + return; + } + + var model = new ComprehensiveConfiguration { Name = "reflection" }; + + YamlConverter.Serialize((object)model).ShouldContain("display-name: reflection"); + YamlConverter.Serialize(model).ShouldContain("display-name: reflection"); + YamlConverter.SerializeJson("{\"name\":\"reflection\"}").ShouldContain("name: reflection"); + YamlConverter.SerializeJson("{\"name\":\"reflection\",}", new JsonSerializerOptions { AllowTrailingCommas = true }) + .ShouldContain("name: reflection"); + + var yaml = YamlConverter.Serialize(model); + YamlConverter.Deserialize(yaml).Name.ShouldBe("reflection"); + Should.Throw(() => YamlConverter.Deserialize("unknown: value")); + YamlConverter.Deserialize( + "unknown: value", ignoreUnmatchedProperties: true).MyProp.ShouldBe("MyProp"); + } + + [Fact] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The reflection builders are explicitly trim-unsafe and this test runs only when dynamic code is supported.")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "The reflection builders are explicitly unsupported for Native AOT and this test runs only when dynamic code is supported.")] + public void ReflectionBuilderExtensionsConfigureJsonHandlingWhenDynamicCodeIsSupported() + { + if (!RuntimeFeature.IsDynamicCodeSupported) + { + return; + } + + var serializer = new SerializerBuilder().AddSystemTextJson().Build(); + serializer.Serialize(new ComprehensiveConfiguration { Name = "builder" }).ShouldContain("display-name: builder"); + + var deserializer = new DeserializerBuilder().AddSystemTextJson().Build(); + deserializer.Deserialize("display-name: builder").Name.ShouldBe("builder"); + } +} diff --git a/tests/YamlDotNet.System.Text.Json.Tests/StaticContextApiTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/StaticContextApiTests.cs index 617d69a..fbd65a6 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/StaticContextApiTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/StaticContextApiTests.cs @@ -5,6 +5,17 @@ namespace YamlDotNet.System.Text.Json.Tests; public class StaticContextApiTests { + [Fact] + public void StaticContextOverloadsRejectNullContexts() + { + Should.Throw(() => YamlConverter.Serialize("value", null!)) + .ParamName.ShouldBe("context"); + Should.Throw(() => YamlConverter.SerializeJson("{}", (YamlDotNet.Serialization.StaticContext)null!)) + .ParamName.ShouldBe("context"); + Should.Throw(() => YamlConverter.Deserialize("value", null!)) + .ParamName.ShouldBe("context"); + } + [Fact] public void SerializeJsonUsesSuppliedDocumentOptions() { diff --git a/tests/YamlDotNet.System.Text.Json.Tests/SystemTextJsonTypeInspectorTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/SystemTextJsonTypeInspectorTests.cs index e4d6422..f4af29a 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/SystemTextJsonTypeInspectorTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/SystemTextJsonTypeInspectorTests.cs @@ -99,6 +99,23 @@ public void GetPropertiesUsesTheMostDerivedHiddenPropertyAttribute() property.Name.ShouldBe("derived-value"); } + [Fact] + public void GetPropertiesIncludesOnlyJsonIncludedFields() + { + var descriptors = new IPropertyDescriptor[] + { + new TestPropertyDescriptor(nameof(FixtureModels.ConfigurationAndAttributes.FieldModel.UnannotatedField), typeof(string)), + new TestPropertyDescriptor(nameof(FixtureModels.ConfigurationAndAttributes.FieldModel.IncludedField), typeof(string)), + }; + var inspector = new SystemTextJsonTypeInspector(new StubTypeInspector(descriptors)); + + var properties = inspector.GetProperties( + typeof(FixtureModels.ConfigurationAndAttributes.FieldModel), + new FixtureModels.ConfigurationAndAttributes.FieldModel()).ToArray(); + + properties.Select(property => property.Name).ShouldBe(new[] { "included-field" }); + } + [Fact] public void SerializerHandlesUnannotatedHiddenProperties() { diff --git a/tests/YamlDotNet.System.Text.Json.Tests/SystemTextJsonYamlTypeConverterTests.cs b/tests/YamlDotNet.System.Text.Json.Tests/SystemTextJsonYamlTypeConverterTests.cs index 506aed7..067ce46 100644 --- a/tests/YamlDotNet.System.Text.Json.Tests/SystemTextJsonYamlTypeConverterTests.cs +++ b/tests/YamlDotNet.System.Text.Json.Tests/SystemTextJsonYamlTypeConverterTests.cs @@ -111,6 +111,10 @@ public void WriteYaml_HandlesJsonValueKinds() var undefinedValue = (JsonValue)JsonValue.Create(default(JsonElement))!; converter.WriteYaml(emitter, undefinedValue, typeof(JsonValue), StubSerializer); + using var nullDocument = JsonDocument.Parse("null"); + var jsonNullValue = JsonValue.Create(nullDocument.RootElement)!; + converter.WriteYaml(emitter, jsonNullValue, typeof(JsonValue), StubSerializer); + emitter.Events.Count.ShouldBeGreaterThan(0); } From 69ea478004d9511623e40bd4e74be2260d53bdfc Mon Sep 17 00:00:00 2001 From: Ivan Josipovic <9521987+IvanJosipovic@users.noreply.github.com> Date: Wed, 23 Sep 2026 17:03:47 -0700 Subject: [PATCH 6/7] Update README.md --- README.md | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 98f3be7..37fb383 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,16 @@ Supported Objects: - [System.Text.Json.Nodes.JsonValue](https://docs.microsoft.com/en-us/dotnet/api/system.text.json.nodes.jsonvalue) - [System.Text.Json.JsonElement](https://docs.microsoft.com/en-us/dotnet/api/system.text.json.jsonelement) - [System.Text.Json.JsonDocument](https://docs.microsoft.com/en-us/dotnet/api/system.text.json.jsondocument) -- [System.Text.Json.Serialization.JsonIgnoreAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonignoreattribute) - - Conditions - controls whether a property is included while serializing and deserializing - - Always = Ignore during serialization and deserialization (default) - - Never = Include during serialization and deserialization - - WhenWritingNull = Ignore during serialization when the value is `null` - - WhenWritingDefault = Ignore during serialization when the value is the type's default value - - WhenWriting = Ignore during serialization - - WhenReading = Ignore during deserialization - - Conditional `WhenWritingNull` and `WhenWritingDefault` properties remain readable during deserialization. Properties ignored with `Always` or `WhenReading` are treated as known ignored properties and skipped when they appear in YAML input. +- [System.Text.Json.Serialization.JsonIgnoreAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonignoreattribute) + - Conditions - controls whether a property is included while serializing and deserializing + - Always = Ignore during serialization and deserialization (default) + - Never = Include during serialization and deserialization + - WhenWritingNull = Ignore during serialization when the value is `null` + - WhenWritingDefault = Ignore during serialization when the value is the type's default value + - WhenWriting = Ignore during serialization + - WhenReading = Ignore during deserialization + + Conditional `WhenWritingNull` and `WhenWritingDefault` properties remain readable during deserialization. Properties ignored with `Always` or `WhenReading` are treated as known ignored properties and skipped when they appear in YAML input. - [System.Text.Json.Serialization.JsonPropertyNameAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonpropertynameattribute) - Name - Specifies the property name that is present in the JSON/YAML when serializing and deserializing. - [System.Text.Json.Serialization.JsonPropertyOrderAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonpropertyorderattribute) @@ -75,18 +75,14 @@ var myObject = deserializer.Deserialize(yaml) ### Trimming -The library is trim-analyzed. Its default `YamlConverter` methods and `AddSystemTextJson` extensions for YamlDotNet's reflection based builders are marked as requiring unreferenced code. For trimmed applications, use the `YamlConverter` overloads that accept a generated `StaticContext`, or YamlDotNet's static builders. +The library is trim-analyzed. Its default `YamlConverter` methods and `AddSystemTextJson` extensions for YamlDotNet's reflection based builders are marked as requiring unreferenced code. For trimmed applications, use the `YamlConverter` overloads that accept a generated `StaticContext`, or YamlDotNet's static builders. -Add the `Vecc.YamlDotNet.Analyzers.StaticGenerator` package to the consuming application, declare a partial `StaticContext` with `[YamlStaticContext]` and `[YamlSerializable(typeof(MyModel))]`, then pass that context to `YamlConverter.Serialize` and `YamlConverter.Deserialize`. Register all application model types used by the context, including enum types. The `AddSystemTextJson` builder extensions also support YamlDotNet's static builder types. - -The test project contains capability-specific shared fixtures and exercises the supported `JsonNode`, `JsonArray`, `JsonObject`, `JsonValue`, `JsonElement`, and `JsonDocument` types, as well as JSON attributes, extension data, naming, ordering, defaults, ignore conditions, enum names, and unmatched properties. CI publishes a focused xUnit Native AOT suite that covers the comprehensive fixture through a generated static context and a source-generated `JsonTypeInfo` converter for included fields and `JsonElement` extension data. - -`SystemTextJsonTypeInspector` reads `System.Text.Json` attributes from model metadata at runtime. A generated YAML static context registers the model descriptors, but trimmed applications must also retain those attributes, for example with `[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(MyModel))]` or by rooting the model assembly. The Native AOT tests apply targeted `DynamicDependency` roots to their registered fixture models. +Add the `Vecc.YamlDotNet.Analyzers.StaticGenerator` package to the consuming application, declare a partial `StaticContext` with `[YamlStaticContext]` and `[YamlSerializable(typeof(MyModel))]`, then pass that context to `YamlConverter.Serialize` and `YamlConverter.Deserialize`. Register all application model types used by the context, including enum types. The `AddSystemTextJson` builder extensions also support YamlDotNet's static builder types. -For applications configuring YamlDotNet's static builders directly, register each model type and enum in the generated static context. The context must include public model properties, fields, and constructors, and enum public fields for `JsonStringEnumMemberNameAttribute`. For `IDictionary` extension data, values may be JSON scalars, dictionaries with string keys, sequences, or existing `JsonElement`, `JsonDocument`, or `JsonNode` instances. Arbitrary CLR objects require runtime JSON metadata and are rejected. `IDictionary` extension data retains its values as objects. +The test project contains capability-specific shared fixtures and exercises the supported `JsonNode`, `JsonArray`, `JsonObject`, `JsonValue`, `JsonElement`, and `JsonDocument` types, as well as JSON attributes, extension data, naming, ordering, defaults, ignore conditions, enum names, and unmatched properties. CI publishes a focused xUnit Native AOT suite that covers the comprehensive fixture through a generated static context and a source-generated `JsonTypeInfo` converter for included fields and `JsonElement` extension data. -Extension-data properties using a concrete dictionary type other than `Dictionary` or `Dictionary` should be initialized before deserialization. +`SystemTextJsonTypeInspector` reads `System.Text.Json` attributes from model metadata at runtime. A generated YAML static context registers the model descriptors, but trimmed applications must also retain those attributes, for example with `[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(MyModel))]` or by rooting the model assembly. The Native AOT tests apply targeted `DynamicDependency` roots to their registered fixture models. -### Inspired By +For applications configuring YamlDotNet's static builders directly, register each model type and enum in the generated static context. The context must include public model properties, fields, and constructors, and enum public fields for `JsonStringEnumMemberNameAttribute`. For `IDictionary` extension data, values may be JSON scalars, dictionaries with string keys, sequences, or existing `JsonElement`, `JsonDocument`, or `JsonNode` instances. Arbitrary CLR objects require runtime JSON metadata and are rejected. `IDictionary` extension data retains its values as objects. -[https://github.com/tomlm/YamlConvert](https://github.com/tomlm/YamlConvert) +Extension-data properties using a concrete dictionary type other than `Dictionary` or `Dictionary` should be initialized before deserialization. From fbc56b8decc456ba93cb630602041ba3be15a7be Mon Sep 17 00:00:00 2001 From: Ivan Josipovic <9521987+IvanJosipovic@users.noreply.github.com> Date: Wed, 23 Sep 2026 17:11:30 -0700 Subject: [PATCH 7/7] docs: enhance trimming section with usage examples for StaticContext in Native AOT applications --- README.md | 72 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 37fb383..9cf34dd 100644 --- a/README.md +++ b/README.md @@ -73,16 +73,62 @@ var deserializer = new DeserializerBuilder() var myObject = deserializer.Deserialize(yaml) ``` -### Trimming - -The library is trim-analyzed. Its default `YamlConverter` methods and `AddSystemTextJson` extensions for YamlDotNet's reflection based builders are marked as requiring unreferenced code. For trimmed applications, use the `YamlConverter` overloads that accept a generated `StaticContext`, or YamlDotNet's static builders. - -Add the `Vecc.YamlDotNet.Analyzers.StaticGenerator` package to the consuming application, declare a partial `StaticContext` with `[YamlStaticContext]` and `[YamlSerializable(typeof(MyModel))]`, then pass that context to `YamlConverter.Serialize` and `YamlConverter.Deserialize`. Register all application model types used by the context, including enum types. The `AddSystemTextJson` builder extensions also support YamlDotNet's static builder types. - -The test project contains capability-specific shared fixtures and exercises the supported `JsonNode`, `JsonArray`, `JsonObject`, `JsonValue`, `JsonElement`, and `JsonDocument` types, as well as JSON attributes, extension data, naming, ordering, defaults, ignore conditions, enum names, and unmatched properties. CI publishes a focused xUnit Native AOT suite that covers the comprehensive fixture through a generated static context and a source-generated `JsonTypeInfo` converter for included fields and `JsonElement` extension data. - -`SystemTextJsonTypeInspector` reads `System.Text.Json` attributes from model metadata at runtime. A generated YAML static context registers the model descriptors, but trimmed applications must also retain those attributes, for example with `[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(MyModel))]` or by rooting the model assembly. The Native AOT tests apply targeted `DynamicDependency` roots to their registered fixture models. - -For applications configuring YamlDotNet's static builders directly, register each model type and enum in the generated static context. The context must include public model properties, fields, and constructors, and enum public fields for `JsonStringEnumMemberNameAttribute`. For `IDictionary` extension data, values may be JSON scalars, dictionaries with string keys, sequences, or existing `JsonElement`, `JsonDocument`, or `JsonNode` instances. Arbitrary CLR objects require runtime JSON metadata and are rejected. `IDictionary` extension data retains its values as objects. - -Extension-data properties using a concrete dictionary type other than `Dictionary` or `Dictionary` should be initialized before deserialization. +### Trimming + +Use the generated `StaticContext` overloads for trimmed or Native AOT apps. Install the generator with `dotnet add package Vecc.YamlDotNet.Analyzers.StaticGenerator`, register every model and enum, and preserve members that carry System.Text.Json attributes: + +```csharp +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; +using YamlDotNet.System.Text.Json; + +[YamlStaticContext] +[YamlSerializable(typeof(ServiceConfig))] +[YamlSerializable(typeof(DeploymentMode))] +public partial class AppYamlContext : StaticContext +{ +} + +public sealed class ServiceConfig +{ + [JsonPropertyName("service-name")] + public string Name { get; set; } = "api"; + + public DeploymentMode Mode { get; set; } +} + +public enum DeploymentMode +{ + [JsonStringEnumMemberName("production")] + Production, +} + +internal static class Program +{ + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ServiceConfig))] + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicFields, typeof(DeploymentMode))] + private static void Main() + { + var context = new AppYamlContext(); + var yaml = YamlConverter.Serialize(new ServiceConfig { Name = "api" }, context); + var config = YamlConverter.Deserialize(yaml, context); + } +} +``` + +To use YamlDotNet's builder APIs with the same context: + +```csharp +var serializer = new StaticSerializerBuilder(context) + .AddSystemTextJson() + .Build(); +var deserializer = new StaticDeserializerBuilder(context) + .AddSystemTextJson() + .Build(); + +var yaml = serializer.Serialize(config); +var roundTripped = deserializer.Deserialize(yaml); +``` + +Set `true` in the app project for Native AOT, or `true` for trimming, then publish for your target runtime identifier. The context APIs use generated YamlDotNet model metadata; contextless `YamlConverter` methods and YamlDotNet's reflection builders require unreferenced code.