diff --git a/docs/attribute-projections.md b/docs/attribute-projections.md index 90c1dd45b..3fef9835d 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 `CSWINRT2021`. `[Experimental]` is the exception to that rule only because the Windows Runtime attribute has no projected form to apply. ## Related documentation diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md index e1c6e605f..0fc27d5da 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md @@ -26,4 +26,5 @@ CSWINRT2016 | WindowsRuntime.SourceGenerator | Warning | Public authored type mi CSWINRT2017 | WindowsRuntime.SourceGenerator | Warning | Public authored type mixing '[ContractVersion]' and '[Version]' 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 \ No newline at end of file +CSWINRT2020 | WindowsRuntime.SourceGenerator | Warning | Duplicate '[WindowsRuntimeNativeExposedType]' target type +CSWINRT2021 | WindowsRuntime.SourceGenerator | Warning | '[Obsolete]' on an authored API without '[Deprecated]' \ No newline at end of file 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 }; + } +} diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs index 84567ce6c..1ed9d9bae 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs @@ -286,4 +286,17 @@ internal static partial class DiagnosticDescriptors description: "A given type should only be used as the target of a single '[WindowsRuntimeNativeExposedType]' attribute in an assembly. CCW marshalling code is only generated once per type, so additional applications of the attribute for the same type have no effect.", helpLinkUri: "https://github.com/microsoft/CsWinRT", customTags: WellKnownDiagnosticTags.CompilationEnd); + + /// + /// Gets a for an authored API with an [Obsolete] attribute, but no [Deprecated] attribute. + /// + public static readonly DiagnosticDescriptor ObsoleteWithoutDeprecated = new( + id: "CSWINRT2021", + 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 diff --git a/src/Tests/SourceGenerator2Test/Test_ObsoleteWithoutDeprecatedAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_ObsoleteWithoutDeprecatedAnalyzer.cs new file mode 100644 index 000000000..d46fcacab --- /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}} {|CSWINRT2021: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 {|CSWINRT2021: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 {|CSWINRT2021: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 {|CSWINRT2021: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 {|CSWINRT2021:OldMethod|}() + { + } + + [Obsolete("Use NewProperty instead")] + public int {|CSWINRT2021:OldProperty|} => 42; + + [Obsolete("Use NewEvent instead")] + public event EventHandler {|CSWINRT2021: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 {|CSWINRT2021:OldMethod|}(); + + [Obsolete("Use NewProperty instead")] + int {|CSWINRT2021:OldProperty|} { get; } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, isCsWinRTComponent: true); + } +}