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
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace Dock.Serializer.SystemTextJson.Generators;

[Generator]
public sealed class DockJsonSourceGenerator : IIncrementalGenerator
{
private const string GeneratedContextTypeNameBase = "DockSerializerGeneratedJsonContext";
private const string GeneratedContextNamespace = "Dock.Serializer.SystemTextJson";

public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider<GenerationModel> modelProvider =
Expand Down Expand Up @@ -166,6 +170,7 @@ private sealed record GeneratedSourceArtifact(string HintName, string SourceText
private sealed record GenerationModel(
bool ShouldGenerate,
ImmutableArray<Diagnostic> Diagnostics,
string ContextTypeName,
string ContextSource,
ImmutableArray<GeneratedSourceArtifact> AdditionalSources,
ImmutableArray<string> ContextTypes,
Expand All @@ -177,6 +182,7 @@ public static GenerationModel Empty(ImmutableArray<Diagnostic> diagnostics)
return new GenerationModel(
ShouldGenerate: false,
Diagnostics: diagnostics,
ContextTypeName: string.Empty,
ContextSource: string.Empty,
AdditionalSources: ImmutableArray<GeneratedSourceArtifact>.Empty,
ContextTypes: ImmutableArray<string>.Empty,
Expand Down Expand Up @@ -232,20 +238,38 @@ public static GenerationModel Build(Compilation compilation, System.Threading.Ca

ImmutableArray<string> contextTypes =
BuildContextTypes(serializableTypes, dockSymbols!);
string contextSource = SourceEmitter.EmitContext(contextTypes);
string contextTypeName = GetUniqueContextTypeName(compilation, cancellationToken);
string contextSource = SourceEmitter.EmitContext(contextTypes, contextTypeName);
ImmutableArray<GeneratedSourceArtifact> additionalSources =
SystemTextJsonContextGenerator.Generate(compilation, contextSource, cancellationToken);
SystemTextJsonContextGenerator.Generate(compilation, contextSource, contextTypeName, cancellationToken);

return new GenerationModel(
ShouldGenerate: true,
Diagnostics: diagnostics.ToImmutable(),
ContextTypeName: contextTypeName,
ContextSource: contextSource,
AdditionalSources: additionalSources,
ContextTypes: contextTypes,
Polymorphisms: polymorphisms,
IgnoredMembers: ignoredMembers);
}

private static string GetUniqueContextTypeName(
Compilation compilation,
System.Threading.CancellationToken cancellationToken)
{
var suffix = 0;
var candidate = GeneratedContextTypeNameBase;

while (compilation.GetSymbolsWithName(candidate, SymbolFilter.Type, cancellationToken).Any())
{
suffix++;
candidate = GeneratedContextTypeNameBase + "_" + suffix.ToString(CultureInfo.InvariantCulture);
}

return candidate;
}

private static ImmutableArray<RegistrationCandidate> GetRegisteredTypes(
Compilation compilation,
ImmutableArray<Diagnostic>.Builder diagnostics,
Expand Down Expand Up @@ -1099,6 +1123,7 @@ private static class SystemTextJsonContextGenerator
public static ImmutableArray<GeneratedSourceArtifact> Generate(
Compilation compilation,
string contextSource,
string contextTypeName,
System.Threading.CancellationToken cancellationToken)
{
ISourceGenerator? generator = CreateGenerator(compilation);
Expand All @@ -1115,19 +1140,52 @@ public static ImmutableArray<GeneratedSourceArtifact> Generate(
GeneratorDriver driver = CSharpGeneratorDriver.Create(
generators: new[] { generator },
parseOptions: parseOptions);
driver = driver.RunGenerators(augmentedCompilation, cancellationToken);
driver = driver.RunGeneratorsAndUpdateCompilation(
augmentedCompilation,
out Compilation outputCompilation,
out _,
cancellationToken);

GeneratorDriverRunResult runResult = driver.GetRunResult();
if (runResult.Results.Length == 0)
{
return ImmutableArray<GeneratedSourceArtifact>.Empty;
}

INamedTypeSymbol? contextSymbol = outputCompilation.GetTypeByMetadataName(
GeneratedContextNamespace + "." + contextTypeName);
if (contextSymbol is null)
{
return ImmutableArray<GeneratedSourceArtifact>.Empty;
}

return runResult.Results[0].GeneratedSources
.Where(x => IsContextArtifact(x, outputCompilation, contextSymbol, cancellationToken))
.Select(static x => new GeneratedSourceArtifact("SystemTextJson." + x.HintName, x.SourceText.ToString()))
.ToImmutableArray();
}

private static bool IsContextArtifact(
GeneratedSourceResult source,
Compilation outputCompilation,
INamedTypeSymbol contextSymbol,
System.Threading.CancellationToken cancellationToken)
{
SemanticModel semanticModel = outputCompilation.GetSemanticModel(source.SyntaxTree);
SyntaxNode root = source.SyntaxTree.GetRoot(cancellationToken);

foreach (ClassDeclarationSyntax declaration in root.DescendantNodes().OfType<ClassDeclarationSyntax>())
{
if (semanticModel.GetDeclaredSymbol(declaration, cancellationToken) is INamedTypeSymbol declaredSymbol
&& SymbolEqualityComparer.Default.Equals(declaredSymbol, contextSymbol))
{
return true;
}
}

return false;
}

private static ISourceGenerator? CreateGenerator(Compilation compilation)
{
Assembly? assembly = AppDomain.CurrentDomain.GetAssemblies()
Expand Down Expand Up @@ -1203,7 +1261,7 @@ x is not null

private static class SourceEmitter
{
public static string EmitContext(ImmutableArray<string> contextTypes)
public static string EmitContext(ImmutableArray<string> contextTypes, string contextTypeName)
{
var builder = new StringBuilder();
builder.AppendLine("// <auto-generated />");
Expand All @@ -1220,7 +1278,9 @@ public static string EmitContext(ImmutableArray<string> contextTypes)
builder.AppendLine("))]");
}

builder.AppendLine("internal sealed partial class DockSystemTextJsonContext : global::System.Text.Json.Serialization.JsonSerializerContext");
builder.Append("internal sealed partial class ");
builder.Append(contextTypeName);
builder.AppendLine(" : global::System.Text.Json.Serialization.JsonSerializerContext");
builder.AppendLine("{");
builder.AppendLine("}");
return builder.ToString();
Expand All @@ -1236,7 +1296,9 @@ public static string EmitGenerated(GenerationModel model)
builder.AppendLine();
builder.AppendLine("internal sealed class DockSystemTextJsonResolver : global::System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver");
builder.AppendLine("{");
builder.AppendLine(" private static readonly global::System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver s_resolver = global::System.Text.Json.Serialization.Metadata.JsonTypeInfoResolver.WithAddedModifier(DockSystemTextJsonContext.Default, ModifyTypeInfo);");
builder.Append(" private static readonly global::System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver s_resolver = global::System.Text.Json.Serialization.Metadata.JsonTypeInfoResolver.WithAddedModifier(");
builder.Append(model.ContextTypeName);
builder.AppendLine(".Default, ModifyTypeInfo);");
builder.AppendLine(" private static readonly global::System.Collections.Generic.IReadOnlyDictionary<global::System.Type, global::System.Collections.Generic.HashSet<string>> s_ignoredMembers = CreateIgnoredMembers();");
builder.AppendLine(" private static readonly global::System.Collections.Generic.IReadOnlyDictionary<global::System.Type, string> s_objectPayloadDiscriminators = CreateObjectPayloadDiscriminators();");
builder.AppendLine(" private static readonly global::System.Collections.Generic.IReadOnlyDictionary<string, global::System.Type> s_objectPayloadTypes = CreateObjectPayloadTypes();");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,34 @@ public sealed class Payload<T>
Assert.Contains("Payload<string>", generatedSource);
}

[Fact]
public void ApplicationContextWithGeneratedSimpleName_DoesNotCollideWithDockContext()
{
const string source = """
using Dock.Serializer.SystemTextJson;
using System.Text.Json.Serialization;

[assembly: DockJsonSourceGeneration]

namespace Example;

[JsonSerializable(typeof(string))]
internal sealed partial class DockSerializerGeneratedJsonContext : JsonSerializerContext
{
}
""";

CompilationRun run = Run(source);
GeneratorRunResult result = Assert.Single(run.RunResult.Results);
Assert.DoesNotContain(run.RunResult.Diagnostics, x => x.Severity == DiagnosticSeverity.Error);
Assert.DoesNotContain(result.Diagnostics, x => x.Severity == DiagnosticSeverity.Error);
string contextSource = GetGeneratedSource(run, "DockSystemTextJsonContext.g.cs");
string generatedSource = GetGeneratedSource(run, "DockSystemTextJsonGenerated.g.cs");

Assert.Contains("DockSerializerGeneratedJsonContext_1", contextSource);
Assert.Contains("DockSerializerGeneratedJsonContext_1.Default", generatedSource);
}

[Fact]
public void AutoDiscovery_IncludesProtectedInternalNestedDockTypes()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,35 @@ namespace Dock.Serializer.SystemTextJson.SourceGenTests;

public class SourceGeneratedSerializerTests
{
[Fact]
public void DockGenerator_CoexistsWithApplicationJsonContexts()
{
var first = new WeatherForecast
{
Date = new DateTime(2026, 7, 14),
Summary = "Sunny"
};
var second = new WeatherForecast2
{
TemperatureCelsius = 21,
Summary = "Clear"
};

string firstJson = JsonSerializer.Serialize(
first,
ApplicationJsonContexts.WeatherForecastContext.Default.WeatherForecast);
string secondJson = JsonSerializer.Serialize(
second,
ApplicationJsonContexts.WeatherForecast2Context.Default.WeatherForecast2);
string collidingNameJson = JsonSerializer.Serialize(
first,
DockSerializerGeneratedJsonContext.Default.WeatherForecast);

Assert.Contains("Sunny", firstJson, StringComparison.Ordinal);
Assert.Contains("21", secondJson, StringComparison.Ordinal);
Assert.Contains("Sunny", collidingNameJson, StringComparison.Ordinal);
}

[Fact]
public void GeneratedSerializer_Roundtrip_CustomDockTypes_Works()
{
Expand Down Expand Up @@ -465,6 +494,40 @@ public sealed class UnregisteredPayload
public string? Name { get; set; }
}

public sealed class WeatherForecast
{
public DateTime Date { get; set; }

public string? Summary { get; set; }
}

public sealed class WeatherForecast2
{
public int TemperatureCelsius { get; set; }

public string? Summary { get; set; }
}

public static partial class ApplicationJsonContexts
{
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(WeatherForecast))]
internal partial class WeatherForecastContext : JsonSerializerContext
{
}

[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(WeatherForecast2))]
internal partial class WeatherForecast2Context : JsonSerializerContext
{
}
}

[JsonSerializable(typeof(WeatherForecast))]
internal partial class DockSerializerGeneratedJsonContext : JsonSerializerContext
{
}

public class CustomRootDock : RootDock
{
public string? RootTag { get; set; }
Expand Down
Loading