Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/attribute-projections.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
CSWINRT2020 | WindowsRuntime.SourceGenerator | Warning | Duplicate '[WindowsRuntimeNativeExposedType]' target type
CSWINRT2021 | WindowsRuntime.SourceGenerator | Warning | '[Obsolete]' on an authored API without '[Deprecated]'
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A diagnostic analyzer that reports when a publicly exposed API of a Windows Runtime component has an
/// <c>[Obsolete]</c> attribute applied, but no <c>[Windows.Foundation.Metadata.Deprecated]</c> attribute.
/// </summary>
/// <remarks>
/// <c>[Obsolete]</c> 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.
/// </remarks>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ObsoleteWithoutDeprecatedAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [DiagnosticDescriptors.ObsoleteWithoutDeprecated];

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterCompilationStartAction(static context =>
{
// This analyzer only applies to Windows Runtime component authoring scenarios
if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetCsWinRTComponent())
{
return;
}

// Get the '[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);
});
}

/// <summary>
/// Checks whether a given symbol is publicly exposed from a Windows Runtime component, and so ends up
/// in the generated <c>.winmd</c>.
/// </summary>
/// <param name="symbol">The symbol to check.</param>
/// <returns>Whether <paramref name="symbol"/> is publicly exposed from the component.</returns>
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 };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> for an authored API with an <c>[Obsolete]</c> attribute, but no <c>[Deprecated]</c> attribute.
/// </summary>
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");
}
Loading
Loading