Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue diagnostic warnings when context types don't indicate serializable types #58768

Closed
wants to merge 1 commit into from
Closed
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
54 changes: 32 additions & 22 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ private sealed class Parser
private const string JsonPropertyNameAttributeFullName = "System.Text.Json.Serialization.JsonPropertyNameAttribute";
private const string JsonPropertyOrderAttributeFullName = "System.Text.Json.Serialization.JsonPropertyOrderAttribute";
private const string JsonSerializerContextFullName = "System.Text.Json.Serialization.JsonSerializerContext";
private const string JsonSerializerAttributeFullName = "System.Text.Json.Serialization.JsonSerializableAttribute";
private const string JsonSerializableAttributeFullName = "System.Text.Json.Serialization.JsonSerializableAttribute";
private const string JsonSourceGenerationOptionsAttributeFullName = "System.Text.Json.Serialization.JsonSourceGenerationOptionsAttribute";

private readonly Compilation _compilation;
Expand Down Expand Up @@ -94,6 +94,14 @@ private sealed class Parser
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

private static DiagnosticDescriptor ContextClassesMustIndicateSerializableTypes { get; } = new DiagnosticDescriptor(
id: "SYSLIB1032",
title: new LocalizableResourceString(nameof(SR.ContextClassesMustIndicateSerializableTypesTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
messageFormat: new LocalizableResourceString(nameof(SR.ContextClassesMustIndicateSerializableTypesMessageFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
category: JsonConstants.SystemTextJsonSourceGenerationName,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

private static DiagnosticDescriptor MultipleJsonConstructorAttribute { get; } = new DiagnosticDescriptor(
id: "SYSLIB1033",
title: new LocalizableResourceString(nameof(SR.MultipleJsonConstructorAttributeTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
Expand Down Expand Up @@ -149,7 +157,7 @@ public Parser(Compilation compilation, in SourceProductionContext sourceProducti
{
Compilation compilation = _compilation;
INamedTypeSymbol jsonSerializerContextSymbol = compilation.GetBestTypeByMetadataName(JsonSerializerContextFullName);
INamedTypeSymbol jsonSerializableAttributeSymbol = compilation.GetBestTypeByMetadataName(JsonSerializerAttributeFullName);
INamedTypeSymbol jsonSerializableAttributeSymbol = compilation.GetBestTypeByMetadataName(JsonSerializableAttributeFullName);
INamedTypeSymbol jsonSourceGenerationOptionsAttributeSymbol = compilation.GetBestTypeByMetadataName(JsonSourceGenerationOptionsAttributeFullName);

if (jsonSerializerContextSymbol == null || jsonSerializableAttributeSymbol == null || jsonSourceGenerationOptionsAttributeSymbol == null)
Expand Down Expand Up @@ -193,19 +201,27 @@ public Parser(Compilation compilation, in SourceProductionContext sourceProducti
}
}

INamedTypeSymbol contextTypeSymbol = (INamedTypeSymbol)compilationSemanticModel.GetDeclaredSymbol(classDeclarationSyntax);
Debug.Assert(contextTypeSymbol != null);
string[] diagnosticMessageArgs = new string[] { contextTypeSymbol.Name };

if (serializableAttributeList == null)
{
// No types were indicated with [JsonSerializable]

// If there are no members on the context, assume that the user wanted source generation and notify them that there are no serializable types.
if (!contextTypeSymbol.MemberNames.Any())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My feeling is that we should refine this to check for the existence of GetTypeInfo overrides. A user that unintentionally adds any member to their context class (be it a helper method, type, etc.) would see this diagnostic disappear completely.

{
_sourceProductionContext.ReportDiagnostic(Diagnostic.Create(ContextClassesMustIndicateSerializableTypes, Location.None, diagnosticMessageArgs));
}

continue;
}

INamedTypeSymbol contextTypeSymbol = (INamedTypeSymbol)compilationSemanticModel.GetDeclaredSymbol(classDeclarationSyntax);
Debug.Assert(contextTypeSymbol != null);

if (!TryGetClassDeclarationList(contextTypeSymbol, out List<string> classDeclarationList))
{
// Class or one of its containing types is not partial so we can't add to it.
_sourceProductionContext.ReportDiagnostic(Diagnostic.Create(ContextClassesMustBePartial, Location.None, new string[] { contextTypeSymbol.Name }));
_sourceProductionContext.ReportDiagnostic(Diagnostic.Create(ContextClassesMustBePartial, Location.None, diagnosticMessageArgs));
continue;
}

Expand Down Expand Up @@ -407,31 +423,25 @@ private static bool TryGetClassDeclarationList(INamedTypeSymbol typeSymbol, [Not
return typeGenerationSpec;
}

internal static bool IsSyntaxTargetForGeneration(SyntaxNode node) => node is ClassDeclarationSyntax { AttributeLists: { Count: > 0 }, BaseList: { Types : {Count : > 0 } } };
internal static bool IsSyntaxTargetForGeneration(SyntaxNode node) => node is ClassDeclarationSyntax { BaseList: { Types : { Count : > 0 } } };
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For diffs between here and EOF, I'm relaxing the criteria for types we consider for generation

Before: include all classes that are annotated with a type called "JsonSerializableAttribute", and also derive or implement at least one type.
After: include all classes that derive from a type called "JsonSerializerContext".

I believe this change is fine since we'll have about the same number of types to consider in most source gen scenarios.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this could impact perf. I would test the before/after of this change and ensure for large solutions that target net6.0 you aren't negatively impacting the perf.

I can get you a large solution offline, if you need it to test.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible this change could also improve perf, depending on the individual performance attributes vs. base classes, and of course the actual solution and its types (do they have base classes, attributes, etc).

@eerhardt how do you suggest measuring perf? Manual measuring the VS experience?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericstj made a micro benchmark for source generators: https://github.com/ericstj/sample-code/tree/generatorBenchmark

But honestly, I would measure it in VS with a large solution. The way I measured the time taken was to add an EventSource around the source generator implementation. And then use PerfView to see how much time was spent in the source generator when it wasn't doing any work.

But if you are using the latest Roslyn builds (as of 5 days ago), this timing measurement is built in. See dotnet/roslyn#55892.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One possible way to mitigate would be check the member count of the type. In pseudocode:

internal static bool IsSyntaxTargetForGeneration(SyntaxNode node) =>
    node is ClassDeclarationSyntax { 
            { BaseList: { Types : { Count : > 0 } } and 
            { AttributeLists: { Count: > 0 } } or 
            { MemberList: { Count: 0 } } };

Note that this would contradict my feedback in https://github.com/dotnet/runtime/pull/58768/files#r704458155.


internal static ClassDeclarationSyntax? GetSemanticTargetForGeneration(GeneratorSyntaxContext context)
{
var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;

foreach (AttributeListSyntax attributeListSyntax in classDeclarationSyntax.AttributeLists)
foreach (BaseTypeSyntax baseTypeSyntax in classDeclarationSyntax.BaseList.Types)
{
foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes)
INamedTypeSymbol? baseTypeSymbol = context.SemanticModel.GetSymbolInfo(baseTypeSyntax.Type).Symbol as INamedTypeSymbol;
if (baseTypeSymbol == null)
{
IMethodSymbol attributeSymbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol as IMethodSymbol;
if (attributeSymbol == null)
{
continue;
}

INamedTypeSymbol attributeContainingTypeSymbol = attributeSymbol.ContainingType;
string fullName = attributeContainingTypeSymbol.ToDisplayString();

if (fullName == "System.Text.Json.Serialization.JsonSerializableAttribute")
{
return classDeclarationSyntax;
}
continue;
}

string fullName = baseTypeSymbol.ToDisplayString();
if (fullName == JsonSerializerContextFullName)
{
return classDeclarationSyntax;
}
}

return null;
Expand Down
8 changes: 7 additions & 1 deletion src/libraries/System.Text.Json/gen/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
<value>Did not generate serialization metadata for type.</value>
</data>
<data name="ContextClassesMustBePartialMessageFormat" xml:space="preserve">
<value>Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation.</value>
<value>Derived 'JsonSerializerContext' type '{0}' and all containing types must be made partial to kick off source generation.</value>
Copy link
Member

@steveharter steveharter Sep 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest rewording to avoid "kick off source generation":
"..and all containing types must be made partial for source generation."

(here and elsewhere)

</data>
<data name="ContextClassesMustBePartialTitle" xml:space="preserve">
<value>Derived 'JsonSerializerContext' types and all containing types must be partial.</value>
Expand All @@ -141,4 +141,10 @@
<data name="MultipleJsonConstructorAttributeTitle" xml:space="preserve">
<value>Type has multiple constructors annotated with JsonConstructorAttribute.</value>
</data>
<data name="ContextClassesMustIndicateSerializableTypesMessageFormat" xml:space="preserve">
<value>Derived 'JsonSerializerContext' type '{0}' must indicate at least one serializable type using 'JsonSerializableAttribute'.</value>
</data>
<data name="ContextClassesMustIndicateSerializableTypesTitle" xml:space="preserve">
<value>Derived 'JsonSerializerContext' type '{0}' must indicate serializable types.</value>
</data>
</root>
14 changes: 12 additions & 2 deletions src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@
<file datatype="xml" source-language="en" target-language="cs" original="../Strings.resx">
<body>
<trans-unit id="ContextClassesMustBePartialMessageFormat">
<source>Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation.</source>
<target state="translated">Odvozený typ JsonSerializerContext {0} určuje typy serializovatelné na JSON. Typ a všechny obsahující typy musí být částečné, aby se zahájilo generování zdroje.</target>
<source>Derived 'JsonSerializerContext' type '{0}' and all containing types must be made partial to kick off source generation.</source>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do backport this to release/6.0 now, isn't there a risk of translations going out of sync?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a 1 week SLA for translations in main. We have enough time to backport to 6.0. That backport will need to be tracked with an issue so we don't forget.

<target state="needs-review-translation">Odvozený typ JsonSerializerContext {0} určuje typy serializovatelné na JSON. Typ a všechny obsahující typy musí být částečné, aby se zahájilo generování zdroje.</target>
<note />
</trans-unit>
<trans-unit id="ContextClassesMustBePartialTitle">
<source>Derived 'JsonSerializerContext' types and all containing types must be partial.</source>
<target state="translated">Odvozené typy JsonSerializerContext a všechny obsahující typy musí být částečné.</target>
<note />
</trans-unit>
<trans-unit id="ContextClassesMustIndicateSerializableTypesMessageFormat">
<source>Derived 'JsonSerializerContext' type '{0}' must indicate at least one serializable type using 'JsonSerializableAttribute'.</source>
<target state="new">Derived 'JsonSerializerContext' type '{0}' must indicate at least one serializable type using 'JsonSerializableAttribute'.</target>
<note />
</trans-unit>
<trans-unit id="ContextClassesMustIndicateSerializableTypesTitle">
<source>Derived 'JsonSerializerContext' type '{0}' must indicate serializable types.</source>
<target state="new">Derived 'JsonSerializerContext' type '{0}' must indicate serializable types.</target>
<note />
</trans-unit>
<trans-unit id="DuplicateTypeNameMessageFormat">
<source>There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision.</source>
<target state="translated">Existuje několik typů s názvem {0}. Zdroj se vygeneroval pro první zjištěný typ. Tuto kolizi vyřešíte pomocí JsonSerializableAttribute.TypeInfoPropertyName.</target>
Expand Down
14 changes: 12 additions & 2 deletions src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@
<file datatype="xml" source-language="en" target-language="de" original="../Strings.resx">
<body>
<trans-unit id="ContextClassesMustBePartialMessageFormat">
<source>Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation.</source>
<target state="translated">Der abgeleitete JsonSerializerContext-Typ "{0}" gibt serialisierbare JSON-Typen an. Der Typ und alle enthaltenden Typen müssen partiell sein, um die Quellgenerierung zu starten.</target>
<source>Derived 'JsonSerializerContext' type '{0}' and all containing types must be made partial to kick off source generation.</source>
<target state="needs-review-translation">Der abgeleitete JsonSerializerContext-Typ "{0}" gibt serialisierbare JSON-Typen an. Der Typ und alle enthaltenden Typen müssen partiell sein, um die Quellgenerierung zu starten.</target>
<note />
</trans-unit>
<trans-unit id="ContextClassesMustBePartialTitle">
<source>Derived 'JsonSerializerContext' types and all containing types must be partial.</source>
<target state="translated">Abgeleitete JsonSerializerContext-Typen und alle enthaltenden Typen müssen partiell sein.</target>
<note />
</trans-unit>
<trans-unit id="ContextClassesMustIndicateSerializableTypesMessageFormat">
<source>Derived 'JsonSerializerContext' type '{0}' must indicate at least one serializable type using 'JsonSerializableAttribute'.</source>
<target state="new">Derived 'JsonSerializerContext' type '{0}' must indicate at least one serializable type using 'JsonSerializableAttribute'.</target>
<note />
</trans-unit>
<trans-unit id="ContextClassesMustIndicateSerializableTypesTitle">
<source>Derived 'JsonSerializerContext' type '{0}' must indicate serializable types.</source>
<target state="new">Derived 'JsonSerializerContext' type '{0}' must indicate serializable types.</target>
<note />
</trans-unit>
<trans-unit id="DuplicateTypeNameMessageFormat">
<source>There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision.</source>
<target state="translated">Es sind mehrere Typen namens "{0}" vorhanden. Die Quelle wurde für den ersten festgestellten Typ generiert. Verwenden Sie "JsonSerializableAttribute.TypeInfoPropertyName", um diesen Konflikt zu beheben.</target>
Expand Down
14 changes: 12 additions & 2 deletions src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@
<file datatype="xml" source-language="en" target-language="es" original="../Strings.resx">
<body>
<trans-unit id="ContextClassesMustBePartialMessageFormat">
<source>Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation.</source>
<target state="translated">El tipo derivado "JsonSerializerContext" "{0}" especifica tipos de JSON serializables. El tipo y todos los tipos que contienen deben hacerse parciales para iniciar la generación de origen.</target>
<source>Derived 'JsonSerializerContext' type '{0}' and all containing types must be made partial to kick off source generation.</source>
<target state="needs-review-translation">El tipo derivado "JsonSerializerContext" "{0}" especifica tipos de JSON serializables. El tipo y todos los tipos que contienen deben hacerse parciales para iniciar la generación de origen.</target>
<note />
</trans-unit>
<trans-unit id="ContextClassesMustBePartialTitle">
<source>Derived 'JsonSerializerContext' types and all containing types must be partial.</source>
<target state="translated">Los tipos derivados "JsonSerializerContext" y todos los tipos que contienen deben ser parciales.</target>
<note />
</trans-unit>
<trans-unit id="ContextClassesMustIndicateSerializableTypesMessageFormat">
<source>Derived 'JsonSerializerContext' type '{0}' must indicate at least one serializable type using 'JsonSerializableAttribute'.</source>
<target state="new">Derived 'JsonSerializerContext' type '{0}' must indicate at least one serializable type using 'JsonSerializableAttribute'.</target>
<note />
</trans-unit>
<trans-unit id="ContextClassesMustIndicateSerializableTypesTitle">
<source>Derived 'JsonSerializerContext' type '{0}' must indicate serializable types.</source>
<target state="new">Derived 'JsonSerializerContext' type '{0}' must indicate serializable types.</target>
<note />
</trans-unit>
<trans-unit id="DuplicateTypeNameMessageFormat">
<source>There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision.</source>
<target state="translated">Hay varios tipos denominados {0}. El origen se generó para el primero detectado. Use "JsonSerializableAttribute.TypeInfoPropertyName" para resolver esta colisión.</target>
Expand Down
14 changes: 12 additions & 2 deletions src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@
<file datatype="xml" source-language="en" target-language="fr" original="../Strings.resx">
<body>
<trans-unit id="ContextClassesMustBePartialMessageFormat">
<source>Derived 'JsonSerializerContext' type '{0}' specifies JSON-serializable types. The type and all containing types must be made partial to kick off source generation.</source>
<target state="translated">Le type 'JsonSerializerContext' dérivé '{0}' spécifie des types Sérialisables JSON. Le type et tous les types contenants doivent être rendus partiels pour lancer la génération de la source.</target>
<source>Derived 'JsonSerializerContext' type '{0}' and all containing types must be made partial to kick off source generation.</source>
<target state="needs-review-translation">Le type 'JsonSerializerContext' dérivé '{0}' spécifie des types Sérialisables JSON. Le type et tous les types contenants doivent être rendus partiels pour lancer la génération de la source.</target>
<note />
</trans-unit>
<trans-unit id="ContextClassesMustBePartialTitle">
<source>Derived 'JsonSerializerContext' types and all containing types must be partial.</source>
<target state="translated">Les types dérivés 'JsonSerializerContext' et tous les types conteneurs doivent être partiels.</target>
<note />
</trans-unit>
<trans-unit id="ContextClassesMustIndicateSerializableTypesMessageFormat">
<source>Derived 'JsonSerializerContext' type '{0}' must indicate at least one serializable type using 'JsonSerializableAttribute'.</source>
<target state="new">Derived 'JsonSerializerContext' type '{0}' must indicate at least one serializable type using 'JsonSerializableAttribute'.</target>
<note />
</trans-unit>
<trans-unit id="ContextClassesMustIndicateSerializableTypesTitle">
<source>Derived 'JsonSerializerContext' type '{0}' must indicate serializable types.</source>
<target state="new">Derived 'JsonSerializerContext' type '{0}' must indicate serializable types.</target>
<note />
</trans-unit>
<trans-unit id="DuplicateTypeNameMessageFormat">
<source>There are multiple types named {0}. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision.</source>
<target state="translated">Plusieurs types nommés {0}. La source a été générée pour la première détection détectée. Utilisez « JsonSerializableAttribute.TypeInfoPropertyName » pour résoudre cette collision.</target>
Expand Down
Loading