diff --git a/eng/Versions.props b/eng/Versions.props index 6f2c2d89ce0dc9..da324b40bb35bb 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -41,11 +41,11 @@ 3.3.3 - 4.2.0-2.final - 4.2.0-2.final - 4.2.0-2.final + 4.3.0-1.22206.2 + 4.3.0-1.22206.2 + 4.3.0-1.22206.2 7.0.0-preview1.22207.3 - 4.2.0-2.final + 4.3.0-1.22206.2 @@ -173,7 +173,7 @@ 2.14.3 - 1.1.2-beta1.22122.4 + 1.1.2-beta1.22205.2 7.0.0-preview-20220331.1 diff --git a/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/AddDisableRuntimeMarshallingAttributeFixer.cs b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/AddDisableRuntimeMarshallingAttributeFixer.cs new file mode 100644 index 00000000000000..2d224e9d30c71e --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/AddDisableRuntimeMarshallingAttributeFixer.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Editing; + +namespace Microsoft.Interop.Analyzers +{ + [ExportCodeFixProvider(LanguageNames.CSharp), Shared] + public class AddDisableRuntimeMarshallingAttributeFixer : CodeFixProvider + { + private const string EquivalenceKey = nameof(AddDisableRuntimeMarshallingAttributeFixer); + + private const string PropertiesFolderName = "Properties"; + private const string AssemblyInfoFileName = "AssemblyInfo.cs"; + + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(GeneratorDiagnostics.Ids.TypeNotSupported); + + // TODO: Write a custom fix all provider + public override FixAllProvider? GetFixAllProvider() => null; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + List fixedDiagnostics = new(context.Diagnostics.Where(IsRequiresDiableRuntimeMarshallingDiagnostic)); + + if (fixedDiagnostics.Count > 0) + { + context.RegisterCodeFix( + CodeAction.Create( + "Add DisableRuntimeMarshallingAttribute to the assembly.", + ct => AddDisableRuntimeMarshallingAttributeApplicationToProject(context.Document.Project, ct), + EquivalenceKey), + fixedDiagnostics); + } + + return Task.CompletedTask; + + static bool IsRequiresDiableRuntimeMarshallingDiagnostic(Diagnostic diagnostic) + { + return diagnostic.Properties.ContainsKey(GeneratorDiagnosticProperties.AddDisableRuntimeMarshallingAttribute); + } + } + + private static async Task AddDisableRuntimeMarshallingAttributeApplicationToProject(Project project, CancellationToken cancellationToken) + { + Document? assemblyInfo = project.Documents.FirstOrDefault(IsPropertiesAssemblyInfo); + + if (assemblyInfo is null) + { + assemblyInfo = project.AddDocument(AssemblyInfoFileName, "", folders: new[] { PropertiesFolderName }); + } + + DocumentEditor editor = await DocumentEditor.CreateAsync(assemblyInfo, cancellationToken).ConfigureAwait(false); + + var syntaxRoot = await assemblyInfo.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + editor.ReplaceNode( + syntaxRoot, + editor.Generator.AddAttributes( + syntaxRoot, + editor.Generator.Attribute(editor.Generator.DottedName(TypeNames.System_Runtime_CompilerServices_DisableRuntimeMarshallingAttribute)))); + + return editor.GetChangedDocument().Project.Solution; + + static bool IsPropertiesAssemblyInfo(Document document) + { + // We specifically want to match a file in the Properties folder with the provided name (AssemblyInfo.cs) to match other VS templates that add this file. + // We are very strict about this to ensure that we discover the correct file when it is already created and added to the project. + return document.Name == AssemblyInfoFileName + && document.Folders.Count == 1 + && document.Folders[0] == PropertiesFolderName; + } + } + } +} diff --git a/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/GeneratorDiagnostics.cs b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/GeneratorDiagnostics.cs index 8908b9068faffd..b17fd14207cc8d 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/GeneratorDiagnostics.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/GeneratorDiagnostics.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; namespace Microsoft.Interop @@ -226,7 +227,8 @@ public void ReportConfigurationNotSupported( public void ReportMarshallingNotSupported( MethodDeclarationSyntax method, TypePositionInfo info, - string? notSupportedDetails) + string? notSupportedDetails, + ImmutableDictionary diagnosticProperties) { Location diagnosticLocation = Location.None; string elementName = string.Empty; @@ -252,6 +254,7 @@ public void ReportMarshallingNotSupported( _diagnostics.Add( diagnosticLocation.CreateDiagnostic( GeneratorDiagnostics.ReturnTypeNotSupportedWithDetails, + diagnosticProperties, notSupportedDetails!, elementName)); } @@ -260,6 +263,7 @@ public void ReportMarshallingNotSupported( _diagnostics.Add( diagnosticLocation.CreateDiagnostic( GeneratorDiagnostics.ParameterTypeNotSupportedWithDetails, + diagnosticProperties, notSupportedDetails!, elementName)); } @@ -274,6 +278,7 @@ public void ReportMarshallingNotSupported( _diagnostics.Add( diagnosticLocation.CreateDiagnostic( GeneratorDiagnostics.ReturnConfigurationNotSupported, + diagnosticProperties, nameof(System.Runtime.InteropServices.MarshalAsAttribute), elementName)); } @@ -282,6 +287,7 @@ public void ReportMarshallingNotSupported( _diagnostics.Add( diagnosticLocation.CreateDiagnostic( GeneratorDiagnostics.ParameterConfigurationNotSupported, + diagnosticProperties, nameof(System.Runtime.InteropServices.MarshalAsAttribute), elementName)); } @@ -294,6 +300,7 @@ public void ReportMarshallingNotSupported( _diagnostics.Add( diagnosticLocation.CreateDiagnostic( GeneratorDiagnostics.ReturnTypeNotSupported, + diagnosticProperties, info.ManagedType.DiagnosticFormattedName, elementName)); } @@ -302,6 +309,7 @@ public void ReportMarshallingNotSupported( _diagnostics.Add( diagnosticLocation.CreateDiagnostic( GeneratorDiagnostics.ParameterTypeNotSupported, + diagnosticProperties, info.ManagedType.DiagnosticFormattedName, elementName)); } diff --git a/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs index b5e1a79da2d1b4..979b4a0e359026 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs @@ -501,7 +501,7 @@ private static (MemberDeclarationSyntax, ImmutableArray) GenerateSou pinvokeStub.LibraryImportData.SetLastError && !options.GenerateForwarders, (elementInfo, ex) => { - diagnostics.ReportMarshallingNotSupported(originalSyntax, elementInfo, ex.NotSupportedDetails); + diagnostics.ReportMarshallingNotSupported(originalSyntax, elementInfo, ex.NotSupportedDetails, ex.DiagnosticProperties ?? ImmutableDictionary.Empty); }, pinvokeStub.StubContext.GeneratorFactory); diff --git a/src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/IGeneratorDiagnostics.cs b/src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/IGeneratorDiagnostics.cs index 3319306b29bd0a..bb1a051199192c 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/IGeneratorDiagnostics.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/IGeneratorDiagnostics.cs @@ -75,7 +75,7 @@ public static Diagnostic CreateDiagnostic( } return firstLocation is null ? - Diagnostic.Create(descriptor, Location.None, args) : + Diagnostic.Create(descriptor, Location.None, properties: properties, args) : Diagnostic.Create(descriptor, firstLocation, additionalLocations, properties, args); } @@ -89,22 +89,24 @@ public static Diagnostic CreateDiagnostic( location: location.IsInSource ? location : Location.None, messageArgs: args); } + + public static Diagnostic CreateDiagnostic( + this Location location, + DiagnosticDescriptor descriptor, + ImmutableDictionary properties, + params object[] args) + { + return Diagnostic.Create( + descriptor, + location: location.IsInSource ? location : Location.None, + properties: properties, + messageArgs: args); + } } public interface IGeneratorDiagnostics { - /// - /// Report diagnostic for marshalling of a parameter/return that is not supported - /// - /// Method with the parameter/return - /// Type info for the parameter/return - /// [Optional] Specific reason for lack of support - void ReportMarshallingNotSupported( - MethodDeclarationSyntax method, - TypePositionInfo info, - string? notSupportedDetails); - /// /// Report diagnostic for configuration that is not supported by the DLL import source generator /// @@ -127,4 +129,9 @@ public static class IGeneratorDiagnosticsExtensions public static void ReportConfigurationNotSupported(this IGeneratorDiagnostics diagnostics, AttributeData attributeData, string configurationName) => diagnostics.ReportConfigurationNotSupported(attributeData, configurationName, null); } + + public class GeneratorDiagnosticProperties + { + public const string AddDisableRuntimeMarshallingAttribute = nameof(AddDisableRuntimeMarshallingAttribute); + } } diff --git a/src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/Marshalling/AttributedMarshallingModelGeneratorFactory.cs b/src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/Marshalling/AttributedMarshallingModelGeneratorFactory.cs index 95bf75c7a5b92b..ffff64e4d841bc 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/Marshalling/AttributedMarshallingModelGeneratorFactory.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/Marshalling/AttributedMarshallingModelGeneratorFactory.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Runtime.InteropServices; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -15,6 +16,9 @@ namespace Microsoft.Interop public class AttributedMarshallingModelGeneratorFactory : IMarshallingGeneratorFactory { + private static readonly ImmutableDictionary AddDisableRuntimeMarshallingAttributeProperties = + ImmutableDictionary.Empty.Add(GeneratorDiagnosticProperties.AddDisableRuntimeMarshallingAttribute, GeneratorDiagnosticProperties.AddDisableRuntimeMarshallingAttribute); + private static readonly BlittableMarshaller s_blittable = new BlittableMarshaller(); private static readonly Forwarder s_forwarder = new Forwarder(); @@ -55,7 +59,8 @@ public IMarshallingGenerator Create(TypePositionInfo info, StubCodeContext conte UnmanagedBlittableMarshallingInfo or NativeMarshallingAttributeInfo when !Options.RuntimeMarshallingDisabled => throw new MarshallingNotSupportedException(info, context) { - NotSupportedDetails = SR.RuntimeMarshallingMustBeDisabled + NotSupportedDetails = SR.RuntimeMarshallingMustBeDisabled, + DiagnosticProperties = AddDisableRuntimeMarshallingAttributeProperties }, GeneratedNativeMarshallingAttributeInfo => s_forwarder, MissingSupportMarshallingInfo => s_forwarder, diff --git a/src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/Marshalling/MarshallingGenerator.cs b/src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/Marshalling/MarshallingGenerator.cs index 65ef66ce0438b3..2f622da69344d5 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/Marshalling/MarshallingGenerator.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/Marshalling/MarshallingGenerator.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; @@ -160,5 +161,10 @@ public MarshallingNotSupportedException(TypePositionInfo info, StubCodeContext c /// [Optional] Specific reason marshalling of the supplied type isn't supported. /// public string? NotSupportedDetails { get; init; } + + /// + /// [Optional] Properties to attach to any diagnostic emitted due to this exception. + /// + public ImmutableDictionary? DiagnosticProperties { get; init; } } } diff --git a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/AddDisableRuntimeMarshallingAttributeFixerTests.cs b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/AddDisableRuntimeMarshallingAttributeFixerTests.cs new file mode 100644 index 00000000000000..c905bfa14ef04c --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/AddDisableRuntimeMarshallingAttributeFixerTests.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing.Model; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using Microsoft.Interop.Analyzers; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Interop; + +using VerifyCS = LibraryImportGenerator.UnitTests.Verifiers.CSharpCodeFixVerifier< + LibraryImportGenerator.UnitTests.AddDisableRuntimeMarshallingAttributeFixerTests.MockAnalyzer, + Microsoft.Interop.Analyzers.AddDisableRuntimeMarshallingAttributeFixer>; +using Xunit; +using System.IO; + +namespace LibraryImportGenerator.UnitTests +{ + public class AddDisableRuntimeMarshallingAttributeFixerTests + { + [Fact] + public static async Task Adds_NewFile_With_Attribute() + { + // Source will have CS8795 (Partial method must have an implementation) without generator run + var source = @" +using System.Runtime.InteropServices; +partial class Foo +{ + [LibraryImport(""Foo"")] + public static partial void {|CS8795:PInvoke|}(S {|#0:s|}); +} + +[NativeMarshalling(typeof(Native))] +struct S +{ +} + +[CustomTypeMarshaller(typeof(S))] +struct Native +{ + public Native(S s) {} + + public S ToManaged() => new S(); +} +"; + var expectedPropertiesFile = "[assembly: System.Runtime.CompilerServices.DisableRuntimeMarshalling]" + Environment.NewLine; + + var diagnostic = VerifyCS.Diagnostic(GeneratorDiagnostics.Ids.TypeNotSupported).WithLocation(0).WithArguments("S", "s"); + await VerifyCodeFixAsync(source, propertiesFile: null, expectedPropertiesFile, diagnostic); + } + + [Fact] + public static async Task Appends_Attribute_To_Existing_AssemblyInfo_File() + { + var source = @" +using System.Runtime.InteropServices; +partial class Foo +{ + [LibraryImport(""Foo"")] + public static partial void {|CS8795:PInvoke|}(S {|#0:s|}); +} + +[NativeMarshalling(typeof(Native))] +struct S +{ +} + +[CustomTypeMarshaller(typeof(S))] +struct Native +{ + public Native(S s) {} + + public S ToManaged() => new S(); +} +"; + var propertiesFile = @" +using System.Reflection; + +[assembly: AssemblyMetadata(""MyMetadata"", ""Value"")] +"; + var expectedPropertiesFile = @" +using System.Reflection; + +[assembly: AssemblyMetadata(""MyMetadata"", ""Value"")] +[assembly: System.Runtime.CompilerServices.DisableRuntimeMarshalling] +"; + + var diagnostic = VerifyCS.Diagnostic(GeneratorDiagnostics.Ids.TypeNotSupported).WithLocation(0).WithArguments("S", "s"); + await VerifyCodeFixAsync(source, propertiesFile, expectedPropertiesFile, diagnostic); + } + + private static async Task VerifyCodeFixAsync(string source, string? propertiesFile, string? expectedPropertiesFile, DiagnosticResult diagnostic) + { + var test = new Test(); + // We don't care about validating the settings for the MockAnalyzer and we're also hitting failures on Mono + // with this check in this case, so skip the check for now. + test.TestBehaviors = TestBehaviors.SkipGeneratedCodeCheck; + test.TestCode = source; + test.FixedCode = source; + test.BatchFixedCode = source; + test.ExpectedDiagnostics.Add(diagnostic); + if (propertiesFile is not null) + { + test.TestState.Sources.Add(($"{Test.FilePathPrefix}Properties{Path.DirectorySeparatorChar}AssemblyInfo.cs", propertiesFile)); + } + if (expectedPropertiesFile is not null) + { + test.FixedState.Sources.Add(($"{Test.FilePathPrefix}Properties{Path.DirectorySeparatorChar}AssemblyInfo.cs", expectedPropertiesFile)); + test.BatchFixedState.Sources.Add(($"{Test.FilePathPrefix}Properties{Path.DirectorySeparatorChar}AssemblyInfo.cs", expectedPropertiesFile)); + } + await test.RunAsync(); + } + + class Test : VerifyCS.Test + { + public const string FilePathPrefix = "/Project/"; + + protected override string DefaultFilePathPrefix => FilePathPrefix; + } + + // The Roslyn SDK doesn't provide a good test harness for testing a code fix that triggers + // on a source-generator-introduced diagnostic. This analyzer does a decent enough job of triggering + // the specific diagnostic in the right place for us to test the code fix. + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class MockAnalyzer : DiagnosticAnalyzer + { + private static readonly DiagnosticDescriptor AddDisableRuntimeMarshallingAttributeRule = GeneratorDiagnostics.ParameterTypeNotSupported; + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(AddDisableRuntimeMarshallingAttributeRule); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.RegisterSymbolAction(context => + { + var symbol = (IParameterSymbol)context.Symbol; + + if (context.Symbol.ContainingAssembly.GetAttributes().Any(attr => attr.AttributeClass!.ToDisplayString() == TypeNames.System_Runtime_CompilerServices_DisableRuntimeMarshallingAttribute)) + { + return; + } + + if (symbol.ContainingSymbol is IMethodSymbol { IsStatic: true, IsPartialDefinition: true }) + { + context.ReportDiagnostic(context.Symbol.CreateDiagnostic( + AddDisableRuntimeMarshallingAttributeRule, + ImmutableDictionary.Empty + .Add( + GeneratorDiagnosticProperties.AddDisableRuntimeMarshallingAttribute, + GeneratorDiagnosticProperties.AddDisableRuntimeMarshallingAttribute), + symbol.Type.ToDisplayString(), symbol.Name)); + } + }, SymbolKind.Parameter); + } + } + } +}