From 7d5caa00a67fce3c68964a9514b893270fc28b67 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Wed, 20 Jan 2021 10:58:45 -0800 Subject: [PATCH] Add fixer for converting to GeneratedDllImport (dotnet/runtimelab#564) Commit migrated from https://github.com/dotnet/runtimelab/commit/dd810614463b99d14972fe1f4f54f3215212e4e9 --- .../ConvertToGeneratedDllImportFixer.cs | 228 ++++++++++++++ .../DllImportGenerator.csproj | 2 +- .../DllImportGenerator/Resources.Designer.cs | 42 ++- .../gen/DllImportGenerator/Resources.resx | 10 + .../ConvertToGeneratedDllImportFixerTests.cs | 280 ++++++++++++++++++ .../DllImportGenerator.UnitTests.csproj | 1 + .../Verifiers/CSharpAnalyzerVerifier.cs | 58 +--- .../Verifiers/CSharpCodeFixVerifier.cs | 119 ++++++++ 8 files changed, 679 insertions(+), 61 deletions(-) create mode 100644 src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/Analyzers/ConvertToGeneratedDllImportFixer.cs create mode 100644 src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/ConvertToGeneratedDllImportFixerTests.cs create mode 100644 src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/Verifiers/CSharpCodeFixVerifier.cs diff --git a/src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/Analyzers/ConvertToGeneratedDllImportFixer.cs b/src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/Analyzers/ConvertToGeneratedDllImportFixer.cs new file mode 100644 index 00000000000000..c08c90c32162d8 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/Analyzers/ConvertToGeneratedDllImportFixer.cs @@ -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 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 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 argumentsToRemove = new List(); + 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; + } + } +} diff --git a/src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/DllImportGenerator.csproj b/src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/DllImportGenerator.csproj index 108713c4eddeb9..9d92212ea0b207 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/DllImportGenerator.csproj +++ b/src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/DllImportGenerator.csproj @@ -29,7 +29,7 @@ - + diff --git a/src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/Resources.Designer.cs b/src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/Resources.Designer.cs index 5a033df756ec2b..071ce9c6328b5e 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/Resources.Designer.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/Resources.Designer.cs @@ -176,7 +176,8 @@ internal static string ConfigurationNotSupportedTitle { return ResourceManager.GetString("ConfigurationNotSupportedTitle", resourceCulture); } } - + + /// /// Looks up a localized string similar to Use 'GeneratedDllImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time. /// internal static string ConvertToGeneratedDllImportDescription { @@ -185,6 +186,7 @@ internal static string ConvertToGeneratedDllImportDescription { } } + /// /// Looks up a localized string similar to Mark the method '{0}' with 'GeneratedDllImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time. /// internal static string ConvertToGeneratedDllImportMessage { @@ -192,7 +194,16 @@ internal static string ConvertToGeneratedDllImportMessage { return ResourceManager.GetString("ConvertToGeneratedDllImportMessage", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Convert to 'GeneratedDllImport'. + /// + internal static string ConvertToGeneratedDllImportNoPreprocessor { + get { + return ResourceManager.GetString("ConvertToGeneratedDllImportNoPreprocessor", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use 'GeneratedDllImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time. /// @@ -201,7 +212,25 @@ internal static string ConvertToGeneratedDllImportTitle { return ResourceManager.GetString("ConvertToGeneratedDllImportTitle", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Conversion to 'GeneratedDllImport' may change behavior and compatibility. See {0} for more information.. + /// + internal static string ConvertToGeneratedDllImportWarning { + get { + return ResourceManager.GetString("ConvertToGeneratedDllImportWarning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Convert to 'GeneratedDllImport' under a preprocessor define. + /// + internal static string ConvertToGeneratedDllImportWithPreprocessor { + get { + return ResourceManager.GetString("ConvertToGeneratedDllImportWithPreprocessor", resourceCulture); + } + } + /// /// Looks up a localized string similar to The specified parameter needs to be marshalled from managed to native, but the native type '{0}' does not support it.. /// @@ -210,7 +239,7 @@ internal static string CustomTypeMarshallingManagedToNativeUnsupported { return ResourceManager.GetString("CustomTypeMarshallingManagedToNativeUnsupported", resourceCulture); } } - + /// /// Looks up a localized string similar to The specified parameter needs to be marshalled from native to managed, but the native type '{0}' does not support it.. /// @@ -219,7 +248,7 @@ internal static string CustomTypeMarshallingNativeToManagedUnsupported { return ResourceManager.GetString("CustomTypeMarshallingNativeToManagedUnsupported", resourceCulture); } } - + /// /// Looks up a localized string similar to Methods marked with 'GeneratedDllImportAttribute' should be 'static' and 'partial'. P/Invoke source generation will ignore methods that are not 'static' and 'partial'.. /// @@ -300,8 +329,9 @@ internal static string InOutAttributeByRefNotSupported { return ResourceManager.GetString("InOutAttributeByRefNotSupported", resourceCulture); } } + /// - /// Looks up a localized string similar to The '[In]' and '[Out]' attributes on this parameter are unsupported on this parameter.. + /// Looks up a localized string similar to The provided '[In]' and '[Out]' attributes on this parameter are unsupported on this parameter.. /// internal static string InOutAttributeMarshalerNotSupported { get { diff --git a/src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/Resources.resx b/src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/Resources.resx index 4fe08e2f600c1d..7f41f03a9b9894 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/Resources.resx +++ b/src/libraries/System.Runtime.InteropServices/gen/DllImportGenerator/Resources.resx @@ -162,9 +162,19 @@ Mark the method '{0}' with 'GeneratedDllImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time + + Convert to 'GeneratedDllImport' + Use 'GeneratedDllImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time + + Conversion to 'GeneratedDllImport' may change behavior and compatibility. See {0} for more information. + {0} is a documentation link + + + Convert to 'GeneratedDllImport' under a preprocessor define + The specified parameter needs to be marshalled from managed to native, but the native type '{0}' does not support it. diff --git a/src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/ConvertToGeneratedDllImportFixerTests.cs b/src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/ConvertToGeneratedDllImportFixerTests.cs new file mode 100644 index 00000000000000..ddad1cc5c67e5e --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/ConvertToGeneratedDllImportFixerTests.cs @@ -0,0 +1,280 @@ +using System.Threading.Tasks; +using Xunit; +using static Microsoft.Interop.Analyzers.ConvertToGeneratedDllImportFixer; + +using VerifyCS = DllImportGenerator.UnitTests.Verifiers.CSharpCodeFixVerifier< + Microsoft.Interop.Analyzers.ConvertToGeneratedDllImportAnalyzer, + Microsoft.Interop.Analyzers.ConvertToGeneratedDllImportFixer>; + +namespace DllImportGenerator.UnitTests +{ + public class ConvertToGeneratedDllImportFixerTests + { + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Basic(bool usePreprocessorDefines) + { + string source = @$" +using System.Runtime.InteropServices; +partial class Test +{{ + [DllImport(""DoesNotExist"")] + public static extern int [|Method|](out int ret); +}}"; + // Fixed source will have CS8795 (Partial method must have an implementation) without generator run + string fixedSource = usePreprocessorDefines + ? @$" +using System.Runtime.InteropServices; +partial class Test +{{ +#if NET + [GeneratedDllImport(""DoesNotExist"")] + public static partial int {{|CS8795:Method|}}(out int ret); +#else + [DllImport(""DoesNotExist"")] + public static extern int Method(out int ret); +#endif +}}" + : @$" +using System.Runtime.InteropServices; +partial class Test +{{ + [GeneratedDllImport(""DoesNotExist"")] + public static partial int {{|CS8795:Method|}}(out int ret); +}}"; + await VerifyCS.VerifyCodeFixAsync( + source, + fixedSource, + usePreprocessorDefines ? WithPreprocessorDefinesKey : NoPreprocessorDefinesKey); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Comments(bool usePreprocessorDefines) + { + string source = @$" +using System.Runtime.InteropServices; +partial class Test +{{ + // P/Invoke + [DllImport(/*name*/""DoesNotExist"")] // comment + public static extern int [|Method1|](out int ret); + + /** P/Invoke **/ + [DllImport(""DoesNotExist"") /*name*/] + // < ... > + public static extern int [|Method2|](out int ret); +}}"; + // Fixed source will have CS8795 (Partial method must have an implementation) without generator run + string fixedSource = usePreprocessorDefines + ? @$" +using System.Runtime.InteropServices; +partial class Test +{{ + // P/Invoke +#if NET + [GeneratedDllImport(/*name*/""DoesNotExist"")] // comment + public static partial int {{|CS8795:Method1|}}(out int ret); +#else + [DllImport(/*name*/""DoesNotExist"")] // comment + public static extern int Method1(out int ret); +#endif + + /** P/Invoke **/ +#if NET + [GeneratedDllImport(""DoesNotExist"") /*name*/] + // < ... > + public static partial int {{|CS8795:Method2|}}(out int ret); +#else + [DllImport(""DoesNotExist"") /*name*/] + // < ... > + public static extern int Method2(out int ret); +#endif +}}" + : @$" +using System.Runtime.InteropServices; +partial class Test +{{ + // P/Invoke + [GeneratedDllImport(/*name*/""DoesNotExist"")] // comment + public static partial int {{|CS8795:Method1|}}(out int ret); + + /** P/Invoke **/ + [GeneratedDllImport(""DoesNotExist"") /*name*/] + // < ... > + public static partial int {{|CS8795:Method2|}}(out int ret); +}}"; + await VerifyCS.VerifyCodeFixAsync( + source, + fixedSource, + usePreprocessorDefines ? WithPreprocessorDefinesKey : NoPreprocessorDefinesKey); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MultipleAttributes(bool usePreprocessorDefines) + { + string source = @$" +using System.Runtime.InteropServices; +partial class Test +{{ + [System.ComponentModel.Description(""Test""), DllImport(""DoesNotExist"")] + public static extern int [|Method1|](out int ret); + + [System.ComponentModel.Description(""Test"")] + [DllImport(""DoesNotExist"")] + [return: MarshalAs(UnmanagedType.I4)] + public static extern int [|Method2|](out int ret); +}}"; + // Fixed source will have CS8795 (Partial method must have an implementation) without generator run + string fixedSource = usePreprocessorDefines + ? @$" +using System.Runtime.InteropServices; +partial class Test +{{ +#if NET + [System.ComponentModel.Description(""Test""), GeneratedDllImport(""DoesNotExist"")] + public static partial int {{|CS8795:Method1|}}(out int ret); +#else + [System.ComponentModel.Description(""Test""), DllImport(""DoesNotExist"")] + public static extern int Method1(out int ret); +#endif + +#if NET + [System.ComponentModel.Description(""Test"")] + [GeneratedDllImport(""DoesNotExist"")] + [return: MarshalAs(UnmanagedType.I4)] + public static partial int {{|CS8795:Method2|}}(out int ret); +#else + [System.ComponentModel.Description(""Test"")] + [DllImport(""DoesNotExist"")] + [return: MarshalAs(UnmanagedType.I4)] + public static extern int Method2(out int ret); +#endif +}}" + : @$" +using System.Runtime.InteropServices; +partial class Test +{{ + [System.ComponentModel.Description(""Test""), GeneratedDllImport(""DoesNotExist"")] + public static partial int {{|CS8795:Method1|}}(out int ret); + + [System.ComponentModel.Description(""Test"")] + [GeneratedDllImport(""DoesNotExist"")] + [return: MarshalAs(UnmanagedType.I4)] + public static partial int {{|CS8795:Method2|}}(out int ret); +}}"; + await VerifyCS.VerifyCodeFixAsync( + source, + fixedSource, + usePreprocessorDefines ? WithPreprocessorDefinesKey : NoPreprocessorDefinesKey); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task NamedArguments(bool usePreprocessorDefines) + { + string source = @$" +using System.Runtime.InteropServices; +partial class Test +{{ + [DllImport(""DoesNotExist"", EntryPoint = ""Entry"")] + public static extern int [|Method1|](out int ret); + + [DllImport(""DoesNotExist"", EntryPoint = ""Entry"", CharSet = CharSet.Unicode)] + public static extern int [|Method2|](out int ret); +}}"; + // Fixed source will have CS8795 (Partial method must have an implementation) without generator run + string fixedSource = usePreprocessorDefines + ? @$" +using System.Runtime.InteropServices; +partial class Test +{{ +#if NET + [GeneratedDllImport(""DoesNotExist"", EntryPoint = ""Entry"")] + public static partial int {{|CS8795:Method1|}}(out int ret); +#else + [DllImport(""DoesNotExist"", EntryPoint = ""Entry"")] + public static extern int Method1(out int ret); +#endif + +#if NET + [GeneratedDllImport(""DoesNotExist"", EntryPoint = ""Entry"", CharSet = CharSet.Unicode)] + public static partial int {{|CS8795:Method2|}}(out int ret); +#else + [DllImport(""DoesNotExist"", EntryPoint = ""Entry"", CharSet = CharSet.Unicode)] + public static extern int Method2(out int ret); +#endif +}}" : @$" +using System.Runtime.InteropServices; +partial class Test +{{ + [GeneratedDllImport(""DoesNotExist"", EntryPoint = ""Entry"")] + public static partial int {{|CS8795:Method1|}}(out int ret); + + [GeneratedDllImport(""DoesNotExist"", EntryPoint = ""Entry"", CharSet = CharSet.Unicode)] + public static partial int {{|CS8795:Method2|}}(out int ret); +}}"; + await VerifyCS.VerifyCodeFixAsync( + source, + fixedSource, + usePreprocessorDefines ? WithPreprocessorDefinesKey : NoPreprocessorDefinesKey); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task RemoveableNamedArguments(bool usePreprocessorDefines) + { + string source = @$" +using System.Runtime.InteropServices; +partial class Test +{{ + [DllImport(""DoesNotExist"", BestFitMapping = false, EntryPoint = ""Entry"")] + public static extern int [|Method1|](out int ret); + + [DllImport(""DoesNotExist"", ThrowOnUnmappableChar = false)] + public static extern int [|Method2|](out int ret); +}}"; + // Fixed source will have CS8795 (Partial method must have an implementation) without generator run + string fixedSource = usePreprocessorDefines + ? @$" +using System.Runtime.InteropServices; +partial class Test +{{ +#if NET + [GeneratedDllImport(""DoesNotExist"", EntryPoint = ""Entry"")] + public static partial int {{|CS8795:Method1|}}(out int ret); +#else + [DllImport(""DoesNotExist"", BestFitMapping = false, EntryPoint = ""Entry"")] + public static extern int Method1(out int ret); +#endif + +#if NET + [GeneratedDllImport(""DoesNotExist"")] + public static partial int {{|CS8795:Method2|}}(out int ret); +#else + [DllImport(""DoesNotExist"", ThrowOnUnmappableChar = false)] + public static extern int Method2(out int ret); +#endif +}}" : @$" +using System.Runtime.InteropServices; +partial class Test +{{ + [GeneratedDllImport(""DoesNotExist"", EntryPoint = ""Entry"")] + public static partial int {{|CS8795:Method1|}}(out int ret); + + [GeneratedDllImport(""DoesNotExist"")] + public static partial int {{|CS8795:Method2|}}(out int ret); +}}"; + await VerifyCS.VerifyCodeFixAsync( + source, + fixedSource, + usePreprocessorDefines ? WithPreprocessorDefinesKey : NoPreprocessorDefinesKey); + } + } +} \ No newline at end of file diff --git a/src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/DllImportGenerator.UnitTests.csproj b/src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/DllImportGenerator.UnitTests.csproj index 2caf2103354ed8..c1ed94a205ff08 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/DllImportGenerator.UnitTests.csproj +++ b/src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/DllImportGenerator.UnitTests.csproj @@ -11,6 +11,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/Verifiers/CSharpAnalyzerVerifier.cs b/src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/Verifiers/CSharpAnalyzerVerifier.cs index 33cbfb8805312b..bb0a07677ca239 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/Verifiers/CSharpAnalyzerVerifier.cs +++ b/src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/Verifiers/CSharpAnalyzerVerifier.cs @@ -1,15 +1,10 @@ -using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.CSharp.Testing.XUnit; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; -using Microsoft.CodeAnalysis.Testing.Verifiers; namespace DllImportGenerator.UnitTests.Verifiers { @@ -40,54 +35,9 @@ public static async Task VerifyAnalyzerAsync(string source, params DiagnosticRes await test.RunAsync(CancellationToken.None); } - internal class Test : CSharpAnalyzerTest - { - public Test() - { - var (refAssem, ancillary) = TestUtils.GetReferenceAssemblies(); - ReferenceAssemblies = refAssem; - SolutionTransforms.Add((solution, projectId) => - { - var project = solution.GetProject(projectId)!; - var compilationOptions = project.CompilationOptions!; - - var diagnosticOptions = compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings); - - // Explicitly enable diagnostics that are not enabled by default - var enableAnalyzersOptions = new System.Collections.Generic.Dictionary(); - foreach (var analyzer in GetDiagnosticAnalyzers().ToImmutableArray()) - { - foreach (var diagnostic in analyzer.SupportedDiagnostics) - { - if (diagnostic.IsEnabledByDefault) - continue; - - // Map the default severity to the reporting behaviour. - // We cannot simply use ReportDiagnostic.Default here, as diagnostics that are not enabled by default - // are treated as suppressed (regardless of their default severity). - var report = diagnostic.DefaultSeverity switch - { - DiagnosticSeverity.Error => ReportDiagnostic.Error, - DiagnosticSeverity.Warning => ReportDiagnostic.Warn, - DiagnosticSeverity.Info => ReportDiagnostic.Info, - DiagnosticSeverity.Hidden => ReportDiagnostic.Hidden, - _ => ReportDiagnostic.Default - }; - enableAnalyzersOptions.Add(diagnostic.Id, report); - } - } - - compilationOptions = compilationOptions.WithSpecificDiagnosticOptions( - compilationOptions.SpecificDiagnosticOptions - .SetItems(CSharpVerifierHelper.NullableWarnings) - .AddRange(enableAnalyzersOptions)); - solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); - solution = solution.WithProjectMetadataReferences(projectId, project.MetadataReferences.Concat(ImmutableArray.Create(ancillary))); - solution = solution.WithProjectParseOptions(projectId, ((CSharpParseOptions)project.ParseOptions!).WithLanguageVersion(LanguageVersion.Preview)); - - return solution; - }); - } - } + // Code fix tests support both analyzer and code fix testing. This test class is derived from the code fix test + // to avoid the need to maintain duplicate copies of the customization work. + internal class Test : CSharpCodeFixVerifier.Test + { } } } \ No newline at end of file diff --git a/src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/Verifiers/CSharpCodeFixVerifier.cs b/src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/Verifiers/CSharpCodeFixVerifier.cs new file mode 100644 index 00000000000000..2d04e5497a18e6 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.UnitTests/Verifiers/CSharpCodeFixVerifier.cs @@ -0,0 +1,119 @@ + +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.CSharp.Testing.XUnit; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; + +namespace DllImportGenerator.UnitTests.Verifiers +{ + public static class CSharpCodeFixVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() + { + /// + public static DiagnosticResult Diagnostic() + => CodeFixVerifier.Diagnostic(); + + /// + public static DiagnosticResult Diagnostic(string diagnosticId) + => CodeFixVerifier.Diagnostic(diagnosticId); + + /// + public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) + => CodeFixVerifier.Diagnostic(descriptor); + + /// + public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + { + var test = new Test + { + TestCode = source, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + + /// + public static async Task VerifyCodeFixAsync(string source, string fixedSource, string? codeActionEquivalenceKey = null) + => await VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource, codeActionEquivalenceKey); + + /// + public static async Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource, string? codeActionEquivalenceKey = null) + => await VerifyCodeFixAsync(source, new[] { expected }, fixedSource, codeActionEquivalenceKey); + + /// + public static async Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource, string? codeActionEquivalenceKey = null) + { + var test = new Test + { + TestCode = source, + FixedCode = fixedSource, + CodeActionEquivalenceKey = codeActionEquivalenceKey, + CodeActionValidationMode = CodeActionValidationMode.None, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + + internal class Test : CSharpCodeFixTest + { + public Test() + { + var (refAssem, ancillary) = TestUtils.GetReferenceAssemblies(); + ReferenceAssemblies = refAssem; + SolutionTransforms.Add((solution, projectId) => + { + var project = solution.GetProject(projectId)!; + var compilationOptions = project.CompilationOptions!; + var diagnosticOptions = compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings); + + // Explicitly enable diagnostics that are not enabled by default + var enableAnalyzersOptions = new System.Collections.Generic.Dictionary(); + foreach (var analyzer in GetDiagnosticAnalyzers().ToImmutableArray()) + { + foreach (var diagnostic in analyzer.SupportedDiagnostics) + { + if (diagnostic.IsEnabledByDefault) + continue; + + // Map the default severity to the reporting behaviour. + // We cannot simply use ReportDiagnostic.Default here, as diagnostics that are not enabled by default + // are treated as suppressed (regardless of their default severity). + var report = diagnostic.DefaultSeverity switch + { + DiagnosticSeverity.Error => ReportDiagnostic.Error, + DiagnosticSeverity.Warning => ReportDiagnostic.Warn, + DiagnosticSeverity.Info => ReportDiagnostic.Info, + DiagnosticSeverity.Hidden => ReportDiagnostic.Hidden, + _ => ReportDiagnostic.Default + }; + enableAnalyzersOptions.Add(diagnostic.Id, report); + } + } + + compilationOptions = compilationOptions.WithSpecificDiagnosticOptions( + compilationOptions.SpecificDiagnosticOptions + .SetItems(CSharpVerifierHelper.NullableWarnings) + .AddRange(enableAnalyzersOptions)); + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + solution = solution.WithProjectMetadataReferences(projectId, project.MetadataReferences.Concat(ImmutableArray.Create(ancillary))); + solution = solution.WithProjectParseOptions(projectId, ((CSharpParseOptions)project.ParseOptions!).WithLanguageVersion(LanguageVersion.Preview)); + return solution; + }); + } + + protected override ParseOptions CreateParseOptions() + => ((CSharpParseOptions)base.CreateParseOptions()).WithPreprocessorSymbols("NET"); + } + } +} \ No newline at end of file