Skip to content

Commit

Permalink
Add fixer for converting to GeneratedDllImport (dotnet/runtimelab#564)
Browse files Browse the repository at this point in the history
Commit migrated from dotnet/runtimelab@dd81061
  • Loading branch information
elinor-fung authored Jan 20, 2021
1 parent 5a89cbb commit 7d5caa0
Show file tree
Hide file tree
Showing 8 changed files with 679 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;

using static Microsoft.Interop.Analyzers.AnalyzerDiagnostics;

namespace Microsoft.Interop.Analyzers
{
[ExportCodeFixProvider(LanguageNames.CSharp)]
public sealed class ConvertToGeneratedDllImportFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(Ids.ConvertToGeneratedDllImport);

public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public const string NoPreprocessorDefinesKey = "ConvertToGeneratedDllImport";
public const string WithPreprocessorDefinesKey = "ConvertToGeneratedDllImportPreprocessor";

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
// Get the syntax root and semantic model
Document doc = context.Document;
SyntaxNode? root = await doc.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
SemanticModel? model = await doc.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
if (root == null || model == null)
return;

// Nothing to do if the GeneratedDllImportAttribute is not in the compilation
INamedTypeSymbol? generatedDllImportAttrType = model.Compilation.GetTypeByMetadataName(TypeNames.GeneratedDllImportAttribute);
if (generatedDllImportAttrType == null)
return;

INamedTypeSymbol? dllImportAttrType = model.Compilation.GetTypeByMetadataName(typeof(DllImportAttribute).FullName);
if (dllImportAttrType == null)
return;

// Get the syntax node tied to the diagnostic and check that it is a method declaration
if (root.FindNode(context.Span) is not MethodDeclarationSyntax methodSyntax)
return;

if (model.GetDeclaredSymbol(methodSyntax, context.CancellationToken) is not IMethodSymbol methodSymbol)
return;

// Make sure the method has the DllImportAttribute
AttributeData? dllImportAttr;
if (!TryGetAttribute(methodSymbol, dllImportAttrType, out dllImportAttr))
return;

// Register code fixes with two options for the fix - using preprocessor or not.
context.RegisterCodeFix(
CodeAction.Create(
Resources.ConvertToGeneratedDllImportNoPreprocessor,
cancelToken => ConvertToGeneratedDllImport(
context.Document,
methodSyntax,
methodSymbol,
dllImportAttr!,
generatedDllImportAttrType,
usePreprocessorDefines: false,
cancelToken),
equivalenceKey: NoPreprocessorDefinesKey),
context.Diagnostics);

context.RegisterCodeFix(
CodeAction.Create(
Resources.ConvertToGeneratedDllImportWithPreprocessor,
cancelToken => ConvertToGeneratedDllImport(
context.Document,
methodSyntax,
methodSymbol,
dllImportAttr!,
generatedDllImportAttrType,
usePreprocessorDefines: true,
cancelToken),
equivalenceKey: WithPreprocessorDefinesKey),
context.Diagnostics);
}

private async Task<Document> ConvertToGeneratedDllImport(
Document doc,
MethodDeclarationSyntax methodSyntax,
IMethodSymbol methodSymbol,
AttributeData dllImportAttr,
INamedTypeSymbol generatedDllImportAttrType,
bool usePreprocessorDefines,
CancellationToken cancellationToken)
{
DocumentEditor editor = await DocumentEditor.CreateAsync(doc, cancellationToken).ConfigureAwait(false);
SyntaxGenerator generator = editor.Generator;

var dllImportSyntax = (AttributeSyntax)dllImportAttr!.ApplicationSyntaxReference!.GetSyntax(cancellationToken);

// Create GeneratedDllImport attribute based on the DllImport attribute
var generatedDllImportSyntax = GetGeneratedDllImportAttribute(
generator,
dllImportSyntax,
methodSymbol.GetDllImportData()!,
generatedDllImportAttrType);

// Add annotation about potential behavioural and compatibility changes
generatedDllImportSyntax = generatedDllImportSyntax.WithAdditionalAnnotations(
WarningAnnotation.Create(string.Format(Resources.ConvertToGeneratedDllImportWarning, "[TODO] Documentation link")));

// Replace DllImport with GeneratedDllImport
SyntaxNode generatedDeclaration = generator.ReplaceNode(methodSyntax, dllImportSyntax, generatedDllImportSyntax);

// Replace extern keyword with partial keyword
generatedDeclaration = generator.WithModifiers(
generatedDeclaration,
generator.GetModifiers(methodSyntax)
.WithIsExtern(false)
.WithPartial(true));

if (!usePreprocessorDefines)
{
// Replace the original method with the updated one
editor.ReplaceNode(methodSyntax, generatedDeclaration);
}
else
{
// #if NET
generatedDeclaration = generatedDeclaration.WithLeadingTrivia(
generatedDeclaration.GetLeadingTrivia()
.AddRange(new[] {
SyntaxFactory.Trivia(SyntaxFactory.IfDirectiveTrivia(SyntaxFactory.IdentifierName("NET"), isActive: true, branchTaken: true, conditionValue: true)),
SyntaxFactory.ElasticMarker
}));

// #else
generatedDeclaration = generatedDeclaration.WithTrailingTrivia(
generatedDeclaration.GetTrailingTrivia()
.AddRange(new[] {
SyntaxFactory.Trivia(SyntaxFactory.ElseDirectiveTrivia(isActive: false, branchTaken: false)),
SyntaxFactory.ElasticMarker
}));

// Remove existing leading trivia - it will be on the GeneratedDllImport method
var updatedDeclaration = methodSyntax.WithLeadingTrivia();

// #endif
updatedDeclaration = updatedDeclaration.WithTrailingTrivia(
methodSyntax.GetTrailingTrivia()
.AddRange(new[] {
SyntaxFactory.Trivia(SyntaxFactory.EndIfDirectiveTrivia(isActive: true)),
SyntaxFactory.ElasticMarker
}));

// Add the GeneratedDllImport method
editor.InsertBefore(methodSyntax, generatedDeclaration);

// Replace the original method with the updated DllImport method
editor.ReplaceNode(methodSyntax, updatedDeclaration);
}

return editor.GetChangedDocument();
}

private SyntaxNode GetGeneratedDllImportAttribute(
SyntaxGenerator generator,
AttributeSyntax dllImportSyntax,
DllImportData dllImportData,
INamedTypeSymbol generatedDllImportAttrType)
{
// Create GeneratedDllImport based on the DllImport attribute
var generatedDllImportSyntax = generator.ReplaceNode(dllImportSyntax,
dllImportSyntax.Name,
generator.TypeExpression(generatedDllImportAttrType));

// Update attribute arguments for GeneratedDllImport
List<SyntaxNode> argumentsToRemove = new List<SyntaxNode>();
foreach (SyntaxNode argument in generator.GetAttributeArguments(generatedDllImportSyntax))
{
if (argument is not AttributeArgumentSyntax attrArg)
continue;

if (dllImportData.BestFitMapping != null
&& !dllImportData.BestFitMapping.Value
&& IsMatchingNamedArg(attrArg, nameof(DllImportAttribute.BestFitMapping)))
{
// BestFitMapping=false is explicitly set
// GeneratedDllImport does not support setting BestFitMapping. The generated code
// has the equivalent behaviour of BestFitMapping=false, so we can remove the argument.
argumentsToRemove.Add(argument);
}
else if (dllImportData.ThrowOnUnmappableCharacter != null
&& !dllImportData.ThrowOnUnmappableCharacter.Value
&& IsMatchingNamedArg(attrArg, nameof(DllImportAttribute.ThrowOnUnmappableChar)))
{
// ThrowOnUnmappableChar=false is explicitly set
// GeneratedDllImport does not support setting ThrowOnUnmappableChar. The generated code
// has the equivalent behaviour of ThrowOnUnmappableChar=false, so we can remove the argument.
argumentsToRemove.Add(argument);
}
}

return generator.RemoveNodes(generatedDllImportSyntax, argumentsToRemove);
}

private static bool TryGetAttribute(IMethodSymbol method, INamedTypeSymbol attributeType, out AttributeData? attr)
{
attr = default;
foreach (var attrLocal in method.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(attrLocal.AttributeClass, attributeType))
{
attr = attrLocal;
return true;
}
}

return false;
}

private static bool IsMatchingNamedArg(AttributeArgumentSyntax arg, string nameToMatch)
{
return arg.NameEquals != null && arg.NameEquals.Name.Identifier.Text == nameToMatch;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(CompilerPlatformVersion)" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(CompilerPlatformVersion)" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.0" PrivateAssets="all" />
</ItemGroup>

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,19 @@
<data name="ConvertToGeneratedDllImportMessage" xml:space="preserve">
<value>Mark the method '{0}' with 'GeneratedDllImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time</value>
</data>
<data name="ConvertToGeneratedDllImportNoPreprocessor" xml:space="preserve">
<value>Convert to 'GeneratedDllImport'</value>
</data>
<data name="ConvertToGeneratedDllImportTitle" xml:space="preserve">
<value>Use 'GeneratedDllImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time</value>
</data>
<data name="ConvertToGeneratedDllImportWarning" xml:space="preserve">
<value>Conversion to 'GeneratedDllImport' may change behavior and compatibility. See {0} for more information.</value>
<comment>{0} is a documentation link</comment>
</data>
<data name="ConvertToGeneratedDllImportWithPreprocessor" xml:space="preserve">
<value>Convert to 'GeneratedDllImport' under a preprocessor define</value>
</data>
<data name="CustomTypeMarshallingManagedToNativeUnsupported" xml:space="preserve">
<value>The specified parameter needs to be marshalled from managed to native, but the native type '{0}' does not support it.</value>
</data>
Expand Down
Loading

0 comments on commit 7d5caa0

Please sign in to comment.