From 0c33285374c1314ea0204ad3d1291632eda1cad1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 11:38:18 -0700 Subject: [PATCH 01/25] Add CSWINRT2010 diagnostic descriptor for invalid '[ApiContract]' enum cases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AnalyzerReleases.Shipped.md | 3 ++- .../Diagnostics/DiagnosticDescriptors.cs | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md index 9d60f534d9..f9f25741d5 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md @@ -15,4 +15,5 @@ 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 \ No newline at end of file +CSWINRT2009 | WindowsRuntime.SourceGenerator | Warning | Cast to '[ComImport]' type not supported +CSWINRT2010 | WindowsRuntime.SourceGenerator | Warning | API contract enum type with enum cases \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs index 112d8156c8..da499c7aa3 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs @@ -139,4 +139,17 @@ internal static partial class DiagnosticDescriptors isEnabledByDefault: true, description: "Types used in cast operations must not be '[ComImport]' interfaces, as they are not compatible with Windows Runtime objects marshalled by CsWinRT.", helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for an [ApiContract] enum type that defines enum cases. + /// + public static readonly DiagnosticDescriptor ApiContractEnumWithCases = new( + id: "CSWINRT2010", + title: "API contract enum type with enum cases", + messageFormat: """The type '{0}' is annotated with '[ApiContract]', but it defines one or more enum cases. API contract types are represented by empty struct types in the Windows Runtime type system, and as such defining any enum cases is invalid. The enum cases will be ignored when generating the resulting .winmd file.""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Enum types annotated with '[ApiContract]' must not define any enum cases, as API contract types are represented by empty struct types in the Windows Runtime type system. Any enum cases will be ignored when generating the resulting .winmd file.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); } \ No newline at end of file From a4f22d00ed50290a5dca628b355b4d5661127c62 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 11:38:53 -0700 Subject: [PATCH 02/25] Add ValidApiContractEnumTypeAnalyzer for CSWINRT2010 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ValidApiContractEnumTypeAnalyzer.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidApiContractEnumTypeAnalyzer.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidApiContractEnumTypeAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidApiContractEnumTypeAnalyzer.cs new file mode 100644 index 0000000000..dfb499a28e --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidApiContractEnumTypeAnalyzer.cs @@ -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; + +/// +/// A diagnostic analyzer that validates that [ApiContract] enum types do not define any enum cases. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ValidApiContractEnumTypeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [DiagnosticDescriptors.ApiContractEnumWithCases]; + + /// + 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); + }); + } +} From 183113293a0aba3b705262383cdebbf939610d37 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 11:42:31 -0700 Subject: [PATCH 03/25] Add tests for ValidApiContractEnumTypeAnalyzer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Helpers/CSharpAnalyzerTest{TAnalyzer}.cs | 14 ++- .../Test_ValidApiContractEnumTypeAnalyzer.cs | 94 +++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/Tests/SourceGenerator2Test/Test_ValidApiContractEnumTypeAnalyzer.cs diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs index d1976d465e..ad67904e34 100644 --- a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs +++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs @@ -59,11 +59,13 @@ protected override ParseOptions CreateParseOptions() /// The list of expected diagnostic for the test (used as alternative to the markdown syntax). /// Whether to enable unsafe blocks. /// The language version to use to run the test. + /// Whether to set the "CsWinRTComponent" MSBuild property to . public static Task VerifyAnalyzerAsync( string source, ReadOnlySpan expectedDiagnostics = default, bool allowUnsafeBlocks = true, - LanguageVersion languageVersion = LanguageVersion.CSharp14) + LanguageVersion languageVersion = LanguageVersion.CSharp14, + bool isCsWinRTComponent = false) { CSharpAnalyzerTest test = new(allowUnsafeBlocks, languageVersion) { TestCode = source }; @@ -73,6 +75,16 @@ public static Task VerifyAnalyzerAsync( test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Button).Assembly.Location)); test.TestState.ExpectedDiagnostics.AddRange([.. expectedDiagnostics]); + // Configure the desired MSBuild properties via a global analyzer config file + if (isCsWinRTComponent) + { + test.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", """ + is_global = true + + build_property.CsWinRTComponent = true + """)); + } + return test.RunAsync(CancellationToken.None); } } \ No newline at end of file diff --git a/src/Tests/SourceGenerator2Test/Test_ValidApiContractEnumTypeAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_ValidApiContractEnumTypeAnalyzer.cs new file mode 100644 index 0000000000..35f2e98c48 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_ValidApiContractEnumTypeAnalyzer.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using WindowsRuntime.SourceGenerator.Diagnostics; +using WindowsRuntime.SourceGenerator.Tests.Helpers; + +namespace WindowsRuntime.SourceGenerator.Tests; + +using VerifyCS = CSharpAnalyzerTest; + +/// +/// Tests for . +/// +[TestClass] +public sealed class Test_ValidApiContractEnumTypeAnalyzer +{ + [TestMethod] + public async Task EmptyApiContractEnum_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + public enum MyContract; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task EnumWithCases_NoApiContract_DoesNotWarn() + { + const string source = """ + public enum MyEnum + { + A, + B, + C + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task ApiContractEnumWithCases_NotComponent_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + public enum MyContract + { + A + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task ApiContractEnumWithCases_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + public enum {|CSWINRT2010:MyContract|} + { + A, + B + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task ApiContractEnumWithSingleCase_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + public enum {|CSWINRT2010:MyContract|} + { + Version1 + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } +} From d80421759569a12e4cb63ca4cca036e868c935ae Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 11:49:52 -0700 Subject: [PATCH 04/25] Add CSWINRT2011-CSWINRT2013 diagnostic descriptors for invalid '[ContractVersion]' usage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AnalyzerReleases.Shipped.md | 5 ++- .../Diagnostics/DiagnosticDescriptors.cs | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md index f9f25741d5..e095770cda 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md @@ -16,4 +16,7 @@ CSWINRT2006 | WindowsRuntime.SourceGenerator | Error | Property name not found f 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 -CSWINRT2010 | WindowsRuntime.SourceGenerator | Warning | API contract enum type with enum cases \ No newline at end of file +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 \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs index da499c7aa3..75c0638158 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs @@ -152,4 +152,43 @@ internal static partial class DiagnosticDescriptors isEnabledByDefault: true, description: "Enum types annotated with '[ApiContract]' must not define any enum cases, as API contract types are represented by empty struct types in the Windows Runtime type system. Any enum cases will be ignored when generating the resulting .winmd file.", helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for a [ContractVersion] attribute using the version-only constructors on a non-API contract type. + /// + public static readonly DiagnosticDescriptor ContractVersionAttributeRequiresApiContractTarget = new( + id: "CSWINRT2011", + title: "Invalid 'ContractVersionAttribute' target for version-only constructor", + messageFormat: """The type '{0}' is annotated with '[ContractVersion]' using a constructor that only specifies the contract version, but '{0}' is not an API contract type (an enum type annotated with '[ApiContract]'). These constructors only apply to API contract types and are used to specify the contract version of that API contract.""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "The 'ContractVersionAttribute' constructors taking only the contract version (or the contract name and version) only apply to API contract types (enum types annotated with '[ApiContract]'), and are used to specify the contract version of that API contract.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for a [ContractVersion] attribute using the contract-type constructor on an API contract type. + /// + public static readonly DiagnosticDescriptor ContractVersionAttributeNotAllowedOnApiContractTarget = new( + id: "CSWINRT2012", + title: "Invalid 'ContractVersionAttribute' target for contract-type constructor", + messageFormat: """The type '{0}' is annotated with '[ContractVersion]' using the constructor that takes a contract type and version, but '{0}' is itself an API contract type. This constructor is used to associate a non-contract type with an API contract; use the constructor that only takes the contract version instead.""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "The 'ContractVersionAttribute' constructor taking a contract type and version cannot be applied to API contract types, as it is meant to associate a non-contract type with an API contract.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for a [ContractVersion] attribute whose contract type argument is not a valid API contract type. + /// + public static readonly DiagnosticDescriptor ContractVersionAttributeInvalidContractTypeArgument = new( + id: "CSWINRT2013", + title: "Invalid 'ContractVersionAttribute' contract type argument", + messageFormat: """The 'ContractVersionAttribute' applied to '{0}' specifies '{1}' as the contract type, but '{1}' is not a valid API contract type (an enum type annotated with '[ApiContract]')""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "The contract type argument of '[ContractVersion]' must be a valid API contract type (an enum type annotated with '[ApiContract]').", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); } \ No newline at end of file From 807bb569591cd26b5f5a22d50cfb7cff80805381 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 11:50:50 -0700 Subject: [PATCH 05/25] Add ValidContractVersionAttributeAnalyzer for CSWINRT2011-CSWINRT2013 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ValidContractVersionAttributeAnalyzer.cs | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs new file mode 100644 index 0000000000..a219dd032c --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace WindowsRuntime.SourceGenerator.Diagnostics; + +/// +/// A diagnostic analyzer that validates applications of [ContractVersion] attributes. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ValidContractVersionAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [ + DiagnosticDescriptors.ContractVersionAttributeRequiresApiContractTarget, + DiagnosticDescriptors.ContractVersionAttributeNotAllowedOnApiContractTarget, + DiagnosticDescriptors.ContractVersionAttributeInvalidContractTypeArgument]; + + /// + 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()) + { + if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, contractVersionAttributeType)) + { + continue; + } + + if (attribute.AttributeConstructor is not { } constructor) + { + continue; + } + + ImmutableArray 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" } }, _]; + + if (isVersionOnlyConstructor) + { + // The version-only constructors must be applied to API contract types + if (!isApiContractType) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.ContractVersionAttributeRequiresApiContractTarget, + GetAttributeLocation(attribute, 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, + GetAttributeLocation(attribute, 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, + GetAttributeArgumentLocation(attribute, argumentIndex: 0, context.CancellationToken) + ?? GetAttributeLocation(attribute, context.CancellationToken) + ?? typeSymbol.Locations.FirstOrDefault(), + typeSymbol, + contractTypeArgument)); + } + } + } + }, SymbolKind.NamedType); + }); + } + + /// + /// Checks whether a type is a valid API contract type (an enum type annotated with [ApiContract]). + /// + /// The type symbol to check. + /// The [ApiContract] attribute symbol. + /// Whether is a valid API contract type. + private static bool IsApiContractType(INamedTypeSymbol typeSymbol, INamedTypeSymbol apiContractAttributeType) + { + return typeSymbol is { TypeKind: TypeKind.Enum } && typeSymbol.HasAttributeWithType(apiContractAttributeType); + } + + /// + /// Gets the location of the syntax node where an attribute is applied. + /// + /// The attribute to locate. + /// The cancellation token to use. + /// The location of the attribute application, or if it cannot be determined. + private static Location? GetAttributeLocation(AttributeData attribute, CancellationToken cancellationToken) + { + return attribute.ApplicationSyntaxReference?.GetSyntax(cancellationToken).GetLocation(); + } + + /// + /// Gets the location of a specific positional argument of an attribute application. + /// + /// The attribute to locate. + /// The index of the positional argument. + /// The cancellation token to use. + /// The location of the argument, or if it cannot be determined. + private static Location? GetAttributeArgumentLocation(AttributeData attribute, int argumentIndex, CancellationToken cancellationToken) + { + return attribute.ApplicationSyntaxReference?.GetSyntax(cancellationToken) is AttributeSyntax { ArgumentList.Arguments: { } arguments } && argumentIndex < arguments.Count + ? arguments[argumentIndex].GetLocation() + : null; + } +} From 7f7403fdd27e4cafd2eb4deb723bed64c003d219 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 11:51:16 -0700 Subject: [PATCH 06/25] Add tests for ValidContractVersionAttributeAnalyzer Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- ...t_ValidContractVersionAttributeAnalyzer.cs | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 src/Tests/SourceGenerator2Test/Test_ValidContractVersionAttributeAnalyzer.cs diff --git a/src/Tests/SourceGenerator2Test/Test_ValidContractVersionAttributeAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_ValidContractVersionAttributeAnalyzer.cs new file mode 100644 index 0000000000..9c019179b6 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_ValidContractVersionAttributeAnalyzer.cs @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using WindowsRuntime.SourceGenerator.Diagnostics; +using WindowsRuntime.SourceGenerator.Tests.Helpers; + +namespace WindowsRuntime.SourceGenerator.Tests; + +using VerifyCS = CSharpAnalyzerTest; + +/// +/// Tests for . +/// +[TestClass] +public sealed class Test_ValidContractVersionAttributeAnalyzer +{ + [TestMethod] + public async Task VersionOnlyConstructor_OnApiContractType_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task ContractNameAndVersionConstructor_OnApiContractType_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion("MyContractName", 1u)] + public enum MyContract; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task ContractTypeConstructor_WithValidContract_OnNonContractType_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + public class MyType; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task InvalidUsage_NotComponent_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + public enum MyContract; + + [ContractVersion(1u)] + public class NotAContract; + + [ContractVersion(typeof(MyContract), 1u)] + [ApiContract] + public enum AlsoAContract; + + [ContractVersion(typeof(NotAContract), 1u)] + public class TypeWithBadContract; + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task VersionOnlyConstructor_OnNonContractType_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + [{|CSWINRT2011:ContractVersion(1u)|}] + public class MyType; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task VersionOnlyConstructor_OnEnumWithoutApiContract_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + [{|CSWINRT2011:ContractVersion(1u)|}] + public enum MyEnum; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task ContractNameAndVersionConstructor_OnNonContractType_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + [{|CSWINRT2011:ContractVersion("MyContractName", 1u)|}] + public class MyType; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task ContractTypeConstructor_OnApiContractType_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + public enum OtherContract; + + [ApiContract] + [{|CSWINRT2012:ContractVersion(typeof(OtherContract), 1u)|}] + public enum MyContract; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task ContractTypeConstructor_WithNonContractTypeArgument_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + public class NotAContract; + + [ContractVersion({|CSWINRT2013:typeof(NotAContract)|}, 1u)] + public class MyType; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task ContractTypeConstructor_WithEnumWithoutApiContractArgument_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + public enum NotAContract; + + [ContractVersion({|CSWINRT2013:typeof(NotAContract)|}, 1u)] + public class MyType; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task ContractTypeConstructor_OnApiContractType_WithNonContractTypeArgument_WarnsBoth() + { + const string source = """ + using Windows.Foundation.Metadata; + + public class NotAContract; + + [ApiContract] + [{|CSWINRT2012:ContractVersion({|CSWINRT2013:typeof(NotAContract)|}, 1u)|}] + public enum MyContract; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } +} From aaa93a8514b67678f8d079851f82b0f4b89985c3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 11:55:37 -0700 Subject: [PATCH 07/25] Update ContractVersionAttribute API and docs Replace AttributeTargets.All with an explicit list of allowed targets and reformat AttributeUsage for clarity. Swap the Type/string constructor signatures and add XML to each constructor to clarify when each overload applies (API-contract vs non-API-contract types). Note: the constructor signature order changed, which can be a breaking change for callers and may require code updates. --- .../Metadata/ContractVersionAttribute.cs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/WinRT.Runtime2/Windows.Foundation/Metadata/ContractVersionAttribute.cs b/src/WinRT.Runtime2/Windows.Foundation/Metadata/ContractVersionAttribute.cs index 49f0db22da..0566bfca1e 100644 --- a/src/WinRT.Runtime2/Windows.Foundation/Metadata/ContractVersionAttribute.cs +++ b/src/WinRT.Runtime2/Windows.Foundation/Metadata/ContractVersionAttribute.cs @@ -13,7 +13,16 @@ namespace Windows.Foundation.Metadata; /// Indicates the version of the API contract. /// [WindowsRuntimeMetadata("Windows.Foundation.FoundationContract")] -[AttributeUsage(AttributeTargets.All, AllowMultiple = true)] +[AttributeUsage( + AttributeTargets.Delegate | + AttributeTargets.Enum | + AttributeTargets.Event | + AttributeTargets.Field | + AttributeTargets.Interface | + AttributeTargets.Method | + AttributeTargets.Property | + AttributeTargets.Class | + AttributeTargets.Struct, AllowMultiple = true)] [SupportedOSPlatform("Windows10.0.10240.0")] [ContractVersion(typeof(FoundationContract), 65536u)] public sealed class ContractVersionAttribute : Attribute @@ -22,6 +31,9 @@ public sealed class ContractVersionAttribute : Attribute /// Creates a new instance with the specified parameters. /// /// The version of the API contract. + /// + /// This constructor applies to a type with the and specifies the contract version of that API contract. + /// public ContractVersionAttribute(uint version) { } @@ -31,7 +43,10 @@ public ContractVersionAttribute(uint version) /// /// The type to associate with the API contract. /// The version of the API contract. - public ContractVersionAttribute(Type contract, uint version) + /// + /// This constructor applies to a type with the and specifies the contract version of that API contract. + /// + public ContractVersionAttribute(string contract, uint version) { } @@ -40,7 +55,11 @@ public ContractVersionAttribute(Type contract, uint version) /// /// The type to associate with the API contract. /// The version of the API contract. - public ContractVersionAttribute(string contract, uint version) + /// + /// This constructor applies to any type that does not have the and + /// indicates the API contract version in which this type was added to the specified API contract. + /// + public ContractVersionAttribute(Type contract, uint version) { } } \ No newline at end of file From 28f4e4caa629c3dd4e6ae2ba973fb5439f73c0e4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 11:58:27 -0700 Subject: [PATCH 08/25] Validate multiple ContractVersion attributes Iterate and validate each [ContractVersion] attribute instance and report diagnostics per-attribute. Simplifies and corrects the condition for version-only constructors by combining checks (isVersionOnlyConstructor && !isApiContractType) so the diagnostic is raised for each attribute applied to non-API contract types. Also updates minor comment wording. --- .../ValidContractVersionAttributeAnalyzer.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs index a219dd032c..e77099e781 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs @@ -59,6 +59,7 @@ public override void Initialize(AnalysisContext context) foreach (AttributeData attribute in typeSymbol.GetAttributes()) { + // We can have multiple '[ContractVersion]' uses, so we need to iterate and check each of them if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, contractVersionAttributeType)) { continue; @@ -82,20 +83,17 @@ public override void Initialize(AnalysisContext context) bool isContractTypeConstructor = parameters is [{ Type: INamedTypeSymbol { MetadataName: "Type", ContainingNamespace.Name: "System" } }, _]; - if (isVersionOnlyConstructor) + // The version-only constructors must be applied to API contract types + if (isVersionOnlyConstructor && !isApiContractType) { - // The version-only constructors must be applied to API contract types - if (!isApiContractType) - { - context.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.ContractVersionAttributeRequiresApiContractTarget, - GetAttributeLocation(attribute, context.CancellationToken) ?? typeSymbol.Locations.FirstOrDefault(), - typeSymbol)); - } + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.ContractVersionAttributeRequiresApiContractTarget, + GetAttributeLocation(attribute, context.CancellationToken) ?? typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); } else if (isContractTypeConstructor) { - // The contract-type constructor must NOT be applied to API contract types + // The contract-type constructor must not be applied to API contract types if (isApiContractType) { context.ReportDiagnostic(Diagnostic.Create( From 8b1e26b43b214006f4a1c544d1ee568e99bc2ec1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 12:00:28 -0700 Subject: [PATCH 09/25] Move AttributeData location helpers to AttributeDataExtensions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ValidContractVersionAttributeAnalyzer.cs | 35 ++------------- .../Extensions/AttributeDataExtensions.cs | 43 +++++++++++++++++++ 2 files changed, 47 insertions(+), 31 deletions(-) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Extensions/AttributeDataExtensions.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs index e77099e781..4b3dcde18a 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs @@ -3,9 +3,7 @@ using System.Collections.Immutable; using System.Linq; -using System.Threading; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; namespace WindowsRuntime.SourceGenerator.Diagnostics; @@ -88,7 +86,7 @@ public override void Initialize(AnalysisContext context) { context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.ContractVersionAttributeRequiresApiContractTarget, - GetAttributeLocation(attribute, context.CancellationToken) ?? typeSymbol.Locations.FirstOrDefault(), + attribute.GetLocation(context.CancellationToken) ?? typeSymbol.Locations.FirstOrDefault(), typeSymbol)); } else if (isContractTypeConstructor) @@ -98,7 +96,7 @@ public override void Initialize(AnalysisContext context) { context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.ContractVersionAttributeNotAllowedOnApiContractTarget, - GetAttributeLocation(attribute, context.CancellationToken) ?? typeSymbol.Locations.FirstOrDefault(), + attribute.GetLocation(context.CancellationToken) ?? typeSymbol.Locations.FirstOrDefault(), typeSymbol)); } @@ -108,8 +106,8 @@ public override void Initialize(AnalysisContext context) { context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.ContractVersionAttributeInvalidContractTypeArgument, - GetAttributeArgumentLocation(attribute, argumentIndex: 0, context.CancellationToken) - ?? GetAttributeLocation(attribute, context.CancellationToken) + attribute.GetArgumentLocation(argumentIndex: 0, context.CancellationToken) + ?? attribute.GetLocation(context.CancellationToken) ?? typeSymbol.Locations.FirstOrDefault(), typeSymbol, contractTypeArgument)); @@ -130,29 +128,4 @@ private static bool IsApiContractType(INamedTypeSymbol typeSymbol, INamedTypeSym { return typeSymbol is { TypeKind: TypeKind.Enum } && typeSymbol.HasAttributeWithType(apiContractAttributeType); } - - /// - /// Gets the location of the syntax node where an attribute is applied. - /// - /// The attribute to locate. - /// The cancellation token to use. - /// The location of the attribute application, or if it cannot be determined. - private static Location? GetAttributeLocation(AttributeData attribute, CancellationToken cancellationToken) - { - return attribute.ApplicationSyntaxReference?.GetSyntax(cancellationToken).GetLocation(); - } - - /// - /// Gets the location of a specific positional argument of an attribute application. - /// - /// The attribute to locate. - /// The index of the positional argument. - /// The cancellation token to use. - /// The location of the argument, or if it cannot be determined. - private static Location? GetAttributeArgumentLocation(AttributeData attribute, int argumentIndex, CancellationToken cancellationToken) - { - return attribute.ApplicationSyntaxReference?.GetSyntax(cancellationToken) is AttributeSyntax { ArgumentList.Arguments: { } arguments } && argumentIndex < arguments.Count - ? arguments[argumentIndex].GetLocation() - : null; - } } diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/AttributeDataExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/AttributeDataExtensions.cs new file mode 100644 index 0000000000..2697f70d32 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/AttributeDataExtensions.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +#pragma warning disable CS1734 + +namespace WindowsRuntime.SourceGenerator; + +/// +/// Extensions for . +/// +internal static class AttributeDataExtensions +{ + /// The input instance. + extension(AttributeData attribute) + { + /// + /// Gets the location of the syntax node where the attribute is applied. + /// + /// The cancellation token to use. + /// The location of the attribute application, or if it cannot be determined. + public Location? GetLocation(CancellationToken cancellationToken) + { + return attribute.ApplicationSyntaxReference?.GetSyntax(cancellationToken).GetLocation(); + } + + /// + /// Gets the location of a specific positional argument of the attribute application. + /// + /// The index of the positional argument. + /// The cancellation token to use. + /// The location of the argument, or if it cannot be determined. + public Location? GetArgumentLocation(int argumentIndex, CancellationToken cancellationToken) + { + return attribute.ApplicationSyntaxReference?.GetSyntax(cancellationToken) is AttributeSyntax { ArgumentList.Arguments: { } arguments } && argumentIndex < arguments.Count + ? arguments[argumentIndex].GetLocation() + : null; + } + } +} From 7ae7ac7e2d2ba91ba41b2fd317a4ca9753224aad Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 12:46:48 -0700 Subject: [PATCH 10/25] Add CSWINRT2014 diagnostic descriptor for API contract types missing 'ContractVersionAttribute' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AnalyzerReleases.Shipped.md | 3 ++- .../Diagnostics/DiagnosticDescriptors.cs | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md index e095770cda..96de01904b 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md @@ -19,4 +19,5 @@ CSWINRT2009 | WindowsRuntime.SourceGenerator | Warning | Cast to '[ComImport]' t 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 \ No newline at end of file +CSWINRT2013 | WindowsRuntime.SourceGenerator | Warning | Invalid 'ContractVersionAttribute' contract type argument +CSWINRT2014 | WindowsRuntime.SourceGenerator | Warning | API contract type missing 'ContractVersionAttribute' \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs index 75c0638158..4e59e836fc 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs @@ -191,4 +191,17 @@ internal static partial class DiagnosticDescriptors isEnabledByDefault: true, description: "The contract type argument of '[ContractVersion]' must be a valid API contract type (an enum type annotated with '[ApiContract]').", helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for an [ApiContract] enum type that is missing a [ContractVersion] attribute. + /// + public static readonly DiagnosticDescriptor ApiContractTypeMissingContractVersion = new( + id: "CSWINRT2014", + title: "API contract type missing 'ContractVersionAttribute'", + messageFormat: """The type '{0}' is annotated with '[ApiContract]', but it does not have a '[ContractVersion]' attribute applied to it. API contract types must declare their contract version using one of the version-only constructors of '[ContractVersion]'.""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Enum types annotated with '[ApiContract]' must also have a '[ContractVersion]' attribute applied to them, using one of the version-only constructors, to declare the contract version of the API contract.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); } \ No newline at end of file From 03acfe6a1e0a61f7dad68f73d2b3c5bd6a7e1757 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 12:47:18 -0700 Subject: [PATCH 11/25] Add ApiContractTypeRequiresContractVersionAnalyzer for CSWINRT2014 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ractTypeRequiresContractVersionAnalyzer.cs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ApiContractTypeRequiresContractVersionAnalyzer.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ApiContractTypeRequiresContractVersionAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ApiContractTypeRequiresContractVersionAnalyzer.cs new file mode 100644 index 0000000000..3b673ffd19 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ApiContractTypeRequiresContractVersionAnalyzer.cs @@ -0,0 +1,83 @@ +// 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; + +/// +/// A diagnostic analyzer that validates that [ApiContract] enum types declare their contract version using [ContractVersion]. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ApiContractTypeRequiresContractVersionAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [DiagnosticDescriptors.ApiContractTypeMissingContractVersion]; + + /// + 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()) + { + if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, contractVersionAttributeType)) + { + continue; + } + + // 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); + }); + } +} From e394ef03958c2d9484275a2c2eacb6884c27afd6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 12:47:37 -0700 Subject: [PATCH 12/25] Add tests for ApiContractTypeRequiresContractVersionAnalyzer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ractTypeRequiresContractVersionAnalyzer.cs | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/Tests/SourceGenerator2Test/Test_ApiContractTypeRequiresContractVersionAnalyzer.cs diff --git a/src/Tests/SourceGenerator2Test/Test_ApiContractTypeRequiresContractVersionAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_ApiContractTypeRequiresContractVersionAnalyzer.cs new file mode 100644 index 0000000000..e8f5873173 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_ApiContractTypeRequiresContractVersionAnalyzer.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using WindowsRuntime.SourceGenerator.Diagnostics; +using WindowsRuntime.SourceGenerator.Tests.Helpers; + +namespace WindowsRuntime.SourceGenerator.Tests; + +using VerifyCS = CSharpAnalyzerTest; + +/// +/// Tests for . +/// +[TestClass] +public sealed class Test_ApiContractTypeRequiresContractVersionAnalyzer +{ + [TestMethod] + public async Task ApiContractEnum_WithVersionOnlyConstructor_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task ApiContractEnum_WithNameAndVersionConstructor_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion("OtherContract", 1u)] + public enum MyContract; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task NonApiContractEnum_DoesNotWarn() + { + const string source = """ + public enum MyEnum + { + A, + B + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task ApiContractEnum_NotComponent_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + public enum MyContract; + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task ApiContractEnum_NoContractVersion_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + public enum {|CSWINRT2014:MyContract|}; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task ApiContractEnum_OnlyContractTypeConstructor_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(typeof(MyContract), 1u)] + public enum {|CSWINRT2014:MyContract|}; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } +} From 6414beb7d16b5ed14c7eddc7406e0513dd1cbd6c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 12:50:29 -0700 Subject: [PATCH 13/25] Add CSWINRT2015 diagnostic descriptor for public types missing 'ContractVersionAttribute' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AnalyzerReleases.Shipped.md | 3 ++- .../Diagnostics/DiagnosticDescriptors.cs | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md index 96de01904b..3d9daec083 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md @@ -20,4 +20,5 @@ CSWINRT2010 | WindowsRuntime.SourceGenerator | Warning | API contract enum type 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' \ No newline at end of file +CSWINRT2014 | WindowsRuntime.SourceGenerator | Warning | API contract type missing 'ContractVersionAttribute' +CSWINRT2015 | WindowsRuntime.SourceGenerator | Info | Public authored type missing 'ContractVersionAttribute' \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs index 4e59e836fc..b5ef97f964 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs @@ -204,4 +204,17 @@ internal static partial class DiagnosticDescriptors isEnabledByDefault: true, description: "Enum types annotated with '[ApiContract]' must also have a '[ContractVersion]' attribute applied to them, using one of the version-only constructors, to declare the contract version of the API contract.", helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for a public authored type missing a [ContractVersion] attribute. + /// + public static readonly DiagnosticDescriptor PublicTypeMissingContractVersion = new( + id: "CSWINRT2015", + title: "Public authored type missing 'ContractVersionAttribute'", + messageFormat: """The type '{0}' is publicly exposed in a Windows Runtime component, but it does not have a '[ContractVersion]' attribute applied to it. Public types should declare their associated API contract using '[ContractVersion(typeof(SomeContract), version)]' so that consumers can target a specific contract version.""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Public types in a Windows Runtime component should declare their associated API contract using '[ContractVersion(typeof(SomeContract), version)]', so that consumers can target a specific contract version.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); } \ No newline at end of file From 0840217e1c91f94bda38970d6a9e3544506bf7f4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 12:51:04 -0700 Subject: [PATCH 14/25] Add PublicTypeRequiresContractVersionAnalyzer for CSWINRT2015 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...blicTypeRequiresContractVersionAnalyzer.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs new file mode 100644 index 0000000000..efdacc8c49 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs @@ -0,0 +1,75 @@ +// 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; + +/// +/// A diagnostic analyzer that reports when a public authored type is missing a [ContractVersion] attribute. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PublicTypeRequiresContractVersionAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [DiagnosticDescriptors.PublicTypeMissingContractVersion]; + + /// + 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 => + { + INamedTypeSymbol typeSymbol = (INamedTypeSymbol)context.Symbol; + + // Only consider top-level public types + if (typeSymbol is not { DeclaredAccessibility: Accessibility.Public, ContainingType: null }) + { + 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( + DiagnosticDescriptors.PublicTypeMissingContractVersion, + typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); + }, SymbolKind.NamedType); + }); + } +} From e0e10162094f4d766e0ebcefcbc82b8d65c90753 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 12:51:23 -0700 Subject: [PATCH 15/25] Add tests for PublicTypeRequiresContractVersionAnalyzer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...blicTypeRequiresContractVersionAnalyzer.cs | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 src/Tests/SourceGenerator2Test/Test_PublicTypeRequiresContractVersionAnalyzer.cs diff --git a/src/Tests/SourceGenerator2Test/Test_PublicTypeRequiresContractVersionAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_PublicTypeRequiresContractVersionAnalyzer.cs new file mode 100644 index 0000000000..37893d6012 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_PublicTypeRequiresContractVersionAnalyzer.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using WindowsRuntime.SourceGenerator.Diagnostics; +using WindowsRuntime.SourceGenerator.Tests.Helpers; + +namespace WindowsRuntime.SourceGenerator.Tests; + +using VerifyCS = CSharpAnalyzerTest; + +/// +/// Tests for . +/// +[TestClass] +public sealed class Test_PublicTypeRequiresContractVersionAnalyzer +{ + [TestMethod] + public async Task PublicClass_WithContractVersion_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task ApiContractEnum_WithoutContractVersion_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + public enum MyContract; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task InternalClass_WithoutContractVersion_DoesNotWarn() + { + const string source = """ + internal sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task NestedPublicClass_WithoutContractVersion_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + public sealed class Outer + { + public sealed class Nested; + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicClass_NotComponent_DoesNotWarn() + { + const string source = """ + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task PublicClass_WithoutContractVersion_Warns() + { + const string source = """ + public sealed class {|CSWINRT2015:MyClass|}; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicInterface_WithoutContractVersion_Warns() + { + const string source = """ + public interface {|CSWINRT2015:IMyInterface|}; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicStruct_WithoutContractVersion_Warns() + { + const string source = """ + public struct {|CSWINRT2015:MyStruct|}; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicEnum_WithoutContractVersion_Warns() + { + const string source = """ + public enum {|CSWINRT2015:MyEnum|} + { + A, + B + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicDelegate_WithoutContractVersion_Warns() + { + const string source = """ + public delegate void {|CSWINRT2015:MyDelegate|}(); + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } +} From 5d12d9466098b29b20d2ec90674437f42ab697aa Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 12:54:17 -0700 Subject: [PATCH 16/25] Add ISymbol.GetAttributes(INamedTypeSymbol) extension and use it in analyzers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ontractTypeRequiresContractVersionAnalyzer.cs | 7 +------ .../ValidContractVersionAttributeAnalyzer.cs | 7 +------ .../Extensions/ISymbolExtensions.cs | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ApiContractTypeRequiresContractVersionAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ApiContractTypeRequiresContractVersionAnalyzer.cs index 3b673ffd19..2858761dd2 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ApiContractTypeRequiresContractVersionAnalyzer.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ApiContractTypeRequiresContractVersionAnalyzer.cs @@ -58,13 +58,8 @@ public override void Initialize(AnalysisContext context) } // Check whether any '[ContractVersion]' attribute using a version-only constructor is applied - foreach (AttributeData attribute in typeSymbol.GetAttributes()) + foreach (AttributeData attribute in typeSymbol.GetAttributes(contractVersionAttributeType)) { - if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, contractVersionAttributeType)) - { - continue; - } - // 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 }, ..]) diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs index 4b3dcde18a..a7ebc27429 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ValidContractVersionAttributeAnalyzer.cs @@ -55,14 +55,9 @@ public override void Initialize(AnalysisContext context) bool isApiContractType = IsApiContractType(typeSymbol, apiContractAttributeType); - foreach (AttributeData attribute in typeSymbol.GetAttributes()) + foreach (AttributeData attribute in typeSymbol.GetAttributes(contractVersionAttributeType)) { // We can have multiple '[ContractVersion]' uses, so we need to iterate and check each of them - if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, contractVersionAttributeType)) - { - continue; - } - if (attribute.AttributeConstructor is not { } constructor) { continue; diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs index 59441b65cb..3bc2402c79 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs @@ -68,6 +68,22 @@ public bool TryGetAttributeWithType(ITypeSymbol typeSymbol, [NotNullWhen(true)] return false; } + /// + /// Gets all attributes applied to a symbol with a specified type. + /// + /// The instance for the attribute type to look for. + /// The sequence of attributes applied to with the specified type. + public IEnumerable GetAttributes(INamedTypeSymbol typeSymbol) + { + foreach (AttributeData attribute in symbol.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, typeSymbol)) + { + yield return attribute; + } + } + } + /// /// Checks whether a given symbol is accessible from the assembly of a given compilation (including eg. through nested types). /// From 31e94e52f4a6d479b1f3b5698fd7e2d903f7fc56 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 24 Apr 2026 13:31:54 -0700 Subject: [PATCH 17/25] Use pattern matching for symbol extraction Refactor PublicTypeRequiresContractVersionAnalyzer to combine the cast and accessibility/containing-type checks into a single pattern-matching expression on context.Symbol. Removes a separate INamedTypeSymbol assignment and simplifies the early-return check without changing behavior. --- .../Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs index efdacc8c49..7a8a10aafd 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs @@ -42,10 +42,8 @@ public override void Initialize(AnalysisContext context) context.RegisterSymbolAction(context => { - INamedTypeSymbol typeSymbol = (INamedTypeSymbol)context.Symbol; - // Only consider top-level public types - if (typeSymbol is not { DeclaredAccessibility: Accessibility.Public, ContainingType: null }) + if (context.Symbol is not INamedTypeSymbol { DeclaredAccessibility: Accessibility.Public, ContainingType: null } typeSymbol) { return; } From 2fd17d7fd6bf9aea04f1c4dcd2bd164e87601aa0 Mon Sep 17 00:00:00 2001 From: Sergio0694 Date: Wed, 6 May 2026 13:36:54 -0700 Subject: [PATCH 18/25] Update CSWINRT2015 to also accept '[VersionAttribute]' The 'PublicTypeRequiresContractVersionAnalyzer' analyzer is renamed to 'PublicTypeRequiresVersioningAnalyzer' and now accepts either a '[ContractVersion]' or a '[Version]' attribute as valid version metadata on a public authored type. This matches the WinRT type system, where either attribute is sufficient to declare the version of a type. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AnalyzerReleases.Shipped.md | 2 +- ...> PublicTypeRequiresVersioningAnalyzer.cs} | 22 ++++++-- .../Diagnostics/DiagnosticDescriptors.cs | 10 ++-- ...t_PublicTypeRequiresVersioningAnalyzer.cs} | 51 +++++++++++++++---- 4 files changed, 64 insertions(+), 21 deletions(-) rename src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/{PublicTypeRequiresContractVersionAnalyzer.cs => PublicTypeRequiresVersioningAnalyzer.cs} (73%) rename src/Tests/SourceGenerator2Test/{Test_PublicTypeRequiresContractVersionAnalyzer.cs => Test_PublicTypeRequiresVersioningAnalyzer.cs} (67%) diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md index 3d9daec083..68d1f2fccd 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md @@ -21,4 +21,4 @@ CSWINRT2011 | WindowsRuntime.SourceGenerator | Warning | Invalid 'ContractVersio 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' \ No newline at end of file +CSWINRT2015 | WindowsRuntime.SourceGenerator | Info | Public authored type missing version metadata \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresVersioningAnalyzer.cs similarity index 73% rename from src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs rename to src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresVersioningAnalyzer.cs index 7a8a10aafd..a4aa04c2ab 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresVersioningAnalyzer.cs @@ -9,13 +9,14 @@ namespace WindowsRuntime.SourceGenerator.Diagnostics; /// -/// A diagnostic analyzer that reports when a public authored type is missing a [ContractVersion] attribute. +/// A diagnostic analyzer that reports when a public authored type is missing version metadata +/// (i.e. is missing both a [ContractVersion] and a [Version] attribute). /// [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class PublicTypeRequiresContractVersionAnalyzer : DiagnosticAnalyzer +public sealed class PublicTypeRequiresVersioningAnalyzer : DiagnosticAnalyzer { /// - public override ImmutableArray SupportedDiagnostics { get; } = [DiagnosticDescriptors.PublicTypeMissingContractVersion]; + public override ImmutableArray SupportedDiagnostics { get; } = [DiagnosticDescriptors.PublicTypeMissingVersioning]; /// public override void Initialize(AnalysisContext context) @@ -37,6 +38,10 @@ public override void Initialize(AnalysisContext context) return; } + // Get the '[Version]' symbol (used to also accept '[Version]' as valid version metadata, + // as an alternative to '[ContractVersion]' for declaring the version of a public type). + INamedTypeSymbol? versionAttributeType = context.Compilation.GetTypeByMetadataName("Windows.Foundation.Metadata.VersionAttribute"); + // 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"); @@ -57,14 +62,21 @@ public override void Initialize(AnalysisContext context) } // Skip if any '[ContractVersion]' attribute is already applied (validity of the - // specific constructor used is reported by the other 'ContractVersion' analyzers) + // specific constructor used is reported by the other 'ContractVersion' analyzers). if (typeSymbol.HasAttributeWithType(contractVersionAttributeType)) { return; } + // Skip if any '[Version]' attribute is applied: '[Version]' is an alternative versioning + // metadata that also satisfies the requirement of declaring a version for the type. + if (versionAttributeType is not null && typeSymbol.HasAttributeWithType(versionAttributeType)) + { + return; + } + context.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.PublicTypeMissingContractVersion, + DiagnosticDescriptors.PublicTypeMissingVersioning, typeSymbol.Locations.FirstOrDefault(), typeSymbol)); }, SymbolKind.NamedType); diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs index b5ef97f964..851419287e 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs @@ -206,15 +206,15 @@ internal static partial class DiagnosticDescriptors helpLinkUri: "https://github.com/microsoft/CsWinRT"); /// - /// Gets a for a public authored type missing a [ContractVersion] attribute. + /// Gets a for a public authored type missing version metadata (either [ContractVersion] or [Version]). /// - public static readonly DiagnosticDescriptor PublicTypeMissingContractVersion = new( + public static readonly DiagnosticDescriptor PublicTypeMissingVersioning = new( id: "CSWINRT2015", - title: "Public authored type missing 'ContractVersionAttribute'", - messageFormat: """The type '{0}' is publicly exposed in a Windows Runtime component, but it does not have a '[ContractVersion]' attribute applied to it. Public types should declare their associated API contract using '[ContractVersion(typeof(SomeContract), version)]' so that consumers can target a specific contract version.""", + title: "Public authored type missing version metadata", + messageFormat: """The type '{0}' is publicly exposed in a Windows Runtime component, but it does not have either a '[ContractVersion]' or a '[Version]' attribute applied to it. Public types should declare their associated API contract version using '[ContractVersion(typeof(SomeContract), version)]', or specify a Windows Runtime version using '[Version(version)]', so that consumers can target a specific version.""", category: "WindowsRuntime.SourceGenerator", defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, - description: "Public types in a Windows Runtime component should declare their associated API contract using '[ContractVersion(typeof(SomeContract), version)]', so that consumers can target a specific contract version.", + description: "Public types in a Windows Runtime component should declare their version using either '[ContractVersion(typeof(SomeContract), version)]' (to associate with an API contract) or '[Version(version)]' (to specify a Windows Runtime version), so that consumers can target a specific version.", helpLinkUri: "https://github.com/microsoft/CsWinRT"); } \ No newline at end of file diff --git a/src/Tests/SourceGenerator2Test/Test_PublicTypeRequiresContractVersionAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_PublicTypeRequiresVersioningAnalyzer.cs similarity index 67% rename from src/Tests/SourceGenerator2Test/Test_PublicTypeRequiresContractVersionAnalyzer.cs rename to src/Tests/SourceGenerator2Test/Test_PublicTypeRequiresVersioningAnalyzer.cs index 37893d6012..44cb1e729c 100644 --- a/src/Tests/SourceGenerator2Test/Test_PublicTypeRequiresContractVersionAnalyzer.cs +++ b/src/Tests/SourceGenerator2Test/Test_PublicTypeRequiresVersioningAnalyzer.cs @@ -7,13 +7,13 @@ namespace WindowsRuntime.SourceGenerator.Tests; -using VerifyCS = CSharpAnalyzerTest; +using VerifyCS = CSharpAnalyzerTest; /// -/// Tests for . +/// Tests for . /// [TestClass] -public sealed class Test_PublicTypeRequiresContractVersionAnalyzer +public sealed class Test_PublicTypeRequiresVersioningAnalyzer { [TestMethod] public async Task PublicClass_WithContractVersion_DoesNotWarn() @@ -32,6 +32,37 @@ public sealed class MyClass; await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); } + [TestMethod] + public async Task PublicClass_WithVersion_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [Version(1u)] + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicClass_WithBothContractVersionAndVersion_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + [Version(1u)] + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + [TestMethod] public async Task ApiContractEnum_WithoutContractVersion_DoesNotWarn() { @@ -46,7 +77,7 @@ public enum MyContract; } [TestMethod] - public async Task InternalClass_WithoutContractVersion_DoesNotWarn() + public async Task InternalClass_WithoutVersioning_DoesNotWarn() { const string source = """ internal sealed class MyClass; @@ -56,7 +87,7 @@ internal sealed class MyClass; } [TestMethod] - public async Task NestedPublicClass_WithoutContractVersion_DoesNotWarn() + public async Task NestedPublicClass_WithoutVersioning_DoesNotWarn() { const string source = """ using Windows.Foundation.Metadata; @@ -86,7 +117,7 @@ public sealed class MyClass; } [TestMethod] - public async Task PublicClass_WithoutContractVersion_Warns() + public async Task PublicClass_WithoutVersioning_Warns() { const string source = """ public sealed class {|CSWINRT2015:MyClass|}; @@ -96,7 +127,7 @@ public sealed class {|CSWINRT2015:MyClass|}; } [TestMethod] - public async Task PublicInterface_WithoutContractVersion_Warns() + public async Task PublicInterface_WithoutVersioning_Warns() { const string source = """ public interface {|CSWINRT2015:IMyInterface|}; @@ -106,7 +137,7 @@ public interface {|CSWINRT2015:IMyInterface|}; } [TestMethod] - public async Task PublicStruct_WithoutContractVersion_Warns() + public async Task PublicStruct_WithoutVersioning_Warns() { const string source = """ public struct {|CSWINRT2015:MyStruct|}; @@ -116,7 +147,7 @@ public struct {|CSWINRT2015:MyStruct|}; } [TestMethod] - public async Task PublicEnum_WithoutContractVersion_Warns() + public async Task PublicEnum_WithoutVersioning_Warns() { const string source = """ public enum {|CSWINRT2015:MyEnum|} @@ -130,7 +161,7 @@ public enum {|CSWINRT2015:MyEnum|} } [TestMethod] - public async Task PublicDelegate_WithoutContractVersion_Warns() + public async Task PublicDelegate_WithoutVersioning_Warns() { const string source = """ public delegate void {|CSWINRT2015:MyDelegate|}(); From ab8f8da9482a34fce09dfbc72976cc02df6278eb Mon Sep 17 00:00:00 2001 From: Sergio0694 Date: Wed, 6 May 2026 13:37:32 -0700 Subject: [PATCH 19/25] Add CSWINRT2016 diagnostic descriptor for inconsistent contract versioning Reports when a public type in a Windows Runtime component is missing a '[ContractVersion]' attribute, but at least one other public type in the same component does have one applied. Public types in a component should consistently use either contract versioning or not, to avoid mixing two different versioning schemes across the public API surface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AnalyzerReleases.Shipped.md | 3 ++- .../Diagnostics/DiagnosticDescriptors.cs | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md index 68d1f2fccd..d4cfc166d0 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md @@ -21,4 +21,5 @@ CSWINRT2011 | WindowsRuntime.SourceGenerator | Warning | Invalid 'ContractVersio 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 version metadata \ No newline at end of file +CSWINRT2015 | WindowsRuntime.SourceGenerator | Info | Public authored type missing version metadata +CSWINRT2016 | WindowsRuntime.SourceGenerator | Warning | Public authored type missing 'ContractVersionAttribute' \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs index 851419287e..4950162348 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs @@ -217,4 +217,18 @@ internal static partial class DiagnosticDescriptors isEnabledByDefault: true, description: "Public types in a Windows Runtime component should declare their version using either '[ContractVersion(typeof(SomeContract), version)]' (to associate with an API contract) or '[Version(version)]' (to specify a Windows Runtime version), so that consumers can target a specific version.", helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for a public authored type missing a [ContractVersion] attribute when at least one other public type in the same component has one applied. + /// + public static readonly DiagnosticDescriptor PublicTypeMissingContractVersion = new( + id: "CSWINRT2016", + title: "Public authored type missing 'ContractVersionAttribute'", + messageFormat: """The type '{0}' is publicly exposed in a Windows Runtime component that uses '[ContractVersion]' on at least one other public type, but '{0}' itself does not have a '[ContractVersion]' attribute applied. Public types in a component using contract versioning should consistently declare their associated API contract using '[ContractVersion(typeof(SomeContract), version)]'.""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Public types in a Windows Runtime component should use a consistent versioning scheme. If at least one public type uses '[ContractVersion]', all other public types should also use '[ContractVersion]' to declare their associated API contract.", + helpLinkUri: "https://github.com/microsoft/CsWinRT", + customTags: WellKnownDiagnosticTags.CompilationEnd); } \ No newline at end of file From 41195b29a05e38ff587c6d0bf250c6ff3e6f400f Mon Sep 17 00:00:00 2001 From: Sergio0694 Date: Wed, 6 May 2026 13:38:14 -0700 Subject: [PATCH 20/25] Add PublicTypeRequiresContractVersionAnalyzer for CSWINRT2016 Implements the consistency check for '[ContractVersion]' usage across the public API surface of a Windows Runtime component. Public types without '[ContractVersion]' are collected during symbol analysis, and diagnostics are reported in the compilation end action only if at least one other public type in the compilation has it applied. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...blicTypeRequiresContractVersionAnalyzer.cs | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs new file mode 100644 index 0000000000..12e7eb8b7b --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeRequiresContractVersionAnalyzer.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace WindowsRuntime.SourceGenerator.Diagnostics; + +/// +/// A diagnostic analyzer that reports when a public authored type is missing a [ContractVersion] attribute, +/// but at least one other public type in the same compilation does have one applied. This enforces a consistent +/// versioning scheme across the public API surface of a Windows Runtime component. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PublicTypeRequiresContractVersionAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [DiagnosticDescriptors.PublicTypeMissingContractVersion]; + + /// + 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 use '[ContractVersion]' + // with the version-only constructors to declare their own contract version, not as an association). + INamedTypeSymbol? apiContractAttributeType = context.Compilation.GetTypeByMetadataName("Windows.Foundation.Metadata.ApiContractAttribute"); + + // Shared state across symbol actions: collect public types missing '[ContractVersion]' and track + // whether at least one public type in the compilation does have a '[ContractVersion]' applied. + ConcurrentBag typesMissingContractVersion = []; + int anyTypeHasContractVersion = 0; + + 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 use '[ContractVersion]' with a different semantics + // (declaring their own contract version, not associating with another contract). + if (apiContractAttributeType is not null && + typeSymbol is { TypeKind: TypeKind.Enum } && + typeSymbol.HasAttributeWithType(apiContractAttributeType)) + { + return; + } + + if (typeSymbol.HasAttributeWithType(contractVersionAttributeType)) + { + _ = Interlocked.Exchange(ref anyTypeHasContractVersion, 1); + } + else + { + typesMissingContractVersion.Add(typeSymbol); + } + }, SymbolKind.NamedType); + + context.RegisterCompilationEndAction(context => + { + // Only report if at least one public type does have a '[ContractVersion]' applied: the + // analyzer specifically targets components that have opted into contract versioning, but + // are inconsistently applying it across the public API surface. + if (Volatile.Read(ref anyTypeHasContractVersion) == 0) + { + return; + } + + // Sort by source location for deterministic diagnostic reporting order + foreach (INamedTypeSymbol typeSymbol in typesMissingContractVersion + .OrderBy(static t => t.Locations.FirstOrDefault()?.SourceTree?.FilePath, StringComparer.Ordinal) + .ThenBy(static t => t.Locations.FirstOrDefault()?.SourceSpan.Start ?? 0)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.PublicTypeMissingContractVersion, + typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); + } + }); + }); + } +} From b26b8bf6dab6339199b16c6626a97656a4396c2c Mon Sep 17 00:00:00 2001 From: Sergio0694 Date: Wed, 6 May 2026 13:38:49 -0700 Subject: [PATCH 21/25] Add tests for PublicTypeRequiresContractVersionAnalyzer Covers: no public types, single public type with/without contract version, all public types with/without contract version, '[Version]' mixed with '[ContractVersion]', API contract enum exclusion, nested and internal types, and the multiple-warning scenario. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...blicTypeRequiresContractVersionAnalyzer.cs | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 src/Tests/SourceGenerator2Test/Test_PublicTypeRequiresContractVersionAnalyzer.cs diff --git a/src/Tests/SourceGenerator2Test/Test_PublicTypeRequiresContractVersionAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_PublicTypeRequiresContractVersionAnalyzer.cs new file mode 100644 index 0000000000..61409c48eb --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_PublicTypeRequiresContractVersionAnalyzer.cs @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using WindowsRuntime.SourceGenerator.Diagnostics; +using WindowsRuntime.SourceGenerator.Tests.Helpers; + +namespace WindowsRuntime.SourceGenerator.Tests; + +using VerifyCS = CSharpAnalyzerTest; + +/// +/// Tests for . +/// +[TestClass] +public sealed class Test_PublicTypeRequiresContractVersionAnalyzer +{ + [TestMethod] + public async Task NoPublicTypes_DoesNotWarn() + { + const string source = """ + internal sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task SinglePublicType_WithContractVersion_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task SinglePublicType_WithoutContractVersion_DoesNotWarn() + { + const string source = """ + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task AllPublicTypes_WithoutContractVersion_DoesNotWarn() + { + const string source = """ + public sealed class MyClass1; + + public sealed class MyClass2; + + public interface IMyInterface; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task AllPublicTypes_WithContractVersion_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + public sealed class MyClass1; + + [ContractVersion(typeof(MyContract), 1u)] + public sealed class MyClass2; + + [ContractVersion(typeof(MyContract), 1u)] + public interface IMyInterface; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task MixedPublicTypes_NotComponent_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + public sealed class MyClass1; + + public sealed class MyClass2; + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task ApiContractEnum_WithoutContractVersion_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + public sealed class MyClass1; + + [ApiContract] + public enum MyOtherContract; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task NestedPublicType_WithoutContractVersion_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + public sealed class Outer + { + public sealed class Nested; + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task InternalType_WithoutContractVersion_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + public sealed class MyClass1; + + internal sealed class MyClass2; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task MixedPublicTypes_WithoutContractVersion_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + public sealed class MyClass1; + + public sealed class {|CSWINRT2016:MyClass2|}; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicTypeWithVersionOnly_OtherTypeWithContractVersion_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + public sealed class MyClass1; + + [Version(1u)] + public sealed class {|CSWINRT2016:MyClass2|}; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task MultiplePublicTypes_WithoutContractVersion_AllWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + public sealed class MyClass1; + + public sealed class {|CSWINRT2016:MyClass2|}; + + public interface {|CSWINRT2016:IMyInterface|}; + + public struct {|CSWINRT2016:MyStruct|}; + + public enum {|CSWINRT2016:MyEnum|} + { + A, + B + } + + public delegate void {|CSWINRT2016:MyDelegate|}(); + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } +} From e6f18e32590a97b99be5ed830062c3db79ea30e9 Mon Sep 17 00:00:00 2001 From: Sergio0694 Date: Wed, 6 May 2026 13:39:29 -0700 Subject: [PATCH 22/25] Add CSWINRT2017 diagnostic descriptor for mixed versioning attributes Reports when a public type in a Windows Runtime component has both '[ContractVersion]' and '[Version]' attributes applied. Public types should use only one of the two versioning schemes, and the choice should be applied consistently across the public API surface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AnalyzerReleases.Shipped.md | 3 ++- .../Diagnostics/DiagnosticDescriptors.cs | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md index d4cfc166d0..8acb415788 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md @@ -22,4 +22,5 @@ CSWINRT2012 | WindowsRuntime.SourceGenerator | Warning | Invalid 'ContractVersio 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 version metadata -CSWINRT2016 | WindowsRuntime.SourceGenerator | Warning | Public authored type missing 'ContractVersionAttribute' \ No newline at end of file +CSWINRT2016 | WindowsRuntime.SourceGenerator | Warning | Public authored type missing 'ContractVersionAttribute' +CSWINRT2017 | WindowsRuntime.SourceGenerator | Warning | Public authored type mixing '[ContractVersion]' and '[Version]' \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs index 4950162348..65b19e356e 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs @@ -231,4 +231,17 @@ internal static partial class DiagnosticDescriptors description: "Public types in a Windows Runtime component should use a consistent versioning scheme. If at least one public type uses '[ContractVersion]', all other public types should also use '[ContractVersion]' to declare their associated API contract.", helpLinkUri: "https://github.com/microsoft/CsWinRT", customTags: WellKnownDiagnosticTags.CompilationEnd); + + /// + /// Gets a for a public authored type that has both a [ContractVersion] and a [Version] attribute applied. + /// + public static readonly DiagnosticDescriptor PublicTypeMixedVersioningAttributes = new( + id: "CSWINRT2017", + title: "Public authored type mixing '[ContractVersion]' and '[Version]'", + messageFormat: """The type '{0}' is publicly exposed in a Windows Runtime component and has both a '[ContractVersion]' and a '[Version]' attribute applied. Public types in a component should not mix two different versioning schemes; pick one of '[ContractVersion]' (to associate with an API contract) or '[Version]' (to specify a Windows Runtime version), and apply it consistently.""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Public types in a Windows Runtime component should not mix '[ContractVersion]' and '[Version]', as these represent two different versioning schemes. Pick one and apply it consistently across the public API surface of the component.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); } \ No newline at end of file From 4d33388d837583d7902a129c04ed60e9fce155ef Mon Sep 17 00:00:00 2001 From: Sergio0694 Date: Wed, 6 May 2026 13:40:05 -0700 Subject: [PATCH 23/25] Add PublicTypeMixedVersioningAttributesAnalyzer for CSWINRT2017 Implements the per-symbol check that flags public types in a Windows Runtime component which have both '[ContractVersion]' and '[Version]' attributes applied. API contract enum types (annotated with '[ApiContract]') are skipped, as those use '[ContractVersion]' with different semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...icTypeMixedVersioningAttributesAnalyzer.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeMixedVersioningAttributesAnalyzer.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeMixedVersioningAttributesAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeMixedVersioningAttributesAnalyzer.cs new file mode 100644 index 0000000000..e003545728 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/PublicTypeMixedVersioningAttributesAnalyzer.cs @@ -0,0 +1,82 @@ +// 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; + +/// +/// A diagnostic analyzer that reports when a public authored type has both a [ContractVersion] +/// and a [Version] attribute applied. Public types in a Windows Runtime component should +/// use only one of these two versioning schemes, applied consistently across the public API surface. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PublicTypeMixedVersioningAttributesAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [DiagnosticDescriptors.PublicTypeMixedVersioningAttributes]; + + /// + 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 '[Version]' symbol + if (context.Compilation.GetTypeByMetadataName("Windows.Foundation.Metadata.VersionAttribute") is not { } versionAttributeType) + { + 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 use '[ContractVersion]' with the version-only constructors + // to declare their own contract version, which is a different scenario from this analyzer. + if (apiContractAttributeType is not null && + typeSymbol is { TypeKind: TypeKind.Enum } && + typeSymbol.HasAttributeWithType(apiContractAttributeType)) + { + return; + } + + // Only report if both '[ContractVersion]' and '[Version]' are applied to the type + if (!typeSymbol.HasAttributeWithType(contractVersionAttributeType) || + !typeSymbol.HasAttributeWithType(versionAttributeType)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.PublicTypeMixedVersioningAttributes, + typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); + }, SymbolKind.NamedType); + }); + } +} From a5448dbc7fc048aa7a62499b035e3086ab1d46f5 Mon Sep 17 00:00:00 2001 From: Sergio0694 Date: Wed, 6 May 2026 13:40:38 -0700 Subject: [PATCH 24/25] Add tests for PublicTypeMixedVersioningAttributesAnalyzer Covers: only '[ContractVersion]' or only '[Version]' applied (no warning), API contract enum with both (skipped), internal/nested types with both (skipped or scoped), the not-a-component case, and the full set of public type kinds (class, interface, struct, enum, delegate) with both attributes applied (warning). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...icTypeMixedVersioningAttributesAnalyzer.cs | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 src/Tests/SourceGenerator2Test/Test_PublicTypeMixedVersioningAttributesAnalyzer.cs diff --git a/src/Tests/SourceGenerator2Test/Test_PublicTypeMixedVersioningAttributesAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_PublicTypeMixedVersioningAttributesAnalyzer.cs new file mode 100644 index 0000000000..f0d0623476 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_PublicTypeMixedVersioningAttributesAnalyzer.cs @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using WindowsRuntime.SourceGenerator.Diagnostics; +using WindowsRuntime.SourceGenerator.Tests.Helpers; + +namespace WindowsRuntime.SourceGenerator.Tests; + +using VerifyCS = CSharpAnalyzerTest; + +/// +/// Tests for . +/// +[TestClass] +public sealed class Test_PublicTypeMixedVersioningAttributesAnalyzer +{ + [TestMethod] + public async Task PublicClass_NoVersioning_DoesNotWarn() + { + const string source = """ + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicClass_OnlyContractVersion_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicClass_OnlyVersion_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [Version(1u)] + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task ApiContractEnum_WithContractVersionAndVersion_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + [Version(1u)] + public enum MyContract; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task InternalClass_WithBothAttributes_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + [Version(1u)] + internal sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task NestedPublicClass_WithBothAttributes_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + public sealed class Outer + { + [ContractVersion(typeof(MyContract), 1u)] + [Version(1u)] + public sealed class Nested; + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicClass_WithBothAttributes_NotComponent_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + [Version(1u)] + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task PublicClass_WithBothAttributes_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + [Version(1u)] + public sealed class {|CSWINRT2017:MyClass|}; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicInterface_WithBothAttributes_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + [Version(1u)] + public interface {|CSWINRT2017:IMyInterface|}; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicStruct_WithBothAttributes_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + [Version(1u)] + public struct {|CSWINRT2017:MyStruct|}; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicEnum_WithBothAttributes_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + [Version(1u)] + public enum {|CSWINRT2017:MyEnum|} + { + A, + B + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicDelegate_WithBothAttributes_Warns() + { + const string source = """ + using Windows.Foundation.Metadata; + + [ApiContract] + [ContractVersion(1u)] + public enum MyContract; + + [ContractVersion(typeof(MyContract), 1u)] + [Version(1u)] + public delegate void {|CSWINRT2017:MyDelegate|}(); + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } +} From 8988d24e974eb475de76e71cdabb6236b29f3b5c Mon Sep 17 00:00:00 2001 From: Sergio0694 Date: Thu, 7 May 2026 06:27:21 -0700 Subject: [PATCH 25/25] Address new authoring analyzer warnings in 'AuthoringTest' Two of the new authoring analyzers fire on the 'AuthoringTest' project, which intentionally exercises a broad mix of WinRT authoring patterns: - 'CSWINRT2014' fires on 'AnotherNamespaceContract', which is missing a '[ContractVersion]' attribute. This is a real oversight, since the enum is used as the contract argument by other types in the same file. Fix it by adding '[ContractVersion(1u)]'. - 'CSWINRT2016' fires on the 81 public types in the project that lack '[ContractVersion]', because a few other types in the same project do have it. This is the intended state of the test surface, which exercises all the various combinations of versioning attributes (with and without '[ContractVersion]', with '[Version]', mixed, etc.). Suppress the warning in the project file with a comment documenting the rationale. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Tests/AuthoringTest/AuthoringTest.csproj | 8 ++++++++ src/Tests/AuthoringTest/Program.cs | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Tests/AuthoringTest/AuthoringTest.csproj b/src/Tests/AuthoringTest/AuthoringTest.csproj index 72ed2eacc1..24afdfea4b 100644 --- a/src/Tests/AuthoringTest/AuthoringTest.csproj +++ b/src/Tests/AuthoringTest/AuthoringTest.csproj @@ -30,6 +30,14 @@ location it is assigned to." --> $(WarningsNotAsErrors);CS0067;CS0282;IL2087 + + + $(NoWarn);CSWINRT2016 diff --git a/src/Tests/AuthoringTest/Program.cs b/src/Tests/AuthoringTest/Program.cs index 68026d883c..9eeb3111c7 100644 --- a/src/Tests/AuthoringTest/Program.cs +++ b/src/Tests/AuthoringTest/Program.cs @@ -2285,6 +2285,7 @@ public void RaiseDataChanged() // Contract versioning [Windows.Foundation.Metadata.ApiContract] + [Windows.Foundation.Metadata.ContractVersion(1u)] public enum AnotherNamespaceContract { } [Windows.Foundation.Metadata.ContractVersion(typeof(AnotherNamespaceContract), 1u)] @@ -2364,4 +2365,4 @@ public void RaiseUrgencyChanged() } } } -} \ No newline at end of file +}