Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ CSWINRT2013 | WindowsRuntime.SourceGenerator | Warning | Invalid 'ContractVersio
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'
CSWINRT2017 | WindowsRuntime.SourceGenerator | Warning | Public authored type mixing '[ContractVersion]' and '[Version]'
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace WindowsRuntime.SourceGenerator.Diagnostics;

/// <summary>
/// A diagnostic analyzer that validates uses of <c>[WindowsRuntimeNativeExposedType]</c>. The attribute is only
/// meaningful when applied with a concrete, non generic projected Windows Runtime class type, as CCW marshalling
/// code is generated automatically for every other kind of type. It also reports redundant applications of the
/// attribute that target a type already used by another application in the same assembly.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class WindowsRuntimeNativeExposedTypeAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
[
DiagnosticDescriptors.NativeExposedTypeNotInstantiable,
DiagnosticDescriptors.NativeExposedTypeNotProjectedClass,
DiagnosticDescriptors.NativeExposedTypeDuplicate
];

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

context.RegisterCompilationAction(static context =>
{
// Get the '[WindowsRuntimeNativeExposedType]' symbol
if (context.Compilation.GetTypeByMetadataName("WindowsRuntime.InteropServices.WindowsRuntimeNativeExposedTypeAttribute") is not { } nativeExposedTypeAttributeType)
{
return;
}

// Get the '[WindowsRuntimeMetadata]' symbol, which is used to detect projected Windows Runtime types
if (context.Compilation.GetTypeByMetadataName("WindowsRuntime.WindowsRuntimeMetadataAttribute") is not { } windowsRuntimeMetadataAttributeType)
{
return;
}

List<ITypeSymbol> validTargetTypes = [];

// Collect the valid target types across all applications of the attribute in the assembly (including
// the ones in generated code), so that a type used both in user code and in generated code is still
// detected as a duplicate. This list is only used to report duplicate applications of the attribute.
foreach (AttributeData attribute in context.Compilation.Assembly.GetAttributes(nativeExposedTypeAttributeType))
{
if (attribute is { ConstructorArguments: [{ Value: ITypeSymbol targetType }] } &&
Classify(targetType, windowsRuntimeMetadataAttributeType) is NativeExposedTypeKind.Valid)
{
validTargetTypes.Add(targetType);
}
}

// Classify and report each application of the attribute. Applications in generated code are
// suppressed by the analysis framework, because this analyzer opts out of generated code analysis,
// so only applications authored in user code are ever reported.
foreach (AttributeData attribute in context.Compilation.Assembly.GetAttributes(nativeExposedTypeAttributeType))
{
// Skip malformed applications, eg. where the argument does not bind to a type
if (attribute is not { ConstructorArguments: [{ Value: ITypeSymbol targetType }] })
{
continue;
}

Location? location = attribute.GetArgumentLocation(0, context.CancellationToken) ?? attribute.GetLocation(context.CancellationToken);

NativeExposedTypeKind kind = Classify(targetType, windowsRuntimeMetadataAttributeType);

// The type cannot be instantiated, so no CCW would ever be created for it
if (kind is NativeExposedTypeKind.NotInstantiable)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.NativeExposedTypeNotInstantiable,
location,
targetType));
}
else if (kind is NativeExposedTypeKind.NotProjectedClass)
{
// The type is not a projected class, so CCW marshalling code is already generated for it
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.NativeExposedTypeNotProjectedClass,
location,
targetType));
}
else if (CountOccurrences(validTargetTypes, targetType) > 1)
{
// The type is valid, but it is also targeted by another application of the attribute
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.NativeExposedTypeDuplicate,
location,
targetType));
}
}
});
}

/// <summary>
/// Classifies a target type used with <c>[WindowsRuntimeNativeExposedType]</c>.
/// </summary>
/// <param name="type">The target type to classify.</param>
/// <param name="windowsRuntimeMetadataAttributeType">The <c>[WindowsRuntimeMetadata]</c> symbol.</param>
/// <returns>The <see cref="NativeExposedTypeKind"/> classification for <paramref name="type"/>.</returns>
private static NativeExposedTypeKind Classify(ITypeSymbol type, INamedTypeSymbol windowsRuntimeMetadataAttributeType)
{
// A type that cannot be instantiated can never have a CCW created for it, so the attribute is meaningless
if (!IsInstantiable(type))
{
return NativeExposedTypeKind.NotInstantiable;
}

// A valid target is a non generic projected Windows Runtime class. CCW marshalling code is generated
// automatically for every other kind of type, so the attribute is only ever needed for projected classes.
if (type is not INamedTypeSymbol { TypeKind: TypeKind.Class, IsGenericType: false } namedType ||
!namedType.HasAttributeWithType(windowsRuntimeMetadataAttributeType))
{
return NativeExposedTypeKind.NotProjectedClass;
}

// The type is a valid, non-generic and projected runtime class
return NativeExposedTypeKind.Valid;
}

/// <summary>
/// Counts how many times a given type appears in a sequence of types.
/// </summary>
/// <param name="types">The sequence of types to search.</param>
/// <param name="type">The type to count occurrences of.</param>
/// <returns>The number of times <paramref name="type"/> appears in <paramref name="types"/>.</returns>
private static int CountOccurrences(IEnumerable<ITypeSymbol> types, ITypeSymbol type)
{
int count = 0;

foreach (ITypeSymbol candidate in types)
{
if (SymbolEqualityComparer.Default.Equals(candidate, type))
{
count++;
}
}

return count;
}

/// <summary>
/// Checks whether a given type can be instantiated.
/// </summary>
/// <param name="type">The type to check.</param>
/// <returns>Whether <paramref name="type"/> can be instantiated.</returns>
private static bool IsInstantiable(ITypeSymbol type)
{
// Array types can always be instantiated
if (type is IArrayTypeSymbol)
{
return true;
}

// Any other non named type (eg. a pointer type or a type parameter) cannot be instantiated
if (type is not INamedTypeSymbol namedType)
{
return false;
}

// An unbound generic type definition (eg. 'typeof(List<>)') cannot be instantiated
if (namedType.IsUnboundGenericType)
{
return false;
}

// Interfaces, abstract classes, and static classes cannot be instantiated
if (namedType.TypeKind is TypeKind.Interface || namedType.IsAbstract || namedType.IsStatic)
{
return false;
}

// Classes, structs, enums, and delegates can be instantiated
return namedType.TypeKind is TypeKind.Class or TypeKind.Struct or TypeKind.Enum or TypeKind.Delegate;
}

/// <summary>
/// The classification of a target type used with <c>[WindowsRuntimeNativeExposedType]</c>.
/// </summary>
private enum NativeExposedTypeKind
{
/// <summary>
/// The target type cannot be instantiated (eg. an interface, an abstract class, or a generic type definition).
/// </summary>
NotInstantiable,

/// <summary>
/// The target type can be instantiated, but it is not a projected Windows Runtime class.
/// </summary>
NotProjectedClass,

/// <summary>
/// The target type is a valid, non generic projected Windows Runtime class.
/// </summary>
Valid
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -244,4 +244,46 @@ internal static partial class DiagnosticDescriptors
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");

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> for a <c>[WindowsRuntimeNativeExposedType]</c> attribute applied with a target type that cannot be instantiated.
/// </summary>
public static readonly DiagnosticDescriptor NativeExposedTypeNotInstantiable = new(
id: "CSWINRT2018",
title: "'[WindowsRuntimeNativeExposedType]' target type cannot be instantiated",
messageFormat: """The type '{0}' is used as the target of a '[WindowsRuntimeNativeExposedType]' attribute, but it cannot be instantiated. Only concrete, non generic projected Windows Runtime class types can be used with this attribute, as no CCW would ever be created for a type that cannot be instantiated.""",
category: "WindowsRuntime.SourceGenerator",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "The '[WindowsRuntimeNativeExposedType]' attribute should only be applied with concrete, non generic projected Windows Runtime class types. Types that cannot be instantiated, such as interfaces, abstract classes, static classes, and generic type definitions, have no effect when used as the target of the attribute.",
helpLinkUri: "https://github.com/microsoft/CsWinRT",
customTags: WellKnownDiagnosticTags.CompilationEnd);

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> for a <c>[WindowsRuntimeNativeExposedType]</c> attribute applied with a target type that is not a projected Windows Runtime class.
/// </summary>
public static readonly DiagnosticDescriptor NativeExposedTypeNotProjectedClass = new(
id: "CSWINRT2019",
title: "'[WindowsRuntimeNativeExposedType]' target type is not a projected class",
messageFormat: """The type '{0}' is used as the target of a '[WindowsRuntimeNativeExposedType]' attribute, but it is not a projected Windows Runtime class type. CCW marshalling code is already generated automatically for all other types (such as user defined types, value types, delegates, arrays, and generic instantiations), so applying the attribute to '{0}' has no effect.""",
category: "WindowsRuntime.SourceGenerator",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "The '[WindowsRuntimeNativeExposedType]' attribute should only be applied with projected Windows Runtime class types. CCW marshalling code is generated automatically for all other types, so applying the attribute to them has no effect.",
helpLinkUri: "https://github.com/microsoft/CsWinRT",
customTags: WellKnownDiagnosticTags.CompilationEnd);

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> for a <c>[WindowsRuntimeNativeExposedType]</c> attribute applied with a target type that is already used by another application of the attribute in the same assembly.
/// </summary>
public static readonly DiagnosticDescriptor NativeExposedTypeDuplicate = new(
id: "CSWINRT2020",
title: "Duplicate '[WindowsRuntimeNativeExposedType]' target type",
messageFormat: """The type '{0}' is used as the target of more than one '[WindowsRuntimeNativeExposedType]' attribute in the same assembly. CCW marshalling code is only generated once per type, so the additional applications of the attribute for '{0}' have no effect and can be removed.""",
category: "WindowsRuntime.SourceGenerator",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>$(FunctionalTestsBuildTFMs)</TargetFrameworks>
<Platforms>x86;x64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
<PublishProfileFullPath>$(MSBuildProjectDirectory)\..\PublishProfiles\win-$(Platform).pubxml</PublishProfileFullPath>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\Authoring\WinRT.SourceGenerator2\WinRT.SourceGenerator2.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" SetPlatform="Platform=AnyCPU" />
<ProjectReference Include="..\..\..\Projections\Windows\Windows.csproj" />
</ItemGroup>

</Project>
49 changes: 49 additions & 0 deletions src/Tests/FunctionalTests/NativeExposedType/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Windows.Data.Json;
using WindowsRuntime.InteropServices;

#pragma warning disable CSWINRT3001 // Type or member is obsolete

// The interop generator normally skips projected types when generating CCW marshalling code, as they are backed
// by native objects and never need to be exposed to native code through a CCW. Opting 'JsonArray' in via the
// '[WindowsRuntimeNativeExposedType]' attribute forces the interop generator to also emit CCW marshalling code
// for it, just like it does for any user defined type. This registers a proxy type map association for the type,
// which the checks below then verify is present (and absent for a projected type that was not opted in).
[assembly: WindowsRuntimeNativeExposedType(typeof(JsonArray))]

namespace NativeExposedType;

internal static class Program
{
private static int Main()
{
IReadOnlyDictionary<Type, Type> proxyTypeMapping = GetComWrappersProxyTypeMapping();

// 'JsonArray' was explicitly opted into CCW marshalling code generation, so the interop generator must
// have generated a proxy for it and registered the associated proxy type map entry.
if (!proxyTypeMapping.TryGetValue(typeof(JsonArray), out _))
{
return 101;
}

// 'JsonObject' is a projected type that was not opted into CCW marshalling code generation. The interop
// generator must have skipped it, as projected types are backed by native objects and never need CCW
// marshalling code generated for them by default.
if (proxyTypeMapping.TryGetValue(typeof(JsonObject), out _))
{
return 102;
}

return 100;
}

// Retrieves the proxy type map used by CsWinRT to resolve CCW marshalling info for managed objects
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The proxy type map is always preserved by the interop generator.")]
private static IReadOnlyDictionary<Type, Type> GetComWrappersProxyTypeMapping()
{
return TypeMapping.GetOrCreateProxyTypeMapping<WindowsRuntimeComWrappersTypeMapGroup>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,14 @@ protected override ParseOptions CreateParseOptions()
/// <param name="allowUnsafeBlocks">Whether to enable unsafe blocks.</param>
/// <param name="languageVersion">The language version to use to run the test.</param>
/// <param name="isCsWinRTComponent">Whether to set the <c>"CsWinRTComponent"</c> MSBuild property to <see langword="true"/>.</param>
/// <param name="generatedSource">An additional source file to add to the compilation as generated code, or <see langword="null"/> to not add one.</param>
public static Task VerifyAnalyzerAsync(
string source,
ReadOnlySpan<DiagnosticResult> expectedDiagnostics = default,
bool allowUnsafeBlocks = true,
LanguageVersion languageVersion = LanguageVersion.CSharp14,
bool isCsWinRTComponent = false)
bool isCsWinRTComponent = false,
string generatedSource = null)
{
CSharpAnalyzerTest<TAnalyzer> test = new(allowUnsafeBlocks, languageVersion) { TestCode = source };

Expand All @@ -75,6 +77,14 @@ public static Task VerifyAnalyzerAsync(
test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Button).Assembly.Location));
test.TestState.ExpectedDiagnostics.AddRange([.. expectedDiagnostics]);

// Add an additional source file that is treated as generated code (the '.g.cs' suffix is recognized by the
// analysis framework). This is used to validate that diagnostics are suppressed for applications of the
// attribute that appear in generated code, when the analyzer opts out of generated code analysis.
if (generatedSource is not null)
{
test.TestState.Sources.Add(("NativeExposedTypes.g.cs", generatedSource));
}

// Configure the desired MSBuild properties via a global analyzer config file
if (isCsWinRTComponent)
{
Expand Down
Loading