Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report generator diagnostic when trying to target pre-.NET 5.0 #276

Merged
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
82 changes: 81 additions & 1 deletion DllImportGenerator/DllImportGenerator.UnitTests/Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,82 @@

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Interop;
using Xunit;

namespace DllImportGenerator.UnitTests
{
public class Diagnostics
{
public enum TargetFramework
{
Framework,
Core,
Standard,
Net
}

[Theory]
[InlineData(TargetFramework.Framework)]
[InlineData(TargetFramework.Core)]
[InlineData(TargetFramework.Standard)]
public async Task TargetFrameworkNotSupported_ReportsDiagnostic(TargetFramework targetFramework)
{
string source = @"
using System.Runtime.InteropServices;
namespace System.Runtime.InteropServices
{
// Define attribute for pre-.NET 5.0
sealed class GeneratedDllImportAttribute : System.Attribute
{
public GeneratedDllImportAttribute(string a) { }
}
}
partial class Test
{
[GeneratedDllImport(""DoesNotExist"")]
public static partial void Method();
}
";
Compilation comp = await TestUtils.CreateCompilationWithReferenceAssemblies(source, GetReferenceAssemblies(targetFramework));
TestUtils.AssertPreSourceGeneratorCompilation(comp);

var newComp = TestUtils.RunGenerators(comp, out var generatorDiags, new Microsoft.Interop.DllImportGenerator());
DiagnosticResult[] expectedDiags = new DiagnosticResult[]
{
new DiagnosticResult(GeneratorDiagnostics.TargetFrameworkNotSupported)
.WithArguments("5.0")
};
VerifyDiagnostics(expectedDiags, GetSortedDiagnostics(generatorDiags));

var newCompDiags = newComp.GetDiagnostics();
Assert.Empty(newCompDiags);
}

[Theory]
[InlineData(TargetFramework.Framework)]
[InlineData(TargetFramework.Core)]
[InlineData(TargetFramework.Standard)]
public async Task TargetFrameworkNotSupported_NoGeneratedDllImport_NoDiagnostic(TargetFramework targetFramework)
{
string source = @"
using System.Runtime.InteropServices;
partial class Test
{
[DllImport(""DoesNotExist"")]
public static extern void Method();
}
";
Compilation comp = await TestUtils.CreateCompilationWithReferenceAssemblies(source, GetReferenceAssemblies(targetFramework));
TestUtils.AssertPreSourceGeneratorCompilation(comp);

var newComp = TestUtils.RunGenerators(comp, out var generatorDiags, new Microsoft.Interop.DllImportGenerator());
Assert.Empty(generatorDiags);

var newCompDiags = newComp.GetDiagnostics();
Assert.Empty(newCompDiags);
}

[Fact]
public async Task ParameterTypeNotSupported_ReportsDiagnostic()
{
Expand Down Expand Up @@ -327,5 +395,17 @@ private static Diagnostic[] GetSortedDiagnostics(IEnumerable<Diagnostic> diagnos
.ThenBy(d => d.Id)
.ToArray();
}

private static ReferenceAssemblies GetReferenceAssemblies(TargetFramework targetFramework)
{
return targetFramework switch
{
TargetFramework.Framework => ReferenceAssemblies.NetFramework.Net48.Default,
TargetFramework.Standard => ReferenceAssemblies.NetStandard.NetStandard21,
TargetFramework.Core => ReferenceAssemblies.NetCore.NetCoreApp31,
TargetFramework.Net => ReferenceAssemblies.NetCore.NetCoreApp50,
_ => ReferenceAssemblies.Default
};
}
}
}
16 changes: 16 additions & 0 deletions DllImportGenerator/DllImportGenerator.UnitTests/TestUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ public static async Task<Compilation> CreateCompilation(string source, OutputKin
new CSharpCompilationOptions(outputKind, allowUnsafe: allowUnsafe));
}

/// <summary>
/// Create a compilation given source and reference assemblies
/// </summary>
/// <param name="source">Source to compile</param>
/// <param name="referenceAssemblies">Reference assemblies to include</param>
/// <param name="outputKind">Output type</param>
/// <param name="allowUnsafe">Whether or not use of the unsafe keyword should be allowed</param>
/// <returns>The resulting compilation</returns>
public static async Task<Compilation> CreateCompilationWithReferenceAssemblies(string source, ReferenceAssemblies referenceAssemblies, OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary, bool allowUnsafe = true)
{
return CSharpCompilation.Create("compilation",
new[] { CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Preview)) },
(await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, CancellationToken.None)),
new CSharpCompilationOptions(outputKind, allowUnsafe: allowUnsafe));
}

public static (ReferenceAssemblies, MetadataReference) GetReferenceAssemblies()
{
// TODO: When .NET 5.0 releases, we can simplify this.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1 @@
## Release 1.0

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
DLLIMPORTGEN001 | Usage | Error
DLLIMPORTGEN002 | Usage | Error
DLLIMPORTGENANALYZER001 | Usage | Error
DLLIMPORTGENANALYZER002 | Usage | Error
DLLIMPORTGENANALYZER003 | Usage | Error
DLLIMPORTGENANALYZER004 | Usage | Error
DLLIMPORTGENANALYZER005 | Usage | Error
DLLIMPORTGENANALYZER006 | Usage | Error
DLLIMPORTGENANALYZER007 | Usage | Error
DLLIMPORTGENANALYZER008 | Usage | Error
DLLIMPORTGENANALYZER009 | Usage | Error
DLLIMPORTGENANALYZER010 | Usage | Warning
DLLIMPORTGENANALYZER011 | Usage | Warning
DLLIMPORTGENANALYZER012 | Usage | Error
; Shipped analyzer releases
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
DLLIMPORTGEN001 | SourceGeneration | Error | TypeNotSupported
DLLIMPORTGEN002 | SourceGeneration | Error | ConfigurationNotSupported
DLLIMPORTGEN003 | SourceGeneration | Error | TargetFrameworkNotSupported
DLLIMPORTGENANALYZER001 | Usage | Error | BlittableTypeMustBeBlittable
DLLIMPORTGENANALYZER002 | Usage | Error | CannotHaveMultipleMarshallingAttributes
DLLIMPORTGENANALYZER003 | Usage | Error | NativeTypeMustBeNonNull
DLLIMPORTGENANALYZER004 | Usage | Error | NativeTypeMustBeBlittable
DLLIMPORTGENANALYZER005 | Usage | Error | GetPinnableReferenceReturnTypeBlittable
DLLIMPORTGENANALYZER006 | Usage | Error | NativeTypeMustBePointerSized
DLLIMPORTGENANALYZER007 | Usage | Error | NativeTypeMustHaveRequiredShape
DLLIMPORTGENANALYZER008 | Usage | Error | ValuePropertyMustHaveSetter
DLLIMPORTGENANALYZER009 | Usage | Error | ValuePropertyMustHaveGetter
DLLIMPORTGENANALYZER010 | Usage | Warning | GetPinnableReferenceShouldSupportAllocatingMarshallingFallback
DLLIMPORTGENANALYZER011 | Usage | Warning | StackallocMarshallingShouldSupportAllocatingMarshallingFallback
DLLIMPORTGENANALYZER012 | Usage | Error | StackallocConstructorMustHaveStackBufferSizeConstant
26 changes: 25 additions & 1 deletion DllImportGenerator/DllImportGenerator/DllImportGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ public class DllImportGenerator : ISourceGenerator
private const string GeneratedDllImport = nameof(GeneratedDllImport);
private const string GeneratedDllImportAttribute = nameof(GeneratedDllImportAttribute);

private static readonly Version MinimumSupportedFrameworkVersion = new Version(5, 0);

public void Execute(GeneratorExecutionContext context)
{
var synRec = context.SyntaxReceiver as SyntaxReceiver;
if (synRec is null)
if (synRec is null || !synRec.Methods.Any())
{
return;
}
Expand All @@ -38,6 +40,13 @@ public void Execute(GeneratorExecutionContext context)
var syntaxToModel = new Dictionary<SyntaxTree, SemanticModel>();

var generatorDiagnostics = new GeneratorDiagnostics(context);
if (!IsSupportedTargetFramework(context.Compilation))
{
// We don't return early here, letting the source generation continue.
// This allows a user to copy generated source and use it as a starting point
// for manual marshalling if desired.
generatorDiagnostics.ReportTargetFrameworkNotSupported(MinimumSupportedFrameworkVersion);
}

var generatedDllImports = new StringBuilder();
foreach (SyntaxReference synRef in synRec.Methods)
Expand Down Expand Up @@ -118,6 +127,21 @@ private void PrintGeneratedSource(
builder.AppendLine(toPrint.NormalizeWhitespace().ToString());
}

private static bool IsSupportedTargetFramework(Compilation compilation)
{
IAssemblySymbol systemAssembly = compilation.GetSpecialType(SpecialType.System_Object).ContainingAssembly;
return systemAssembly.Identity.Name switch
{
// .NET Framework
"mscorlib" => false,
// .NET Standard
"netstandard" => false,
// .NET Core (when version < 5.0) or .NET
"System.Runtime" => systemAssembly.Identity.Version >= MinimumSupportedFrameworkVersion,
_ => false,
};
}

private static bool IsGeneratedDllImportAttribute(AttributeSyntax attrSyntaxMaybe)
{
var attrName = attrSyntaxMaybe.Name.ToString();
Expand Down
29 changes: 26 additions & 3 deletions DllImportGenerator/DllImportGenerator/GeneratorDiagnostics.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

using Microsoft.CodeAnalysis;

#nullable enable

namespace Microsoft.Interop
{
internal static class DiagnosticExtensions
Expand Down Expand Up @@ -53,6 +52,7 @@ public class Ids
public const string Prefix = "DLLIMPORTGEN";
public const string TypeNotSupported = Prefix + "001";
public const string ConfigurationNotSupported = Prefix + "002";
public const string TargetFrameworkNotSupported = Prefix + "003";
}

private const string Category = "SourceGeneration";
Expand Down Expand Up @@ -137,6 +137,16 @@ public class Ids
isEnabledByDefault: true,
description: GetResourceString(nameof(Resources.ConfigurationNotSupportedDescription)));

public readonly static DiagnosticDescriptor TargetFrameworkNotSupported =
new DiagnosticDescriptor(
Ids.TargetFrameworkNotSupported,
GetResourceString(nameof(Resources.TargetFrameworkNotSupportedTitle)),
GetResourceString(nameof(Resources.TargetFrameworkNotSupportedMessage)),
Category,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: GetResourceString(nameof(Resources.TargetFrameworkNotSupportedDescription)));

private readonly GeneratorExecutionContext context;

public GeneratorDiagnostics(GeneratorExecutionContext context)
Expand Down Expand Up @@ -253,6 +263,19 @@ internal void ReportMarshallingNotSupported(
}
}

/// <summary>
/// Report diagnostic for targeting a framework that is not supported
/// </summary>
/// <param name="minimumSupportedVersion">Minimum supported version of .NET</param>
public void ReportTargetFrameworkNotSupported(Version minimumSupportedVersion)
{
this.context.ReportDiagnostic(
Diagnostic.Create(
TargetFrameworkNotSupported,
Location.None,
minimumSupportedVersion.ToString(2)));
}

private static LocalizableResourceString GetResourceString(string resourceName)
{
return new LocalizableResourceString(resourceName, Resources.ResourceManager, typeof(Resources));
Expand Down
27 changes: 27 additions & 0 deletions DllImportGenerator/DllImportGenerator/Resources.Designer.cs

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

11 changes: 11 additions & 0 deletions DllImportGenerator/DllImportGenerator/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,17 @@
<data name="StackallocMarshallingShouldSupportAllocatingMarshallingFallbackMessage" xml:space="preserve">
<value>Native type '{0}' has a stack-allocating constructor does not support marshalling in scenarios where stack allocation is impossible</value>
</data>
<data name="TargetFrameworkNotSupportedDescription" xml:space="preserve">
<value>P/Invoke source generation is only supported on .NET {0} or above. The generated source will not be compatible with other frameworks.</value>
<comment>{0} is a version number</comment>
</data>
<data name="TargetFrameworkNotSupportedMessage" xml:space="preserve">
<value>'GeneratedDllImportAttribute' cannot be used for source-generated P/Invokes on the current target framework. Source-generated P/Invokes require .NET {0} or above.</value>
<comment>{0} is a version number</comment>
</data>
<data name="TargetFrameworkNotSupportedTitle" xml:space="preserve">
<value>Current target framework is not supported by source-generated P/Invokes</value>
</data>
<data name="TypeNotSupportedDescription" xml:space="preserve">
<value>For types that are not supported by source-generated P/Invokes, the resulting P/Invoke will rely on the underlying runtime to marshal the specified type.</value>
</data>
Expand Down
12 changes: 10 additions & 2 deletions DllImportGenerator/designs/Diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ For all [Roslyn diagnostics](https://docs.microsoft.com/dotnet/api/microsoft.cod

| Setting | Value |
| -------- | ------------------ |
| Category | DllImportGenerator |
| Category | SourceGeneration |
| Severity | Error |
| Enabled | True |

Expand Down Expand Up @@ -36,4 +36,12 @@ public static partial void Method([MarshalAs(UnmanagedType.SafeArray, SafeArrayS
// Unsupported combination of MarshalAs and type being marshalled
[GeneratedDllImport("NativeLib")]
public static partial void Method([MarshalAs(UnmanagedType.LPStr)] bool b);
```
```

## `DLLIMPORTGEN003`: Current target framework is not supported by source-generated P/Invokes

The `GeneratedDllImport` is being used when targeting a framework that is not supported by source-generated P/Invokes. The generated code is currently only compatible with .NET 5.0 or above.

# Analyzer Diagnostics

The P/Invoke source generator library also contains analyzers that emit diagnostics. These diagnostics flag issues around usage (i.e. of P/Invoke and marshalling attributes) rather than the source generation scenario support issues flagged by the generator itself.