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
18 changes: 17 additions & 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 [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

Expand All @@ -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. 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

- [CSWINRT3005](diagnostics/cswinrt3005.md): using an experimental Windows Runtime API
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
CSWINRT2021 | WindowsRuntime.SourceGenerator | Warning | Unsupported '[Experimental]' target for Windows Runtime metadata
CSWINRT2022 | WindowsRuntime.SourceGenerator | Warning | '[Obsolete]' on an authored API without '[Deprecated]'
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// 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
Comment thread
manodasanW marked this conversation as resolved.
{
/// <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, SymbolKind.Field);
});
}

/// <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,
// 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;
}

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;
}

// 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.
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;
}

// 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");

/// <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: "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");
}
Loading