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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,11 @@ COPY . .
RUN dotnet build -c Release --framework net47 YamlDotNet/YamlDotNet.csproj -o /output/net47
RUN dotnet build -c Release --framework netstandard2.0 YamlDotNet/YamlDotNet.csproj -o /output/netstandard2.0
RUN dotnet build -c Release --framework netstandard2.1 YamlDotNet/YamlDotNet.csproj -o /output/netstandard2.1
RUN dotnet build -c Release --framework net6.0 YamlDotNet/YamlDotNet.csproj -o /output/net6.0
RUN dotnet build -c Release --framework net8.0 YamlDotNet/YamlDotNet.csproj -o /output/net8.0

RUN dotnet pack -c Release YamlDotNet/YamlDotNet.csproj -o /output/package /p:Version=$PACKAGE_VERSION

RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net8.0 --logger:"trx;LogFileName=/output/tests-net8.0.trx" --logger:"console;Verbosity=detailed"
RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net6.0 --logger:"trx;LogFileName=/output/tests-net6.0.trx" --logger:"console;Verbosity=detailed"

FROM alpine
VOLUME /output
Expand Down
3 changes: 0 additions & 3 deletions Dockerfile.NonEnglish
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ COPY . .
RUN dotnet build -c Release --framework net47 YamlDotNet/YamlDotNet.csproj -o /output/net47
RUN dotnet build -c Release --framework netstandard2.0 YamlDotNet/YamlDotNet.csproj -o /output/netstandard2.0
RUN dotnet build -c Release --framework netstandard2.1 YamlDotNet/YamlDotNet.csproj -o /output/netstandard2.1
RUN dotnet build -c Release --framework net6.0 YamlDotNet/YamlDotNet.csproj -o /output/net6.0
RUN dotnet build -c Release --framework net8.0 YamlDotNet/YamlDotNet.csproj -o /output/net8.0

RUN dotnet pack -c Release YamlDotNet/YamlDotNet.csproj -o /output/package /p:Version=$PACKAGE_VERSION
Expand All @@ -62,8 +61,6 @@ RUN echo -n "${LC_ALL}" > /etc/locale.gen && \
locale-gen

RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net8.0 --logger:"trx;LogFileName=/output/tests-net8.0.trx" --logger:"console;Verbosity=detailed"
RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework net6.0 --logger:"trx;LogFileName=/output/tests-net6.0.trx" --logger:"console;Verbosity=detailed"
RUN dotnet test -c Release YamlDotNet.Test/YamlDotNet.Test.csproj --framework netcoreapp3.1 --logger:"trx;LogFileName=/output/tests-netcoreapp3.1.trx" --logger:"console;Verbosity=detailed"

FROM alpine
VOLUME /output
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ The library has now been successfully used in multiple projects and is considere

* netstandard 2.0
* netstandard 2.1
* .NET 6.0
* .NET 8.0
* .NET 10.0
* .NET Framework 4.7

## Quick start
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ private void CheckForSupportedGeneric(ITypeSymbol type)
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));
Expand Down
34 changes: 29 additions & 5 deletions YamlDotNet.Analyzers.StaticGenerator/StaticObjectFactoryFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,39 @@ public override void Write(SerializableSyntaxReceiver syntaxReceiver)
}
else
{
Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)})) return new {classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}();");
var fullName = classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty);
var requiredMembers = new System.Collections.Generic.List<string>();
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<string, {classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>)) return new System.Collections.Generic.Dictionary<string, {classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}>();");
}
// always support dictionary when deserializing object
Write("if (type == typeof(System.Collections.Generic.Dictionary<object, object>)) return new System.Collections.Generic.Dictionary<object, object>();");
Write($"throw new ArgumentOutOfRangeException(\"Unknown type: \" + type.ToString());");
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)");
Expand All @@ -82,7 +106,7 @@ public override void Write(SerializableSyntaxReceiver syntaxReceiver)
Write($"if (type == typeof({classObject.ModuleSymbol.GetFullName().Replace("?", string.Empty)}[])) return new {classObject.ModuleSymbol.GetFullName(false).Replace("?", string.Empty)}[count];");
}
}
Write($"throw new ArgumentOutOfRangeException(\"Unknown type: \" + type.ToString());");
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)");
Expand Down Expand Up @@ -170,7 +194,7 @@ public override void Write(SerializableSyntaxReceiver syntaxReceiver)

// always support dictionary object
Write("if (type == typeof(System.Collections.Generic.Dictionary<object, object>)) return typeof(object);");
Write("throw new ArgumentOutOfRangeException(\"Unknown type: \" + type.ToString());");
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)");
Expand Down Expand Up @@ -211,7 +235,7 @@ public override void Write(SerializableSyntaxReceiver syntaxReceiver)
}

Write("if (type == typeof(System.Collections.Generic.Dictionary<object, object>)) return typeof(object);");
Write("throw new ArgumentOutOfRangeException(\"Unknown type: \" + type.ToString());");
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);
Expand Down
165 changes: 165 additions & 0 deletions YamlDotNet.Analyzers.StaticGenerator/TypeFactoryGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
// SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;

Expand All @@ -30,6 +32,14 @@ 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))
Expand All @@ -50,6 +60,8 @@ public void Execute(GeneratorExecutionContext context)
}

context.AddSource("YamlDotNetAutoGraph.g.cs", GenerateSource(receiver));

ReportUnregisteredTypes(context, receiver);
}

public void Initialize(GeneratorInitializationContext context)
Expand Down Expand Up @@ -118,5 +130,158 @@ private string GenerateSource(SerializableSyntaxReceiver syntaxReceiver)
}
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<string>(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<string> registeredTypeNames,
ITypeSymbol type,
string memberName,
string containingTypeName,
System.Collections.Immutable.ImmutableArray<Location> 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());
}
}
2 changes: 1 addition & 1 deletion YamlDotNet.Fsharp.Test/YamlDotNet.Fsharp.Test.fsproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net6.0;net47</TargetFrameworks>
<TargetFrameworks>net8.0;net47</TargetFrameworks>
<IsPackable>false</IsPackable>
<AssemblyOriginatorKeyFile>..\YamlDotNet.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>
Expand Down
43 changes: 43 additions & 0 deletions YamlDotNet.Test/Analyzers/StaticGenerator/ObjectTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,24 @@ public void ReadOnlyDictionariesAreTreatedAsDictionaries()
Assert.Equal("d", dictionary["c"]);
}

#if NET8_0_OR_GREATER
[Fact]
public void RequiredMembersWork()
{
var deserializer = new StaticDeserializerBuilder(new StaticContext()).Build();
var yaml = @"Name: hello
Value: 42
";
var actual = deserializer.Deserialize<RequiredMemberClass>(yaml);
Assert.Equal("hello", actual.Name);
Assert.Equal(42, actual.Value);

var serializer = new StaticSerializerBuilder(new StaticContext()).Build();
var actualYaml = serializer.Serialize(actual);
Assert.Equal(yaml.NormalizeNewLines().TrimNewLines(), actualYaml.NormalizeNewLines().TrimNewLines());
}
#endif

#if NET6_0_OR_GREATER
[Fact]
public void EnumDeserializationUsesEnumMemberAttribute()
Expand Down Expand Up @@ -399,6 +417,16 @@ private void ExecuteListOverrideTest<TClass>() where TClass : InterfaceLists
Assert.Equal("value2", ((List<string>)actual.TestValue)[1]);
}

[Fact]
public void UnregisteredTypeThrowsDescriptiveException()
{
var context = new StaticContext();
var factory = context.GetFactory();
var ex = Assert.Throws<InvalidOperationException>(() => factory.Create(typeof(UnregisteredType)));
Assert.Contains("is not registered in the YamlDotNet static context", ex.Message);
Assert.Contains("YamlSerializable", ex.Message);
}

[YamlSerializable]
public class TestState
{
Expand Down Expand Up @@ -516,4 +544,19 @@ public interface InterfaceLists
{
object TestValue { get; }
}

// Not decorated with [YamlSerializable] — intentionally unregistered
public class UnregisteredType
{
public string Value { get; set; }
}

#if NET8_0_OR_GREATER
[YamlSerializable]
public class RequiredMemberClass
{
public required string Name { get; set; }
public required int Value { get; set; }
}
#endif
}
Loading
Loading