-
Notifications
You must be signed in to change notification settings - Fork 124
Add authoring analyzers for [ApiContract] and [ContractVersion] #2407
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
base: staging/3.0
Are you sure you want to change the base?
Changes from all commits
d0ad453
88dbe95
28a7efa
48a4503
6871805
7cc5a2d
f1e2c51
ab17e38
93c5e0b
0e5946f
3132e1a
35dbacc
6f188e1
4d9d3f6
fdfb225
61a4562
f951cc3
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 |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Collections.Immutable; | ||
| using System.Linq; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.Diagnostics; | ||
|
|
||
| namespace WindowsRuntime.SourceGenerator.Diagnostics; | ||
|
|
||
| /// <summary> | ||
| /// A diagnostic analyzer that validates that <c>[ApiContract]</c> enum types declare their contract version using <c>[ContractVersion]</c>. | ||
| /// </summary> | ||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
| public sealed class ApiContractTypeRequiresContractVersionAnalyzer : DiagnosticAnalyzer | ||
| { | ||
| /// <inheritdoc/> | ||
| public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [DiagnosticDescriptors.ApiContractTypeMissingContractVersion]; | ||
|
|
||
| /// <inheritdoc/> | ||
| public override void Initialize(AnalysisContext context) | ||
| { | ||
| context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
| context.EnableConcurrentExecution(); | ||
|
|
||
| context.RegisterCompilationStartAction(static context => | ||
| { | ||
| // This analyzer only applies to Windows Runtime component authoring scenarios | ||
| if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetCsWinRTComponent()) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Get the '[ApiContract]' symbol | ||
| if (context.Compilation.GetTypeByMetadataName("Windows.Foundation.Metadata.ApiContractAttribute") is not { } apiContractAttributeType) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Get the '[ContractVersion]' symbol | ||
| if (context.Compilation.GetTypeByMetadataName("Windows.Foundation.Metadata.ContractVersionAttribute") is not { } contractVersionAttributeType) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| context.RegisterSymbolAction(context => | ||
| { | ||
| // Only enum types can be valid API contract types | ||
| if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Enum } typeSymbol) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Immediately bail if the type is not an API contract type | ||
| if (!typeSymbol.HasAttributeWithType(apiContractAttributeType)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Check whether any '[ContractVersion]' attribute using a version-only constructor is applied | ||
| foreach (AttributeData attribute in typeSymbol.GetAttributes(contractVersionAttributeType)) | ||
| { | ||
| // The version-only constructors are '(uint)' and '(string, uint)'. | ||
| // The contract-type constructor is '(Type, uint)', which we want to ignore here. | ||
| if (attribute.AttributeConstructor?.Parameters is [{ Type.SpecialType: SpecialType.System_UInt32 or SpecialType.System_String }, ..]) | ||
| { | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| context.ReportDiagnostic(Diagnostic.Create( | ||
| DiagnosticDescriptors.ApiContractTypeMissingContractVersion, | ||
| typeSymbol.Locations.FirstOrDefault(), | ||
| typeSymbol)); | ||
| }, SymbolKind.NamedType); | ||
| }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Collections.Immutable; | ||
| using System.Linq; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.Diagnostics; | ||
|
|
||
| namespace WindowsRuntime.SourceGenerator.Diagnostics; | ||
|
|
||
| /// <summary> | ||
| /// A diagnostic analyzer that reports when a public authored type is missing a <c>[ContractVersion]</c> attribute. | ||
| /// </summary> | ||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
| public sealed class PublicTypeRequiresContractVersionAnalyzer : DiagnosticAnalyzer | ||
| { | ||
| /// <inheritdoc/> | ||
| public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [DiagnosticDescriptors.PublicTypeMissingContractVersion]; | ||
|
|
||
| /// <inheritdoc/> | ||
| public override void Initialize(AnalysisContext context) | ||
| { | ||
| context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
| context.EnableConcurrentExecution(); | ||
|
|
||
| context.RegisterCompilationStartAction(static context => | ||
| { | ||
| // This analyzer only applies to Windows Runtime component authoring scenarios | ||
| if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetCsWinRTComponent()) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Get the '[ContractVersion]' symbol | ||
| if (context.Compilation.GetTypeByMetadataName("Windows.Foundation.Metadata.ContractVersionAttribute") is not { } contractVersionAttributeType) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Get the '[ApiContract]' symbol (used to skip API contract types, which are validated by a different analyzer) | ||
| INamedTypeSymbol? apiContractAttributeType = context.Compilation.GetTypeByMetadataName("Windows.Foundation.Metadata.ApiContractAttribute"); | ||
|
|
||
| context.RegisterSymbolAction(context => | ||
| { | ||
| // Only consider top-level public types | ||
| if (context.Symbol is not INamedTypeSymbol { DeclaredAccessibility: Accessibility.Public, ContainingType: null } typeSymbol) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Skip API contract types: those are handled by 'ApiContractTypeRequiresContractVersionAnalyzer' | ||
| if (apiContractAttributeType is not null && | ||
| typeSymbol is { TypeKind: TypeKind.Enum } && | ||
| typeSymbol.HasAttributeWithType(apiContractAttributeType)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Skip if any '[ContractVersion]' attribute is already applied (validity of the | ||
| // specific constructor used is reported by the other 'ContractVersion' analyzers) | ||
| if (typeSymbol.HasAttributeWithType(contractVersionAttributeType)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| context.ReportDiagnostic(Diagnostic.Create( | ||
|
Member
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. They might be doing regular versioning rather than contract versioning which we should take into account.
Member
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. and I believe we default version attribute if not specified.
Member
Author
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. Yeah this is why this diagnostic is just info, not a warning. Is that fine or should we still change it?
Member
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. Given regular versioning and contract versioning have different audiences, I think we basically have a property that someone can set to true if they want to use contract versioning and if that is set, all the diagnostics for it are enabled and enforced. If it isn't set and it will default to false, then regular versioning is done and the diagnostics are not enforced. Thoughts?
Member
Author
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. Mh having an additional MSBuild property feels a bit overkill perhaps. What about:
This way there's also no property to worry about, things just adapt automatically to your code.
Member
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. That could work as long as the diagnostic message for the first one is clear enough to be like you are not using regular or contract versioning, so we will default version to 1 unless you manually specify or so. |
||
| DiagnosticDescriptors.PublicTypeMissingContractVersion, | ||
| typeSymbol.Locations.FirstOrDefault(), | ||
| typeSymbol)); | ||
| }, SymbolKind.NamedType); | ||
| }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Collections.Immutable; | ||
| using System.Linq; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.Diagnostics; | ||
|
|
||
| namespace WindowsRuntime.SourceGenerator.Diagnostics; | ||
|
|
||
| /// <summary> | ||
| /// A diagnostic analyzer that validates that <c>[ApiContract]</c> enum types do not define any enum cases. | ||
| /// </summary> | ||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
| public sealed class ValidApiContractEnumTypeAnalyzer : DiagnosticAnalyzer | ||
| { | ||
| /// <inheritdoc/> | ||
| public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [DiagnosticDescriptors.ApiContractEnumWithCases]; | ||
|
|
||
| /// <inheritdoc/> | ||
| public override void Initialize(AnalysisContext context) | ||
| { | ||
| context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
| context.EnableConcurrentExecution(); | ||
|
|
||
| context.RegisterCompilationStartAction(static context => | ||
| { | ||
| // This analyzer only applies to Windows Runtime component authoring scenarios | ||
| if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetCsWinRTComponent()) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Get the '[ApiContract]' symbol | ||
| if (context.Compilation.GetTypeByMetadataName("Windows.Foundation.Metadata.ApiContractAttribute") is not { } attributeType) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| context.RegisterSymbolAction(context => | ||
| { | ||
| // Only enum types can be annotated with '[ApiContract]' | ||
| if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Enum } typeSymbol) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Immediately bail if the type doesn't have the attribute | ||
| if (!typeSymbol.HasAttributeWithType(attributeType)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Warn if the enum defines any enum cases (i.e. any fields other than the implicit value field) | ||
| if (typeSymbol.GetMembers().Any(static member => member is IFieldSymbol { IsConst: true })) | ||
| { | ||
| context.ReportDiagnostic(Diagnostic.Create( | ||
| DiagnosticDescriptors.ApiContractEnumWithCases, | ||
| typeSymbol.Locations.FirstOrDefault(), | ||
| typeSymbol)); | ||
| } | ||
| }, SymbolKind.NamedType); | ||
| }); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Collections.Immutable; | ||
| using System.Linq; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.Diagnostics; | ||
|
|
||
| namespace WindowsRuntime.SourceGenerator.Diagnostics; | ||
|
|
||
| /// <summary> | ||
| /// A diagnostic analyzer that validates applications of <c>[ContractVersion]</c> attributes. | ||
| /// </summary> | ||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
| public sealed class ValidContractVersionAttributeAnalyzer : DiagnosticAnalyzer | ||
| { | ||
| /// <inheritdoc/> | ||
| public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [ | ||
| DiagnosticDescriptors.ContractVersionAttributeRequiresApiContractTarget, | ||
| DiagnosticDescriptors.ContractVersionAttributeNotAllowedOnApiContractTarget, | ||
| DiagnosticDescriptors.ContractVersionAttributeInvalidContractTypeArgument]; | ||
|
|
||
| /// <inheritdoc/> | ||
| public override void Initialize(AnalysisContext context) | ||
| { | ||
| context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
| context.EnableConcurrentExecution(); | ||
|
|
||
| context.RegisterCompilationStartAction(static context => | ||
| { | ||
| // This analyzer only applies to Windows Runtime component authoring scenarios | ||
| if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetCsWinRTComponent()) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Get the '[ContractVersion]' symbol | ||
| if (context.Compilation.GetTypeByMetadataName("Windows.Foundation.Metadata.ContractVersionAttribute") is not { } contractVersionAttributeType) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Get the '[ApiContract]' symbol | ||
| if (context.Compilation.GetTypeByMetadataName("Windows.Foundation.Metadata.ApiContractAttribute") is not { } apiContractAttributeType) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| context.RegisterSymbolAction(context => | ||
| { | ||
| if (context.Symbol is not INamedTypeSymbol typeSymbol) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| bool isApiContractType = IsApiContractType(typeSymbol, apiContractAttributeType); | ||
|
|
||
| foreach (AttributeData attribute in typeSymbol.GetAttributes(contractVersionAttributeType)) | ||
| { | ||
| // We can have multiple '[ContractVersion]' uses, so we need to iterate and check each of them | ||
| if (attribute.AttributeConstructor is not { } constructor) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| ImmutableArray<IParameterSymbol> parameters = constructor.Parameters; | ||
|
|
||
| // Identify the constructor by its first parameter type: | ||
| // - 'ContractVersionAttribute(uint)' | ||
| // - 'ContractVersionAttribute(string, uint)' | ||
| // - 'ContractVersionAttribute(Type, uint)' | ||
| bool isVersionOnlyConstructor = parameters is | ||
| [{ Type.SpecialType: SpecialType.System_UInt32 }] or | ||
| [{ Type.SpecialType: SpecialType.System_String }, _]; | ||
|
|
||
| bool isContractTypeConstructor = parameters is | ||
| [{ Type: INamedTypeSymbol { MetadataName: "Type", ContainingNamespace.Name: "System" } }, _]; | ||
|
|
||
| // The version-only constructors must be applied to API contract types | ||
| if (isVersionOnlyConstructor && !isApiContractType) | ||
| { | ||
| context.ReportDiagnostic(Diagnostic.Create( | ||
| DiagnosticDescriptors.ContractVersionAttributeRequiresApiContractTarget, | ||
| attribute.GetLocation(context.CancellationToken) ?? typeSymbol.Locations.FirstOrDefault(), | ||
| typeSymbol)); | ||
| } | ||
| else if (isContractTypeConstructor) | ||
| { | ||
| // The contract-type constructor must not be applied to API contract types | ||
| if (isApiContractType) | ||
| { | ||
| context.ReportDiagnostic(Diagnostic.Create( | ||
| DiagnosticDescriptors.ContractVersionAttributeNotAllowedOnApiContractTarget, | ||
| attribute.GetLocation(context.CancellationToken) ?? typeSymbol.Locations.FirstOrDefault(), | ||
| typeSymbol)); | ||
| } | ||
|
|
||
| // The contract type argument must be a valid API contract type | ||
| if (attribute.ConstructorArguments is [{ Value: INamedTypeSymbol contractTypeArgument }, ..] && | ||
| !IsApiContractType(contractTypeArgument, apiContractAttributeType)) | ||
| { | ||
| context.ReportDiagnostic(Diagnostic.Create( | ||
| DiagnosticDescriptors.ContractVersionAttributeInvalidContractTypeArgument, | ||
| attribute.GetArgumentLocation(argumentIndex: 0, context.CancellationToken) | ||
| ?? attribute.GetLocation(context.CancellationToken) | ||
| ?? typeSymbol.Locations.FirstOrDefault(), | ||
| typeSymbol, | ||
| contractTypeArgument)); | ||
| } | ||
| } | ||
| } | ||
| }, SymbolKind.NamedType); | ||
| }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks whether a type is a valid API contract type (an enum type annotated with <c>[ApiContract]</c>). | ||
| /// </summary> | ||
| /// <param name="typeSymbol">The type symbol to check.</param> | ||
| /// <param name="apiContractAttributeType">The <c>[ApiContract]</c> attribute symbol.</param> | ||
| /// <returns>Whether <paramref name="typeSymbol"/> is a valid API contract type.</returns> | ||
| private static bool IsApiContractType(INamedTypeSymbol typeSymbol, INamedTypeSymbol apiContractAttributeType) | ||
| { | ||
| return typeSymbol is { TypeKind: TypeKind.Enum } && typeSymbol.HasAttributeWithType(apiContractAttributeType); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.