-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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)), | ||
|
@@ -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) | ||
|
@@ -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()) | ||
{ | ||
_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; | ||
} | ||
|
||
|
@@ -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 } } }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. I believe this change is fine since we'll have about the same number of types to consider in most source gen scenarios. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 I can get you a large solution offline, if you need it to test. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggest rewording to avoid "kick off source generation": (here and elsewhere) |
||
</data> | ||
<data name="ContextClassesMustBePartialTitle" xml:space="preserve"> | ||
<value>Derived 'JsonSerializerContext' types and all containing types must be partial.</value> | ||
|
@@ -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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we do backport this to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have a 1 week SLA for translations in |
||
<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> | ||
|
There was a problem hiding this comment.
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.