Skip to content

Commit 4d52032

Browse files
authored
Add analyzers and code-fixes to help adoption of source-generated COM (#87223)
* Add first pass of the "convert to generated COM interface" analyzer and add tests for the various other analyzers we are going to introduce. * Get all analyzer-specific components of the "convert" tests passing. * Implement all interface-attribute-level changes in the code fixer. * Add bool marshalling insertion logic to the fixer. * Add support for removing shadowing members from interfaces. * Rename fixer * ActiveIssue the new tests * Implement basic AddGeneratedComClass analyzer/fixer * Add ComHosting + GeneratedComInterface analyzer implementation. * Implement the "runtime COM APIs with source-generated COM types" analyzer. * Factor out a base class from the ConvertToLibraryImportFixer so we can share it with the ComInterfaceGenerator-family of fixers. * Move more of the ConvertToLibraryImportFixer to use SyntaxGenerator APIs instead of dropping to C#-specific syntax APIs (improves consistency throughout our code fixes) * Move support for specifying explicit boolean marshalling rules up to the base class. * Move the code fixes in ComInterfaceGenerator over to using the ConvertToSourceGeneratedInteropFixer base type. * Remove use of multicasted delegates and use a more traditional "array of delegates" model. * Do some refactoring to move more into the new fixer base class. * Remove custom CodeAction-derived types now that we have a record type to represent fixes from the subclasses. * Make sure we make types and containing types partial * Fix negative test. * Add tests for transitive interface inheritance and add iids. * Change bool parsing for internal parsing and add warning annotation text. Update diagnostics list md.
1 parent d270a4d commit 4d52032

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+3307
-316
lines changed

docs/project/list-of-diagnostics.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,10 @@ The diagnostic id values reserved for .NET Libraries analyzer warnings are `SYSL
212212
| __`SYSLIB1093`__ | _`SYSLIB1092`-`SYSLIB1099` reserved for Microsoft.Interop.ComInteropGenerator._ |
213213
| __`SYSLIB1094`__ | _`SYSLIB1092`-`SYSLIB1099` reserved for Microsoft.Interop.ComInteropGenerator._ |
214214
| __`SYSLIB1095`__ | _`SYSLIB1092`-`SYSLIB1099` reserved for Microsoft.Interop.ComInteropGenerator._ |
215-
| __`SYSLIB1096`__ | _`SYSLIB1092`-`SYSLIB1099` reserved for Microsoft.Interop.ComInteropGenerator._ |
216-
| __`SYSLIB1097`__ | _`SYSLIB1092`-`SYSLIB1099` reserved for Microsoft.Interop.ComInteropGenerator._ |
217-
| __`SYSLIB1098`__ | _`SYSLIB1092`-`SYSLIB1099` reserved for Microsoft.Interop.ComInteropGenerator._ |
218-
| __`SYSLIB1099`__ | _`SYSLIB1092`-`SYSLIB1099` reserved for Microsoft.Interop.ComInteropGenerator._ |
215+
| __`SYSLIB1096`__ | Use 'GeneratedComInterfaceAttribute' instead of 'ComImportAttribute' to generate COM marshalling code at compile time |
216+
| __`SYSLIB1097`__ | This type implements at least one type with the 'GeneratedComInterfaceAttribute' attribute. Add the 'GeneratedComClassAttribute' to enable passing this type to COM and exposing the COM interfaces for the types with the 'GeneratedComInterfaceAttribute' from objects of this type. |
217+
| __`SYSLIB1098`__ | .NET COM hosting with 'EnableComHosting' only supports built-in COM interop. It does not support source-generated COM interop with 'GeneratedComInterfaceAttribute'. |
218+
| __`SYSLIB1099`__ | COM Interop APIs on 'System.Runtime.InteropServices.Marshal' do not support source-generated COM and will fail at runtime |
219219
| __`SYSLIB1100`__ | Configuration binding generator: type is not supported. |
220220
| __`SYSLIB1101`__ | Configuration binding generator: property on type is not supported. |
221221
| __`SYSLIB1102`__ | Configuration binding generator: project's language version must be at least C# 11.|
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Immutable;
5+
using System.Linq;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
using Microsoft.CodeAnalysis.DotnetRuntime.Extensions;
9+
using static Microsoft.Interop.Analyzers.AnalyzerDiagnostics;
10+
11+
namespace Microsoft.Interop.Analyzers
12+
{
13+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
14+
public class AddGeneratedComClassAnalyzer : DiagnosticAnalyzer
15+
{
16+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(AddGeneratedComClassAttribute);
17+
18+
public override void Initialize(AnalysisContext context)
19+
{
20+
context.EnableConcurrentExecution();
21+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
22+
23+
context.RegisterCompilationStartAction(context =>
24+
{
25+
var generatedComClassAttributeType = context.Compilation.GetBestTypeByMetadataName(TypeNames.GeneratedComClassAttribute);
26+
var generatedComInterfaceAttributeType = context.Compilation.GetBestTypeByMetadataName(TypeNames.GeneratedComInterfaceAttribute);
27+
28+
if (generatedComClassAttributeType is null || generatedComInterfaceAttributeType is null)
29+
{
30+
return;
31+
}
32+
33+
context.RegisterSymbolAction(context =>
34+
{
35+
INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol;
36+
if (type.GetAttributes().Any(attr => generatedComClassAttributeType.Equals(attr.AttributeClass, SymbolEqualityComparer.Default)))
37+
{
38+
return;
39+
}
40+
41+
// Only direct people to put the GeneratedComClassAttribute on classes.
42+
if (type.TypeKind != TypeKind.Class)
43+
{
44+
return;
45+
}
46+
47+
foreach (var iface in type.AllInterfaces)
48+
{
49+
if (iface.GetAttributes().Any(attr => generatedComInterfaceAttributeType.Equals(attr.AttributeClass, SymbolEqualityComparer.Default)))
50+
{
51+
context.ReportDiagnostic(type.CreateDiagnostic(AddGeneratedComClassAttribute, type.Name));
52+
return;
53+
}
54+
}
55+
}, SymbolKind.NamedType);
56+
});
57+
}
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Immutable;
6+
using System.Composition;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CodeFixes;
11+
using Microsoft.CodeAnalysis.DotnetRuntime.Extensions;
12+
using Microsoft.CodeAnalysis.Editing;
13+
using Microsoft.CodeAnalysis.Simplification;
14+
15+
namespace Microsoft.Interop.Analyzers
16+
{
17+
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
18+
public class AddGeneratedComClassFixer : ConvertToSourceGeneratedInteropFixer
19+
{
20+
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(AnalyzerDiagnostics.Ids.AddGeneratedComClassAttribute);
21+
22+
protected override string BaseEquivalenceKey => nameof(AddGeneratedComClassFixer);
23+
24+
private static Task AddGeneratedComClassAsync(DocumentEditor editor, SyntaxNode node)
25+
{
26+
editor.ReplaceNode(node, (node, gen) =>
27+
{
28+
var attribute = gen.Attribute(gen.TypeExpression(editor.SemanticModel.Compilation.GetBestTypeByMetadataName(TypeNames.GeneratedComClassAttribute)).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation));
29+
var updatedNode = gen.AddAttributes(node, attribute);
30+
var declarationModifiers = gen.GetModifiers(updatedNode);
31+
if (!declarationModifiers.IsPartial)
32+
{
33+
updatedNode = gen.WithModifiers(updatedNode, declarationModifiers.WithPartial(true));
34+
}
35+
return updatedNode;
36+
});
37+
38+
MakeNodeParentsPartial(editor, node);
39+
40+
return Task.CompletedTask;
41+
}
42+
43+
protected override Func<DocumentEditor, CancellationToken, Task> CreateFixForSelectedOptions(SyntaxNode node, ImmutableDictionary<string, Option> selectedOptions)
44+
{
45+
return (editor, _) => AddGeneratedComClassAsync(editor, node);
46+
}
47+
48+
protected override string GetDiagnosticTitle(ImmutableDictionary<string, Option> selectedOptions)
49+
{
50+
bool allowUnsafe = selectedOptions.TryGetValue(Option.AllowUnsafe, out var allowUnsafeOption) && allowUnsafeOption is Option.Bool(true);
51+
52+
return allowUnsafe
53+
? SR.AddGeneratedComClassAttributeTitle
54+
: SR.AddGeneratedComClassAddUnsafe;
55+
}
56+
57+
protected override ImmutableDictionary<string, Option> ParseOptionsFromDiagnostic(Diagnostic diagnostic)
58+
{
59+
return ImmutableDictionary<string, Option>.Empty;
60+
}
61+
}
62+
}

src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/Analyzers/AnalyzerDiagnostics.cs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,19 @@ public static class Ids
1111
{
1212
public const string Prefix = "SYSLIB";
1313
public const string InvalidGeneratedComAttributeUsage = Prefix + "1090";
14+
public const string ConvertToGeneratedComInterface = Prefix + "1096";
15+
public const string AddGeneratedComClassAttribute = Prefix + "1097";
16+
public const string ComHostingDoesNotSupportGeneratedComInterface = Prefix + "1098";
17+
public const string RuntimeComApisDoNotSupportSourceGeneratedCom = Prefix + "1099";
1418
}
1519

16-
private const string Category = "ComInterfaceGenerator";
20+
public static class Metadata
21+
{
22+
public const string MayRequireAdditionalWork = nameof(MayRequireAdditionalWork);
23+
public const string AddStringMarshalling = nameof(AddStringMarshalling);
24+
}
25+
26+
private const string Category = "Interoperability";
1727

1828
private static LocalizableResourceString GetResourceString(string resourceName)
1929
{
@@ -29,5 +39,45 @@ private static LocalizableResourceString GetResourceString(string resourceName)
2939
DiagnosticSeverity.Error,
3040
isEnabledByDefault: true,
3141
description: GetResourceString(nameof(SR.InterfaceTypeNotSupportedMessage)));
42+
43+
public static readonly DiagnosticDescriptor ConvertToGeneratedComInterface =
44+
new DiagnosticDescriptor(
45+
Ids.ConvertToGeneratedComInterface,
46+
GetResourceString(nameof(SR.ConvertToGeneratedComInterfaceTitle)),
47+
GetResourceString(nameof(SR.ConvertToGeneratedComInterfaceMessage)),
48+
Category,
49+
DiagnosticSeverity.Info,
50+
isEnabledByDefault: true,
51+
description: GetResourceString(nameof(SR.ConvertToGeneratedComInterfaceDescription)));
52+
53+
public static readonly DiagnosticDescriptor AddGeneratedComClassAttribute =
54+
new DiagnosticDescriptor(
55+
Ids.AddGeneratedComClassAttribute,
56+
GetResourceString(nameof(SR.AddGeneratedComClassAttributeTitle)),
57+
GetResourceString(nameof(SR.AddGeneratedComClassAttributeMessage)),
58+
Category,
59+
DiagnosticSeverity.Info,
60+
isEnabledByDefault: true,
61+
description: GetResourceString(nameof(SR.AddGeneratedComClassAttributeDescription)));
62+
63+
public static readonly DiagnosticDescriptor ComHostingDoesNotSupportGeneratedComInterface =
64+
new DiagnosticDescriptor(
65+
Ids.ComHostingDoesNotSupportGeneratedComInterface,
66+
GetResourceString(nameof(SR.ComHostingDoesNotSupportGeneratedComInterfaceTitle)),
67+
GetResourceString(nameof(SR.ComHostingDoesNotSupportGeneratedComInterfaceMessage)),
68+
Category,
69+
DiagnosticSeverity.Warning,
70+
isEnabledByDefault: true,
71+
description: GetResourceString(nameof(SR.ComHostingDoesNotSupportGeneratedComInterfaceDescription)));
72+
73+
public static readonly DiagnosticDescriptor RuntimeComApisDoNotSupportSourceGeneratedCom =
74+
new DiagnosticDescriptor(
75+
Ids.RuntimeComApisDoNotSupportSourceGeneratedCom,
76+
GetResourceString(nameof(SR.RuntimeComApisDoNotSupportSourceGeneratedComTitle)),
77+
GetResourceString(nameof(SR.RuntimeComApisDoNotSupportSourceGeneratedComMessage)),
78+
Category,
79+
DiagnosticSeverity.Warning,
80+
isEnabledByDefault: true,
81+
description: GetResourceString(nameof(SR.RuntimeComApisDoNotSupportSourceGeneratedComDescription)));
3282
}
3383
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Immutable;
5+
using System.Linq;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
using Microsoft.CodeAnalysis.DotnetRuntime.Extensions;
9+
using Microsoft.CodeAnalysis.Operations;
10+
using static Microsoft.Interop.Analyzers.AnalyzerDiagnostics;
11+
12+
namespace Microsoft.Interop.Analyzers
13+
{
14+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
15+
public class ComHostingDoesNotSupportGeneratedComInterfaceAnalyzer : DiagnosticAnalyzer
16+
{
17+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(ComHostingDoesNotSupportGeneratedComInterface);
18+
19+
public override void Initialize(AnalysisContext context)
20+
{
21+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
22+
context.EnableConcurrentExecution();
23+
context.RegisterCompilationStartAction(context =>
24+
{
25+
if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.EnableComHosting", out string? enableComHosting)
26+
|| !bool.TryParse(enableComHosting, out bool enableComHostingValue)
27+
|| !enableComHostingValue)
28+
{
29+
return;
30+
}
31+
32+
INamedTypeSymbol? generatedComClassAttribute = context.Compilation.GetBestTypeByMetadataName(TypeNames.GeneratedComClassAttribute);
33+
INamedTypeSymbol? generatedComInterfaceAttribute = context.Compilation.GetBestTypeByMetadataName(TypeNames.GeneratedComInterfaceAttribute);
34+
INamedTypeSymbol? comVisibleAttribute = context.Compilation.GetBestTypeByMetadataName(TypeNames.System_Runtime_InteropServices_ComVisibleAttribute)!;
35+
36+
if (generatedComClassAttribute is null || generatedComInterfaceAttribute is null || comVisibleAttribute is null)
37+
{
38+
return;
39+
}
40+
41+
context.RegisterOperationAction(context =>
42+
{
43+
IAttributeOperation attr = (IAttributeOperation)context.Operation;
44+
if (attr.Operation is not IObjectCreationOperation ctor
45+
|| !comVisibleAttribute.Equals(ctor.Type, SymbolEqualityComparer.Default)
46+
|| ctor.Arguments[0].Value.ConstantValue.Value is not true)
47+
{
48+
return;
49+
}
50+
51+
INamedTypeSymbol containingType = (INamedTypeSymbol)context.ContainingSymbol;
52+
53+
if (containingType.GetAttributes().Any(attr => generatedComClassAttribute.Equals(attr.AttributeClass, SymbolEqualityComparer.Default)))
54+
{
55+
context.ReportDiagnostic(context.ContainingSymbol.CreateDiagnostic(ComHostingDoesNotSupportGeneratedComInterface, context.ContainingSymbol.Name));
56+
return;
57+
}
58+
59+
foreach (var iface in containingType.AllInterfaces)
60+
{
61+
if (iface.GetAttributes().Any(attr => generatedComInterfaceAttribute.Equals(attr.AttributeClass, SymbolEqualityComparer.Default)))
62+
{
63+
context.ReportDiagnostic(context.ContainingSymbol.CreateDiagnostic(ComHostingDoesNotSupportGeneratedComInterface, context.ContainingSymbol.Name));
64+
return;
65+
}
66+
}
67+
}, OperationKind.Attribute);
68+
});
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)