forked from dotnet/runtime
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add analyzer to flag converting from DllImport to GeneratedDllImport (d…
…otnet/runtimelab#367) Commit migrated from dotnet/runtimelab@a7ccdd8
- Loading branch information
1 parent
4700722
commit 67e674b
Showing
8 changed files
with
358 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
125 changes: 125 additions & 0 deletions
125
...e.InteropServices/gen/DllImportGenerator/Analyzers/ConvertToGeneratedDllImportAnalyzer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
using System.Collections.Immutable; | ||
using System.Diagnostics; | ||
using System.Runtime.InteropServices; | ||
|
||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
|
||
using static Microsoft.Interop.Analyzers.AnalyzerDiagnostics; | ||
|
||
namespace Microsoft.Interop.Analyzers | ||
{ | ||
[DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
public class ConvertToGeneratedDllImportAnalyzer : DiagnosticAnalyzer | ||
{ | ||
private const string Category = "Interoperability"; | ||
|
||
public readonly static DiagnosticDescriptor ConvertToGeneratedDllImport = | ||
new DiagnosticDescriptor( | ||
Ids.ConvertToGeneratedDllImport, | ||
GetResourceString(nameof(Resources.ConvertToGeneratedDllImportTitle)), | ||
GetResourceString(nameof(Resources.ConvertToGeneratedDllImportMessage)), | ||
Category, | ||
DiagnosticSeverity.Info, | ||
isEnabledByDefault: false, | ||
description: GetResourceString(nameof(Resources.ConvertToGeneratedDllImportDescription))); | ||
|
||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(ConvertToGeneratedDllImport); | ||
|
||
public override void Initialize(AnalysisContext context) | ||
{ | ||
// Don't analyze generated code | ||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
context.EnableConcurrentExecution(); | ||
context.RegisterCompilationStartAction( | ||
compilationContext => | ||
{ | ||
// Nothing to do if the GeneratedDllImportAttribute is not in the compilation | ||
INamedTypeSymbol? generatedDllImportAttrType = compilationContext.Compilation.GetTypeByMetadataName(TypeNames.GeneratedDllImportAttribute); | ||
if (generatedDllImportAttrType == null) | ||
return; | ||
INamedTypeSymbol? dllImportAttrType = compilationContext.Compilation.GetTypeByMetadataName(typeof(DllImportAttribute).FullName); | ||
if (dllImportAttrType == null) | ||
return; | ||
compilationContext.RegisterSymbolAction(symbolContext => AnalyzeSymbol(symbolContext, dllImportAttrType), SymbolKind.Method); | ||
}); | ||
} | ||
|
||
private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbol dllImportAttrType) | ||
{ | ||
var method = (IMethodSymbol)context.Symbol; | ||
|
||
// Check if method is a DllImport | ||
DllImportData? dllImportData = method.GetDllImportData(); | ||
if (dllImportData == null) | ||
return; | ||
|
||
// Ignore QCalls | ||
if (dllImportData.ModuleName == "QCall") | ||
return; | ||
|
||
if (RequiresMarshalling(method, dllImportData, dllImportAttrType)) | ||
{ | ||
context.ReportDiagnostic(method.CreateDiagnostic(ConvertToGeneratedDllImport, method.Name)); | ||
} | ||
} | ||
|
||
private static bool RequiresMarshalling(IMethodSymbol method, DllImportData dllImportData, INamedTypeSymbol dllImportAttrType) | ||
{ | ||
// SetLastError=true requires marshalling | ||
if (dllImportData.SetLastError) | ||
return true; | ||
|
||
// Check if return value requires marshalling | ||
if (!method.ReturnsVoid && RequiresMarshalling(method.ReturnType)) | ||
return true; | ||
|
||
// Check if parameters require marshalling | ||
foreach (IParameterSymbol paramType in method.Parameters) | ||
{ | ||
if (paramType.RefKind != RefKind.None) | ||
return true; | ||
|
||
if (RequiresMarshalling(paramType.Type)) | ||
return true; | ||
} | ||
|
||
// DllImportData does not expose all information (e.g. PreserveSig), so we still need to get the attribute data | ||
AttributeData? dllImportAttr = null; | ||
foreach (AttributeData attr in method.GetAttributes()) | ||
{ | ||
if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, dllImportAttrType)) | ||
continue; | ||
|
||
dllImportAttr = attr; | ||
break; | ||
} | ||
|
||
Debug.Assert(dllImportAttr != null); | ||
foreach (var namedArg in dllImportAttr!.NamedArguments) | ||
{ | ||
if (namedArg.Key != nameof(DllImportAttribute.PreserveSig)) | ||
continue; | ||
|
||
// PreserveSig=false requires marshalling | ||
if (!(bool)namedArg.Value.Value!) | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
private static bool RequiresMarshalling(ITypeSymbol typeSymbol) | ||
{ | ||
if (typeSymbol.TypeKind == TypeKind.Enum) | ||
return false; | ||
|
||
if (typeSymbol.TypeKind == TypeKind.Pointer) | ||
return false; | ||
|
||
return !typeSymbol.IsConsideredBlittable(); | ||
} | ||
} | ||
} |
29 changes: 27 additions & 2 deletions
29
src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/Resources.Designer.cs
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
159 changes: 159 additions & 0 deletions
159
...opServices/tests/DllImportGenerator.UnitTests/ConvertToGeneratedDllImportAnalyzerTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Threading.Tasks; | ||
using Xunit; | ||
using static Microsoft.Interop.Analyzers.ConvertToGeneratedDllImportAnalyzer; | ||
|
||
using VerifyCS = DllImportGenerator.UnitTests.Verifiers.CSharpAnalyzerVerifier<Microsoft.Interop.Analyzers.ConvertToGeneratedDllImportAnalyzer>; | ||
|
||
namespace DllImportGenerator.UnitTests | ||
{ | ||
public class ConvertToGeneratedDllImportAnalyzerTests | ||
{ | ||
public static IEnumerable<object[]> MarshallingRequiredTypes() => new[] | ||
{ | ||
new object[] { typeof(bool) }, | ||
new object[] { typeof(char) }, | ||
new object[] { typeof(string) }, | ||
new object[] { typeof(int[]) }, | ||
new object[] { typeof(string[]) }, | ||
new object[] { typeof(ConsoleKeyInfo) }, // struct | ||
}; | ||
|
||
public static IEnumerable<object[]> NoMarshallingRequiredTypes() => new[] | ||
{ | ||
new object[] { typeof(byte) }, | ||
new object[] { typeof(int) }, | ||
new object[] { typeof(byte*) }, | ||
new object[] { typeof(int*) }, | ||
new object[] { typeof(bool*) }, | ||
new object[] { typeof(char*) }, | ||
new object[] { typeof(IntPtr) }, | ||
new object[] { typeof(ConsoleKey) }, // enum | ||
}; | ||
|
||
[Theory] | ||
[MemberData(nameof(MarshallingRequiredTypes))] | ||
public async Task TypeRequiresMarshalling_ReportsDiagnostic(Type type) | ||
{ | ||
string source = DllImportWithType(type.FullName!); | ||
await VerifyCS.VerifyAnalyzerAsync( | ||
source, | ||
VerifyCS.Diagnostic(ConvertToGeneratedDllImport) | ||
.WithLocation(0) | ||
.WithArguments("Method_Parameter"), | ||
VerifyCS.Diagnostic(ConvertToGeneratedDllImport) | ||
.WithLocation(1) | ||
.WithArguments("Method_Return")); | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(MarshallingRequiredTypes))] | ||
[MemberData(nameof(NoMarshallingRequiredTypes))] | ||
public async Task ByRef_ReportsDiagnostic(Type type) | ||
{ | ||
string typeName = type.FullName!; | ||
string source = @$" | ||
using System.Runtime.InteropServices; | ||
unsafe partial class Test | ||
{{ | ||
[DllImport(""DoesNotExist"")] | ||
public static extern void {{|#0:Method_In|}}(in {typeName} p); | ||
[DllImport(""DoesNotExist"")] | ||
public static extern void {{|#1:Method_Out|}}(out {typeName} p); | ||
[DllImport(""DoesNotExist"")] | ||
public static extern void {{|#2:Method_Ref|}}(ref {typeName} p); | ||
}} | ||
"; | ||
await VerifyCS.VerifyAnalyzerAsync( | ||
source, | ||
VerifyCS.Diagnostic(ConvertToGeneratedDllImport) | ||
.WithLocation(0) | ||
.WithArguments("Method_In"), | ||
VerifyCS.Diagnostic(ConvertToGeneratedDllImport) | ||
.WithLocation(1) | ||
.WithArguments("Method_Out"), | ||
VerifyCS.Diagnostic(ConvertToGeneratedDllImport) | ||
.WithLocation(2) | ||
.WithArguments("Method_Ref")); | ||
} | ||
|
||
[Fact] | ||
public async Task PreserveSigFalse_ReportsDiagnostic() | ||
{ | ||
string source = @$" | ||
using System.Runtime.InteropServices; | ||
partial class Test | ||
{{ | ||
[DllImport(""DoesNotExist"", PreserveSig = false)] | ||
public static extern void {{|#0:Method1|}}(); | ||
[DllImport(""DoesNotExist"", PreserveSig = true)] | ||
public static extern void Method2(); | ||
}} | ||
"; | ||
await VerifyCS.VerifyAnalyzerAsync( | ||
source, | ||
VerifyCS.Diagnostic(ConvertToGeneratedDllImport) | ||
.WithLocation(0) | ||
.WithArguments("Method1")); | ||
} | ||
|
||
[Fact] | ||
public async Task SetLastErrorTrue_ReportsDiagnostic() | ||
{ | ||
string source = @$" | ||
using System.Runtime.InteropServices; | ||
partial class Test | ||
{{ | ||
[DllImport(""DoesNotExist"", SetLastError = false)] | ||
public static extern void Method1(); | ||
[DllImport(""DoesNotExist"", SetLastError = true)] | ||
public static extern void {{|#0:Method2|}}(); | ||
}} | ||
"; | ||
await VerifyCS.VerifyAnalyzerAsync( | ||
source, | ||
VerifyCS.Diagnostic(ConvertToGeneratedDllImport) | ||
.WithLocation(0) | ||
.WithArguments("Method2")); | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(NoMarshallingRequiredTypes))] | ||
public async Task BlittablePrimitive_NoDiagnostic(Type type) | ||
{ | ||
string source = DllImportWithType(type.FullName!); | ||
await VerifyCS.VerifyAnalyzerAsync(source); | ||
} | ||
|
||
[Fact] | ||
public async Task NotDllImport_NoDiagnostic() | ||
{ | ||
string source = @$" | ||
using System.Runtime.InteropServices; | ||
partial class Test | ||
{{ | ||
public static extern bool Method1(bool p, in bool pIn, ref bool pRef, out bool pOut); | ||
public static extern int Method2(int p, in int pIn, ref int pRef, out int pOut); | ||
}} | ||
"; | ||
await VerifyCS.VerifyAnalyzerAsync(source); | ||
} | ||
|
||
private static string DllImportWithType(string typeName) => @$" | ||
using System.Runtime.InteropServices; | ||
unsafe partial class Test | ||
{{ | ||
[DllImport(""DoesNotExist"")] | ||
public static extern void {{|#0:Method_Parameter|}}({typeName} p); | ||
[DllImport(""DoesNotExist"")] | ||
public static extern {typeName} {{|#1:Method_Return|}}(); | ||
}} | ||
"; | ||
} | ||
} |
Oops, something went wrong.