diff --git a/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs b/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs index 4204d056c..53da24de7 100644 --- a/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs +++ b/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs @@ -10,6 +10,7 @@ using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; namespace Dock.Serializer.SystemTextJson.Generators; @@ -17,6 +18,9 @@ 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 modelProvider = @@ -166,6 +170,7 @@ private sealed record GeneratedSourceArtifact(string HintName, string SourceText private sealed record GenerationModel( bool ShouldGenerate, ImmutableArray Diagnostics, + string ContextTypeName, string ContextSource, ImmutableArray AdditionalSources, ImmutableArray ContextTypes, @@ -177,6 +182,7 @@ public static GenerationModel Empty(ImmutableArray diagnostics) return new GenerationModel( ShouldGenerate: false, Diagnostics: diagnostics, + ContextTypeName: string.Empty, ContextSource: string.Empty, AdditionalSources: ImmutableArray.Empty, ContextTypes: ImmutableArray.Empty, @@ -232,13 +238,15 @@ public static GenerationModel Build(Compilation compilation, System.Threading.Ca ImmutableArray contextTypes = BuildContextTypes(serializableTypes, dockSymbols!); - string contextSource = SourceEmitter.EmitContext(contextTypes); + string contextTypeName = GetUniqueContextTypeName(compilation, cancellationToken); + string contextSource = SourceEmitter.EmitContext(contextTypes, contextTypeName); ImmutableArray 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, @@ -246,6 +254,22 @@ public static GenerationModel Build(Compilation compilation, System.Threading.Ca 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 GetRegisteredTypes( Compilation compilation, ImmutableArray.Builder diagnostics, @@ -1099,6 +1123,7 @@ private static class SystemTextJsonContextGenerator public static ImmutableArray Generate( Compilation compilation, string contextSource, + string contextTypeName, System.Threading.CancellationToken cancellationToken) { ISourceGenerator? generator = CreateGenerator(compilation); @@ -1115,7 +1140,11 @@ public static ImmutableArray 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) @@ -1123,11 +1152,40 @@ public static ImmutableArray Generate( return ImmutableArray.Empty; } + INamedTypeSymbol? contextSymbol = outputCompilation.GetTypeByMetadataName( + GeneratedContextNamespace + "." + contextTypeName); + if (contextSymbol is null) + { + return ImmutableArray.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()) + { + 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() @@ -1203,7 +1261,7 @@ x is not null private static class SourceEmitter { - public static string EmitContext(ImmutableArray contextTypes) + public static string EmitContext(ImmutableArray contextTypes, string contextTypeName) { var builder = new StringBuilder(); builder.AppendLine("// "); @@ -1220,7 +1278,9 @@ public static string EmitContext(ImmutableArray 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(); @@ -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> s_ignoredMembers = CreateIgnoredMembers();"); builder.AppendLine(" private static readonly global::System.Collections.Generic.IReadOnlyDictionary s_objectPayloadDiscriminators = CreateObjectPayloadDiscriminators();"); builder.AppendLine(" private static readonly global::System.Collections.Generic.IReadOnlyDictionary s_objectPayloadTypes = CreateObjectPayloadTypes();"); diff --git a/tests/Dock.Serializer.SystemTextJson.Generators.UnitTests/DockJsonSourceGeneratorTests.cs b/tests/Dock.Serializer.SystemTextJson.Generators.UnitTests/DockJsonSourceGeneratorTests.cs index 88133e969..c0cc0a758 100644 --- a/tests/Dock.Serializer.SystemTextJson.Generators.UnitTests/DockJsonSourceGeneratorTests.cs +++ b/tests/Dock.Serializer.SystemTextJson.Generators.UnitTests/DockJsonSourceGeneratorTests.cs @@ -158,6 +158,34 @@ public sealed class Payload Assert.Contains("Payload", 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() { diff --git a/tests/Dock.Serializer.SystemTextJson.SourceGenTests/SourceGeneratedSerializerTests.cs b/tests/Dock.Serializer.SystemTextJson.SourceGenTests/SourceGeneratedSerializerTests.cs index 9712f279d..792654920 100644 --- a/tests/Dock.Serializer.SystemTextJson.SourceGenTests/SourceGeneratedSerializerTests.cs +++ b/tests/Dock.Serializer.SystemTextJson.SourceGenTests/SourceGeneratedSerializerTests.cs @@ -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() { @@ -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; }