Skip to content

Commit

Permalink
Add analyzer to flag converting from DllImport to GeneratedDllImport (d…
Browse files Browse the repository at this point in the history
  • Loading branch information
elinor-fung authored Dec 1, 2020
1 parent 4700722 commit 67e674b
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ DLLIMPORTGENANALYZER011 | Usage | Warning | StackallocMarshallingSho
DLLIMPORTGENANALYZER012 | Usage | Error | StackallocConstructorMustHaveStackBufferSizeConstant
DLLIMPORTGENANALYZER013 | Usage | Warning | GeneratedDllImportMissingRequiredModifiers
DLLIMPORTGENANALYZER014 | Usage | Error | RefValuePropertyUnsupported
DLLIMPORTGENANALYZER015 | Interoperability | Disabled | ConvertToGeneratedDllImportAnalyzer
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public static class Ids

// GeneratedDllImport
public const string GeneratedDllImportMissingRequiredModifiers = Prefix + "013";

// Migration from DllImport to GeneratedDllImport
public const string ConvertToGeneratedDllImport = Prefix + "015";
}

internal static LocalizableResourceString GetResourceString(string resourceName)
Expand Down
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();
}
}
}

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 @@ -156,6 +156,15 @@
<data name="ConfigurationNotSupportedTitle" xml:space="preserve">
<value>Specified configuration is not supported by source-generated P/Invokes.</value>
</data>
<data name="ConvertToGeneratedDllImportDescription" xml:space="preserve">
<value>Use 'GeneratedDllImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time</value>
</data>
<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="ConvertToGeneratedDllImportTitle" xml:space="preserve">
<value>Use 'GeneratedDllImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time</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
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|}}();
}}
";
}
}
Loading

0 comments on commit 67e674b

Please sign in to comment.