From 981471a7b3b8ce55a3554b287bd1d3d9f0ec8d04 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 11 Aug 2026 10:29:31 -0700 Subject: [PATCH 1/4] Add CSWINRT2022 diagnostic descriptor for [Obsolete] without [Deprecated] '[Obsolete]' is a .NET concept with no Windows Runtime counterpart, so the WinMD generator copies it verbatim into the '.winmd' rather than translating it, where no other language projection can see it. Deprecating an API of an authored component requires '[Windows.Foundation.Metadata.Deprecated]', which is the only deprecation Windows Runtime metadata can carry. The id is 'CSWINRT2022' rather than 'CSWINRT2021', which this branch originally used: the base branch has since claimed 'CSWINRT2021' for the unsupported '[Experimental]' target diagnostic. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ed5549ac-fb33-4d08-a8f3-d6cf3b8bc8e1 --- .../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 86e754983..77e6d05e8 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md @@ -27,4 +27,5 @@ CSWINRT2017 | WindowsRuntime.SourceGenerator | Warning | Public authored type mi CSWINRT2018 | WindowsRuntime.SourceGenerator | Warning | '[WindowsRuntimeNativeExposedType]' target type cannot be instantiated CSWINRT2019 | WindowsRuntime.SourceGenerator | Warning | '[WindowsRuntimeNativeExposedType]' target type is not a projected class CSWINRT2020 | WindowsRuntime.SourceGenerator | Warning | Duplicate '[WindowsRuntimeNativeExposedType]' target type -CSWINRT2021 | WindowsRuntime.SourceGenerator | Warning | Unsupported '[Experimental]' target for Windows Runtime metadata \ No newline at end of file +CSWINRT2021 | WindowsRuntime.SourceGenerator | Warning | Unsupported '[Experimental]' target for Windows Runtime metadata +CSWINRT2022 | WindowsRuntime.SourceGenerator | Warning | '[Obsolete]' on an authored API without '[Deprecated]' diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs index 640e0b1c4..bdab6b302 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs @@ -299,4 +299,17 @@ internal static partial class DiagnosticDescriptors isEnabledByDefault: true, description: "The .NET '[Experimental]' attribute supports more targets than the Windows Runtime one it is translated into, so applications on targets that Windows Runtime metadata cannot represent are dropped, making the API appear stable to every other language projection.", helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for an authored API with an [Obsolete] attribute, but no [Deprecated] attribute. + /// + public static readonly DiagnosticDescriptor ObsoleteWithoutDeprecated = new( + id: "CSWINRT2022", + title: "'[Obsolete]' on an authored API without '[Deprecated]'", + messageFormat: """The API '{0}' is publicly exposed from a Windows Runtime component and has an '[Obsolete]' attribute applied, but no '[Windows.Foundation.Metadata.Deprecated]' attribute. '[Obsolete]' has no Windows Runtime counterpart, so it is not translated when the '.winmd' is generated, and the deprecation is invisible to every consumer of the component. Apply '[Windows.Foundation.Metadata.Deprecated]' to also deprecate '{0}' in the Windows Runtime metadata.""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Deprecating a publicly exposed API of a Windows Runtime component requires '[Windows.Foundation.Metadata.Deprecated]', which is the only deprecation the '.winmd' can carry. '[Obsolete]' is a .NET concept with no Windows Runtime counterpart: it is not translated by the WinMD generator, so on its own it deprecates the API for nobody but the C# code inside the component itself.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); } \ No newline at end of file From 05491430ec5a0cc093cc58f9d33647416da337f0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 11 Aug 2026 10:29:47 -0700 Subject: [PATCH 2/4] Add the ObsoleteWithoutDeprecated analyzer for CSWINRT2022 Reports publicly exposed APIs of a Windows Runtime component that carry '[Obsolete]' but no '[Windows.Foundation.Metadata.Deprecated]'. Having both applied is the supported way to deprecate an API for .NET and Windows Runtime consumers alike, so that combination is not reported. The check covers types and their members, not just types: the WinMD generator supports '[Deprecated]' on methods, properties and events too, so '[Obsolete]' on a member is exactly as silently ineffective as it is on a type. Only what actually reaches the '.winmd' is considered, so that every report has an action the developer can take: - Types are only reported when public and top level, as Windows Runtime has no nested types. - Members are only reported when public and declared by such a type. They are also restricted to classes and interfaces: a Windows Runtime struct is a plain field aggregate, so the generator drops every member of one other than its public instance fields. - Accessors are skipped, as they are only exported as part of their property or event (which is reported instead). The generator moves a '[Deprecated]' from the property or event down onto the accessor row, and never reads one written on a C# accessor. - Constructors are skipped, because '[Deprecated]' does not include 'AttributeTargets.Constructor' in its usage and so cannot be applied to one at all. Reporting them would produce a warning whose only resolutions are suppressing it or dropping the '[Obsolete]'. This matches how the sibling 'ExperimentalAttributeTargetAnalyzer' treats constructors for the same reason. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ed5549ac-fb33-4d08-a8f3-d6cf3b8bc8e1 --- .../ObsoleteWithoutDeprecatedAnalyzer.cs | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ObsoleteWithoutDeprecatedAnalyzer.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ObsoleteWithoutDeprecatedAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ObsoleteWithoutDeprecatedAnalyzer.cs new file mode 100644 index 000000000..160d016ee --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ObsoleteWithoutDeprecatedAnalyzer.cs @@ -0,0 +1,124 @@ +// 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 publicly exposed API of a Windows Runtime component has an +/// [Obsolete] attribute applied, but no [Windows.Foundation.Metadata.Deprecated] attribute. +/// +/// +/// [Obsolete] has no Windows Runtime counterpart, so the WinMD generator copies it verbatim rather +/// than translating it, which leaves the deprecation invisible to every consumer of the component. Having +/// both attributes applied is the supported way to deprecate an API for .NET and Windows Runtime consumers +/// alike, so that combination is not reported. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ObsoleteWithoutDeprecatedAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [DiagnosticDescriptors.ObsoleteWithoutDeprecated]; + + /// + 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 '[Obsolete]' symbol + if (context.Compilation.GetTypeByMetadataName("System.ObsoleteAttribute") is not { } obsoleteAttributeType) + { + return; + } + + // Get the '[Deprecated]' symbol. Without it there is no way to author the deprecation the + // diagnostic would be suggesting, so nothing is reported (this also means the analyzer is + // inert when the component does not reference a Windows SDK projection at all). + if (context.Compilation.GetTypeByMetadataName("Windows.Foundation.Metadata.DeprecatedAttribute") is not { } deprecatedAttributeType) + { + return; + } + + context.RegisterSymbolAction(context => + { + if (!IsPubliclyExposedFromComponent(context.Symbol)) + { + return; + } + + // Only report APIs that are deprecated for .NET consumers, but not for Windows Runtime ones + if (!context.Symbol.HasAttributeWithType(obsoleteAttributeType) || + context.Symbol.HasAttributeWithType(deprecatedAttributeType)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.ObsoleteWithoutDeprecated, + context.Symbol.Locations.FirstOrDefault(), + context.Symbol)); + }, SymbolKind.NamedType, SymbolKind.Method, SymbolKind.Property, SymbolKind.Event); + }); + } + + /// + /// Checks whether a given symbol is publicly exposed from a Windows Runtime component, and so ends up + /// in the generated .winmd. + /// + /// The symbol to check. + /// Whether is publicly exposed from the component. + private static bool IsPubliclyExposedFromComponent(ISymbol symbol) + { + // Skip symbols with no declaration in source (eg. the implicit parameterless constructor of a + // class), which cannot carry an attribute of their own and have nowhere to report a diagnostic + if (symbol.IsImplicitlyDeclared) + { + return false; + } + + if (symbol.DeclaredAccessibility is not Accessibility.Public) + { + return false; + } + + // Types are only exported when they are top level: Windows Runtime has no nested types + if (symbol is INamedTypeSymbol) + { + return symbol.ContainingType is null; + } + + // Property and event accessors are only exported as part of the property or event they belong to, + // which is the symbol reported instead. The generator moves a '[Deprecated]' from the property or + // event down onto the accessor row itself, and never reads one written on a C# accessor. + if (symbol is IMethodSymbol { AssociatedSymbol: not null }) + { + return false; + } + + // Constructors are exported as activation factory methods, and '[Deprecated]' cannot be applied to + // them ('AttributeTargets.Constructor' is not part of its usage), so there would be no way to act + // on the diagnostic. Deprecating the whole type is the only option, and that is reported already. + if (symbol is IMethodSymbol { MethodKind: MethodKind.Constructor or MethodKind.StaticConstructor }) + { + return false; + } + + // Members are exported with their declaring type, so they are only exported when it is. Only classes + // and interfaces export members at all: a Windows Runtime struct is a plain field aggregate, so the + // generator drops every member of one other than its public instance fields. + return symbol.ContainingType is { DeclaredAccessibility: Accessibility.Public, ContainingType: null, TypeKind: TypeKind.Class or TypeKind.Interface }; + } +} From 8ad0333dc4c2ea3a52db0c2d6ab2e8d6a5fdf5bd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 11 Aug 2026 10:30:02 -0700 Subject: [PATCH 3/4] Add tests and docs for the ObsoleteWithoutDeprecated analyzer Covers the cases the analyzer must stay quiet for (no attributes, only '[Deprecated]', both attributes, non component projects, non public types and members, nested types, struct members, constructors and accessors) and the ones it must report (every public type kind, and public methods, properties and events of both classes and interfaces). The accessor case is the one most likely to regress into a vacuous test, so it puts '[Obsolete]' on the accessor itself rather than on the property: an attribute written on a property is never surfaced on its accessor symbols, so a test that relies on that would pass with the guard removed. Verified that this one does fail without it. The attribute projections doc gains a 'CSWINRT2022' section alongside the 'CSWINRT2021' one the base branch added, and the '[Obsolete]' note now links to it. The two diagnostics are neighbors by construction: both exist because an attribute an author reaches for in C# has no Windows Runtime metadata target to land on, and both exclude constructors for the same reason. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ed5549ac-fb33-4d08-a8f3-d6cf3b8bc8e1 --- docs/attribute-projections.md | 18 +- .../Test_ObsoleteWithoutDeprecatedAnalyzer.cs | 334 ++++++++++++++++++ 2 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 src/Tests/SourceGenerator2Test/Test_ObsoleteWithoutDeprecatedAnalyzer.cs diff --git a/docs/attribute-projections.md b/docs/attribute-projections.md index b82ec7a21..02e1acfb6 100644 --- a/docs/attribute-projections.md +++ b/docs/attribute-projections.md @@ -52,7 +52,7 @@ What the WinMD generator emits into an authored component's `.winmd` for an attr | `[WindowsRuntime.Xaml.GeneratedCustomPropertyProvider]`, `[System.Reflection.DefaultMember]`, and anything under `System.Runtime.CompilerServices` | *nothing* | Either handled by CsWinRT itself or meaningless in Windows Runtime metadata. | | Any other public attribute type | itself | Non-public attribute types, and attributes whose signature cannot be read, are skipped. | -> **Note**: `[System.Obsolete]` is **not** translated into `[Windows.Foundation.Metadata.Deprecated]`. It is copied as-is, so it is invisible to every other language projection. Use `[Windows.Foundation.Metadata.Deprecated]` to deprecate an API of an authored component. `[Experimental]` is the exception to that rule only because the Windows Runtime attribute has no projected form to apply. +> **Note**: `[System.Obsolete]` is **not** translated into `[Windows.Foundation.Metadata.Deprecated]`. It is copied as-is, so it is invisible to every other language projection. Use `[Windows.Foundation.Metadata.Deprecated]` to deprecate an API of an authored component; applying `[Obsolete]` without it is reported as [CSWINRT2022](#cswinrt2022-obsolete-without-deprecated). `[Experimental]` is the exception to that rule only because the Windows Runtime attribute has no projected form to apply. ### CSWINRT2021: unsupported `[Experimental]` targets @@ -63,6 +63,22 @@ The .NET `[Experimental]` attribute supports more targets than the Windows Runti Rather than emit the marker where nothing would read it, which would silently make the API look stable to every other language projection, those applications are dropped and `CSWINRT2021` reports them at the source. Mark the whole runtime class as experimental to cover its constructors. +### CSWINRT2022: `[Obsolete]` without `[Deprecated]` + +`[Obsolete]` is *the* way to deprecate an API in C#, so it is the natural thing to reach for in an authored component. It is not translated into `[Windows.Foundation.Metadata.Deprecated]` though: it is copied verbatim, so the `.winmd` ends up carrying a `System.ObsoleteAttribute` reference that no other language projection understands. The component still builds and works, and the deprecation simply never reaches any consumer. + +`CSWINRT2022` reports a publicly exposed API that has `[Obsolete]` but no `[Deprecated]`. Applying both is the supported way to deprecate an API for .NET and Windows Runtime consumers alike, and silences the diagnostic: + +```csharp +[Obsolete("Use NewMethod instead")] +[Deprecated("Use NewMethod instead", DeprecationType.Deprecate, 1)] +public void OldMethod() +{ +} +``` + +Only APIs that actually reach the `.winmd` are reported, so that every report has an action available. Constructors are excluded for the same reason as in `CSWINRT2021`: `[Deprecated]` has no `AttributeTargets.Constructor` in its usage, so it cannot be applied to one at all. Deprecate the whole runtime class to cover its constructors. + ## Related documentation - [CSWINRT3005](diagnostics/cswinrt3005.md): using an experimental Windows Runtime API diff --git a/src/Tests/SourceGenerator2Test/Test_ObsoleteWithoutDeprecatedAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_ObsoleteWithoutDeprecatedAnalyzer.cs new file mode 100644 index 000000000..0032d0e5a --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_ObsoleteWithoutDeprecatedAnalyzer.cs @@ -0,0 +1,334 @@ +// 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_ObsoleteWithoutDeprecatedAnalyzer +{ + [TestMethod] + public async Task PublicClass_NoAttributes_DoesNotWarn() + { + const string source = """ + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicClass_OnlyDeprecated_DoesNotWarn() + { + const string source = """ + using Windows.Foundation.Metadata; + + [Deprecated("Use MyOtherClass instead", DeprecationType.Deprecate, 1u)] + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicClass_ObsoleteAndDeprecated_DoesNotWarn() + { + const string source = """ + using System; + using Windows.Foundation.Metadata; + + [Obsolete("Use MyOtherClass instead")] + [Deprecated("Use MyOtherClass instead", DeprecationType.Deprecate, 1u)] + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicClass_OnlyObsolete_NotComponent_DoesNotWarn() + { + const string source = """ + using System; + + [Obsolete("Use MyOtherClass instead")] + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task InternalClass_OnlyObsolete_DoesNotWarn() + { + const string source = """ + using System; + + [Obsolete("Use MyOtherClass instead")] + internal sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task NestedPublicClass_OnlyObsolete_DoesNotWarn() + { + // Windows Runtime has no nested types, so a nested type never reaches the '.winmd' + const string source = """ + using System; + + public sealed class Outer + { + [Obsolete("Use something else instead")] + public sealed class Nested; + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task NonPublicMember_OnlyObsolete_DoesNotWarn() + { + const string source = """ + using System; + + public sealed class MyClass + { + [Obsolete("Use NewMethod instead")] + internal void OldMethod() + { + } + + [Obsolete("Use NewProperty instead")] + private int OldProperty => 42; + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicMemberOfInternalType_OnlyObsolete_DoesNotWarn() + { + const string source = """ + using System; + + internal sealed class MyClass + { + [Obsolete("Use NewMethod instead")] + public void OldMethod() + { + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + [DataRow("class")] + [DataRow("struct")] + public async Task PublicType_OnlyObsolete_Warns(string typeKeyword) + { + string source = $$""" + using System; + + [Obsolete("Use MyOtherType instead")] + public {{typeKeyword}} {|CSWINRT2022:MyType|}; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicInterface_OnlyObsolete_Warns() + { + const string source = """ + using System; + + [Obsolete("Use IMyOtherInterface instead")] + public interface {|CSWINRT2022:IMyInterface|}; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicEnum_OnlyObsolete_Warns() + { + const string source = """ + using System; + + [Obsolete("Use MyOtherEnum instead")] + public enum {|CSWINRT2022:MyEnum|} + { + A, + B + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicDelegate_OnlyObsolete_Warns() + { + const string source = """ + using System; + + [Obsolete("Use MyOtherDelegate instead")] + public delegate void {|CSWINRT2022:MyDelegate|}(); + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicMembers_OnlyObsolete_Warn() + { + // '[Deprecated]' is supported on members too, so '[Obsolete]' is just as ineffective there + const string source = """ + using System; + + public sealed class MyClass + { + [Obsolete("Use NewMethod instead")] + public void {|CSWINRT2022:OldMethod|}() + { + } + + [Obsolete("Use NewProperty instead")] + public int {|CSWINRT2022:OldProperty|} => 42; + + [Obsolete("Use NewEvent instead")] + public event EventHandler {|CSWINRT2022:OldEvent|}; + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicMembers_ObsoleteAndDeprecated_DoNotWarn() + { + const string source = """ + using System; + using Windows.Foundation.Metadata; + + public sealed class MyClass + { + [Obsolete("Use NewMethod instead")] + [Deprecated("Use NewMethod instead", DeprecationType.Deprecate, 1u)] + public void OldMethod() + { + } + + [Obsolete("Use NewProperty instead")] + [Deprecated("Use NewProperty instead", DeprecationType.Deprecate, 1u)] + public int OldProperty => 42; + + [Obsolete("Use NewEvent instead")] + [Deprecated("Use NewEvent instead", DeprecationType.Deprecate, 1u)] + public event EventHandler OldEvent; + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicConstructor_OnlyObsolete_DoesNotWarn() + { + // '[Deprecated]' does not include 'AttributeTargets.Constructor' in its usage, so it cannot be + // applied to a constructor at all: there would be no way to act on the diagnostic + const string source = """ + using System; + + public sealed class MyClass + { + [Obsolete("Use the parameterless constructor instead")] + public MyClass(int value) + { + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task ObsoleteOnPropertyAccessor_DoesNotWarn() + { + // Accessors are exported as part of their property, and the generator only ever moves a + // '[Deprecated]' from the property down onto the accessor row, never the other way around + const string source = """ + using System; + + public sealed class MyClass + { + public int OldProperty + { + [Obsolete("Use NewProperty instead")] + get; + + set; + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task StructMembers_OnlyObsolete_DoNotWarn() + { + // A Windows Runtime struct is a plain field aggregate: the generator drops every member of one + // other than its public instance fields, so those members never reach the '.winmd' + const string source = """ + using System; + + public struct MyStruct + { + public int Value; + + [Obsolete("Use Value instead")] + public int GetValue() + { + return Value; + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task PublicInterfaceMembers_OnlyObsolete_Warn() + { + // Interface members are implicitly public, so they are exported with the interface + const string source = """ + using System; + + public interface IMyInterface + { + [Obsolete("Use NewMethod instead")] + void {|CSWINRT2022:OldMethod|}(); + + [Obsolete("Use NewProperty instead")] + int {|CSWINRT2022:OldProperty|} { get; } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } +} From fd7a968c212bb9e823bdbd8fbe2f118b3bfe1827 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 11 Aug 2026 10:46:50 -0700 Subject: [PATCH 4/4] Also report [Obsolete] on authored enum members and struct fields Windows Runtime metadata carries member markers on individual enum members and struct fields, and the base branch just started propagating attributes onto them ("Propagate custom attributes onto authored enum members and struct fields"). An '[Obsolete]' there is now silently ineffective in exactly the way this diagnostic exists to catch, but it went unreported: the analyzer only registered for types, methods, properties and events. Fields are now analyzed too, matching what the generator actually exports: - Every member of a public top level enum, as the generator copies the attributes of all of them. - The public instance fields of a public top level struct. Its static and const fields are not exported (the generator skips them), so they are not reported. - Nothing else: a Windows Runtime class is exposed purely through its interfaces, so no field of one ever reaches the '.winmd'. The struct case also removes an inaccuracy in the previous scoping, which excluded struct members wholesale on the grounds that a struct is a plain field aggregate. That is the reason its methods and properties are not reported, but its fields are precisely what it does export. Verified the two new positive tests fail when the field registration is removed, so they cover the new path rather than passing by construction. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ed5549ac-fb33-4d08-a8f3-d6cf3b8bc8e1 --- docs/attribute-projections.md | 2 +- .../ObsoleteWithoutDeprecatedAnalyzer.cs | 31 +++-- .../Test_ObsoleteWithoutDeprecatedAnalyzer.cs | 112 ++++++++++++++++++ 3 files changed, 137 insertions(+), 8 deletions(-) diff --git a/docs/attribute-projections.md b/docs/attribute-projections.md index 02e1acfb6..cdf7676e8 100644 --- a/docs/attribute-projections.md +++ b/docs/attribute-projections.md @@ -77,7 +77,7 @@ public void OldMethod() } ``` -Only APIs that actually reach the `.winmd` are reported, so that every report has an action available. Constructors are excluded for the same reason as in `CSWINRT2021`: `[Deprecated]` has no `AttributeTargets.Constructor` in its usage, so it cannot be applied to one at all. Deprecate the whole runtime class to cover its constructors. +Only APIs that actually reach the `.winmd` are reported, so that every report has an action available. That includes individual **enum members** and **struct fields**, which Windows Runtime metadata carries member markers on (the Windows SDK uses this to deprecate a single member of an existing enum). Constructors are excluded for the same reason as in `CSWINRT2021`: `[Deprecated]` has no `AttributeTargets.Constructor` in its usage, so it cannot be applied to one at all. Deprecate the whole runtime class to cover its constructors. ## Related documentation diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ObsoleteWithoutDeprecatedAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ObsoleteWithoutDeprecatedAnalyzer.cs index 160d016ee..3ba123574 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ObsoleteWithoutDeprecatedAnalyzer.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/ObsoleteWithoutDeprecatedAnalyzer.cs @@ -70,7 +70,7 @@ public override void Initialize(AnalysisContext context) DiagnosticDescriptors.ObsoleteWithoutDeprecated, context.Symbol.Locations.FirstOrDefault(), context.Symbol)); - }, SymbolKind.NamedType, SymbolKind.Method, SymbolKind.Property, SymbolKind.Event); + }, SymbolKind.NamedType, SymbolKind.Method, SymbolKind.Property, SymbolKind.Event, SymbolKind.Field); }); } @@ -82,8 +82,9 @@ public override void Initialize(AnalysisContext context) /// Whether is publicly exposed from the component. private static bool IsPubliclyExposedFromComponent(ISymbol symbol) { - // Skip symbols with no declaration in source (eg. the implicit parameterless constructor of a - // class), which cannot carry an attribute of their own and have nowhere to report a diagnostic + // Skip symbols with no declaration in source (eg. the implicit parameterless constructor of a class, + // or the 'value__' field of an enum), which cannot carry an attribute of their own and have nowhere + // to report a diagnostic if (symbol.IsImplicitlyDeclared) { return false; @@ -100,6 +101,23 @@ private static bool IsPubliclyExposedFromComponent(ISymbol symbol) return symbol.ContainingType is null; } + // Every remaining symbol is a member, and a member is only exported when its declaring type is + if (symbol.ContainingType is not { DeclaredAccessibility: Accessibility.Public, ContainingType: null } containingType) + { + return false; + } + + // Fields are exported from the two type kinds that are pure data in Windows Runtime, and metadata + // supports member markers on them individually (the Windows SDK uses this to mark a single new + // member of an existing enum experimental). + if (symbol is IFieldSymbol fieldSymbol) + { + // Every enum member is exported, while a struct only exports its public instance fields, so its + // static and const fields are not reported. No other type kind exports a field at all. + return containingType.TypeKind is TypeKind.Enum + || (containingType.TypeKind is TypeKind.Struct && !fieldSymbol.IsStatic); + } + // Property and event accessors are only exported as part of the property or event they belong to, // which is the symbol reported instead. The generator moves a '[Deprecated]' from the property or // event down onto the accessor row itself, and never reads one written on a C# accessor. @@ -116,9 +134,8 @@ private static bool IsPubliclyExposedFromComponent(ISymbol symbol) return false; } - // Members are exported with their declaring type, so they are only exported when it is. Only classes - // and interfaces export members at all: a Windows Runtime struct is a plain field aggregate, so the - // generator drops every member of one other than its public instance fields. - return symbol.ContainingType is { DeclaredAccessibility: Accessibility.Public, ContainingType: null, TypeKind: TypeKind.Class or TypeKind.Interface }; + // Only classes and interfaces export members other than fields: a Windows Runtime struct is a plain + // field aggregate, so the generator drops every member of one other than its public instance fields. + return containingType.TypeKind is TypeKind.Class or TypeKind.Interface; } } diff --git a/src/Tests/SourceGenerator2Test/Test_ObsoleteWithoutDeprecatedAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_ObsoleteWithoutDeprecatedAnalyzer.cs index 0032d0e5a..3d69ed382 100644 --- a/src/Tests/SourceGenerator2Test/Test_ObsoleteWithoutDeprecatedAnalyzer.cs +++ b/src/Tests/SourceGenerator2Test/Test_ObsoleteWithoutDeprecatedAnalyzer.cs @@ -331,4 +331,116 @@ public interface IMyInterface await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); } + + [TestMethod] + public async Task EnumMember_OnlyObsolete_Warns() + { + // Windows Runtime metadata carries member markers on individual enum members, and the generator + // copies the attributes of every enum member over, so an '[Obsolete]' there is just as ineffective + const string source = """ + using System; + + public enum MyEnum + { + A, + + [Obsolete("Use A instead")] + {|CSWINRT2022:B|} + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task EnumMember_ObsoleteAndDeprecated_DoesNotWarn() + { + const string source = """ + using System; + using Windows.Foundation.Metadata; + + public enum MyEnum + { + A, + + [Obsolete("Use A instead")] + [Deprecated("Use A instead", DeprecationType.Deprecate, 1u)] + B + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task StructInstanceField_OnlyObsolete_Warns() + { + // A struct's public instance fields are the one kind of member it does export, and the generator + // copies their attributes over + const string source = """ + using System; + + public struct MyStruct + { + [Obsolete("Use NewValue instead")] + public int {|CSWINRT2022:OldValue|}; + + public int NewValue; + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task StructInstanceField_ObsoleteAndDeprecated_DoesNotWarn() + { + const string source = """ + using System; + using Windows.Foundation.Metadata; + + public struct MyStruct + { + [Obsolete("Use NewValue instead")] + [Deprecated("Use NewValue instead", DeprecationType.Deprecate, 1u)] + public int OldValue; + + public int NewValue; + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } + + [TestMethod] + public async Task NonExportedFields_OnlyObsolete_DoNotWarn() + { + // A struct only exports its public instance fields, and a class exports no field at all: a Windows + // Runtime class is exposed purely through its interfaces, so none of these reach the '.winmd' + const string source = """ + using System; + + public struct MyStruct + { + public int Value; + + [Obsolete("Use Value instead")] + public static int OldStatic; + + [Obsolete("Use Value instead")] + public const int OldConstant = 42; + + [Obsolete("Use Value instead")] + internal int OldInternal; + } + + public sealed class MyClass + { + [Obsolete("Use NewProperty instead")] + public int OldField; + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } }