Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d0ad453
Add CSWINRT2010 diagnostic descriptor for invalid '[ApiContract]' enu…
Sergio0694 Apr 24, 2026
88dbe95
Add ValidApiContractEnumTypeAnalyzer for CSWINRT2010
Sergio0694 Apr 24, 2026
28a7efa
Add tests for ValidApiContractEnumTypeAnalyzer
Sergio0694 Apr 24, 2026
48a4503
Add CSWINRT2011-CSWINRT2013 diagnostic descriptors for invalid '[Cont…
Sergio0694 Apr 24, 2026
6871805
Add ValidContractVersionAttributeAnalyzer for CSWINRT2011-CSWINRT2013
Sergio0694 Apr 24, 2026
7cc5a2d
Add tests for ValidContractVersionAttributeAnalyzer
Sergio0694 Apr 24, 2026
f1e2c51
Update ContractVersionAttribute API and docs
Sergio0694 Apr 24, 2026
ab17e38
Validate multiple ContractVersion attributes
Sergio0694 Apr 24, 2026
93c5e0b
Move AttributeData location helpers to AttributeDataExtensions
Sergio0694 Apr 24, 2026
0e5946f
Add CSWINRT2014 diagnostic descriptor for API contract types missing …
Sergio0694 Apr 24, 2026
3132e1a
Add ApiContractTypeRequiresContractVersionAnalyzer for CSWINRT2014
Sergio0694 Apr 24, 2026
35dbacc
Add tests for ApiContractTypeRequiresContractVersionAnalyzer
Sergio0694 Apr 24, 2026
6f188e1
Add CSWINRT2015 diagnostic descriptor for public types missing 'Contr…
Sergio0694 Apr 24, 2026
4d9d3f6
Add PublicTypeRequiresContractVersionAnalyzer for CSWINRT2015
Sergio0694 Apr 24, 2026
fdfb225
Add tests for PublicTypeRequiresContractVersionAnalyzer
Sergio0694 Apr 24, 2026
61a4562
Add ISymbol.GetAttributes(INamedTypeSymbol) extension and use it in a…
Sergio0694 Apr 24, 2026
f951cc3
Use pattern matching for symbol extraction
Sergio0694 Apr 24, 2026
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 @@ -15,4 +15,10 @@ CSWINRT2005 | WindowsRuntime.SourceGenerator | Error | Null indexer type in '[Ge
CSWINRT2006 | WindowsRuntime.SourceGenerator | Error | Property name not found for '[GeneratedCustomPropertyProvider]'
CSWINRT2007 | WindowsRuntime.SourceGenerator | Error | Indexer type not found for '[GeneratedCustomPropertyProvider]'
CSWINRT2008 | WindowsRuntime.SourceGenerator | Error | Static indexer for '[GeneratedCustomPropertyProvider]'
CSWINRT2009 | WindowsRuntime.SourceGenerator | Warning | Cast to '[ComImport]' type not supported
CSWINRT2009 | WindowsRuntime.SourceGenerator | Warning | Cast to '[ComImport]' type not supported
CSWINRT2010 | WindowsRuntime.SourceGenerator | Warning | API contract enum type with enum cases
CSWINRT2011 | WindowsRuntime.SourceGenerator | Warning | Invalid 'ContractVersionAttribute' target for version-only constructor
CSWINRT2012 | WindowsRuntime.SourceGenerator | Warning | Invalid 'ContractVersionAttribute' target for contract-type constructor
CSWINRT2013 | WindowsRuntime.SourceGenerator | Warning | Invalid 'ContractVersionAttribute' contract type argument
CSWINRT2014 | WindowsRuntime.SourceGenerator | Warning | API contract type missing 'ContractVersionAttribute'
CSWINRT2015 | WindowsRuntime.SourceGenerator | Info | Public authored type missing 'ContractVersionAttribute'
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)
Comment thread
Sergio0694 marked this conversation as resolved.
{
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(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

and I believe we default version attribute if not specified.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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:

  • Having an analyzer (info level) that fires if a public type has neither a version nor a contract version
    • I know we add a version by default, this is why the level would just be info, not a warning
  • Having an analyzer that warns if there's a public type without a contract version, only if there's at least another public type with a contract version. As in: if you do use contract versioning, you should be consistent and not mix.
  • All other analyzers are always on, since they're about general correctness things.

This way there's also no property to worry about, things just adapt automatically to your code.
Thoughts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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);
}
}
Loading