diff --git a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs index 64cc64aaa57796..ff85fad5fcf43b 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs @@ -41,7 +41,7 @@ public sealed class DownlevelLibraryImportDiagnosticsAnalyzer : DiagnosticAnalyz public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(context => { @@ -96,6 +96,12 @@ private static bool AnalyzeMethod(SymbolAnalysisContext context, StubEnvironment { IMethodSymbol method = (IMethodSymbol)context.Symbol; + // With Analyze | ReportDiagnostics, the callback fires for both the partial definition + // and the partial implementation as separate symbols. Skip the implementation part to + // avoid reporting duplicate diagnostics — we only want to report from the definition. + if (method.PartialDefinitionPart is not null) + return false; + // Only analyze methods with LibraryImportAttribute AttributeData? libraryImportAttr = null; foreach (AttributeData attr in method.GetAttributes()) @@ -110,12 +116,20 @@ private static bool AnalyzeMethod(SymbolAnalysisContext context, StubEnvironment if (libraryImportAttr is null) return false; - // Find the method syntax + bool isGeneratedByOurGenerator = IsGeneratedByOurGenerator(method); + + // Find the method syntax - prefer the partial declaration (no body) over the generated implementation. + // With Analyze | ReportDiagnostics, both parts of a partial method appear in DeclaringSyntaxReferences. + // If we encounter the generated implementation (which has a body and is marked with [GeneratedCode] + // from our generator), skip it to find the user's partial declaration. foreach (SyntaxReference syntaxRef in method.DeclaringSyntaxReferences) { if (syntaxRef.GetSyntax(context.CancellationToken) is MethodDeclarationSyntax methodSyntax) { - AnalyzeMethodSyntax(context, methodSyntax, method, libraryImportAttr, env); + if (methodSyntax.Body is not null && isGeneratedByOurGenerator) + continue; + + AnalyzeMethodSyntax(context, methodSyntax, method, libraryImportAttr, env, isGeneratedByOurGenerator); break; } } @@ -123,27 +137,58 @@ private static bool AnalyzeMethod(SymbolAnalysisContext context, StubEnvironment return true; } + private static bool IsGeneratedByOurGenerator(IMethodSymbol method) + { + foreach (AttributeData attr in method.GetAttributes()) + { + // The generator marks its output with [GeneratedCode("Microsoft.Interop.LibraryImportGenerator...", "...")] + if (attr.AttributeClass is { Name: "GeneratedCodeAttribute", ContainingNamespace.Name: "Compiler", ContainingNamespace.ContainingNamespace.Name: "CodeDom", ContainingNamespace.ContainingNamespace.ContainingNamespace.Name: "System" } + && attr.ConstructorArguments.Length >= 1 + && attr.ConstructorArguments[0].Value is string toolName + && toolName.StartsWith("Microsoft.Interop.LibraryImportGenerator", StringComparison.Ordinal)) + { + return true; + } + } + return false; + } + private static void AnalyzeMethodSyntax( SymbolAnalysisContext context, MethodDeclarationSyntax methodSyntax, IMethodSymbol method, AttributeData libraryImportAttr, - StubEnvironment env) + StubEnvironment env, + bool skipInvalidMethodCheck) { - // Check for invalid method signature - DiagnosticInfo? invalidMethodDiagnostic = GetDiagnosticIfInvalidMethodForGeneration(methodSyntax, method); - if (invalidMethodDiagnostic is not null) + // When our generator has already produced an implementation, skip the signature validity check: + // the method must already have been valid for the generator to run. We still run the rest of + // the analysis (CalculateDiagnostics) to catch other issues. + if (!skipInvalidMethodCheck) { - context.ReportDiagnostic(invalidMethodDiagnostic.ToDiagnostic()); - return; // Don't continue analysis if the method is invalid + DiagnosticInfo? invalidMethodDiagnostic = GetDiagnosticIfInvalidMethodForGeneration(methodSyntax, method); + if (invalidMethodDiagnostic is not null) + { + context.ReportDiagnostic(invalidMethodDiagnostic.ToDiagnostic()); + return; // Don't continue analysis if the method is invalid + } } // Calculate stub information and collect diagnostics var diagnostics = CalculateDiagnostics(methodSyntax, method, libraryImportAttr, env, context.CancellationToken); + SyntaxTree userSyntaxTree = methodSyntax.SyntaxTree; foreach (DiagnosticInfo diagnostic in diagnostics) { - context.ReportDiagnostic(diagnostic.ToDiagnostic()); + // Only report diagnostics located in the user's (non-generated) source. + // With Analyze | ReportDiagnostics, some diagnostics may have locations in + // generated source; we must filter those out to avoid duplicates. + if (diagnostic.Location is null + || !diagnostic.Location.IsInSource + || diagnostic.Location.SourceTree == userSyntaxTree) + { + context.ReportDiagnostic(diagnostic.ToDiagnostic()); + } } } diff --git a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs index aa84f8615811f9..f549251c4924c4 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs @@ -320,6 +320,35 @@ private static LocalFunctionStatementSyntax CreateTargetDllImportAsLocalStatemen string stubTargetName, string stubMethodName) { + var dllImportArgs = new List + { + AttributeArgument(LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal(libraryImportData.ModuleName))), + AttributeArgument( + NameEquals(nameof(DllImportAttribute.EntryPoint)), + null, + LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal(libraryImportData.EntryPoint ?? stubMethodName))), + AttributeArgument( + NameEquals(nameof(DllImportAttribute.ExactSpelling)), + null, + LiteralExpression(SyntaxKind.TrueLiteralExpression)) + }; + + // When StringMarshalling.Utf16 is specified, forward CharSet = Unicode to the inner + // DllImport. This ensures that any types forwarded to the runtime marshaller (e.g. + // char parameters) use the correct encoding instead of defaulting to Ansi. + if (libraryImportData.IsUserDefined.HasFlag(InteropAttributeMember.StringMarshalling) + && libraryImportData.StringMarshalling == StringMarshalling.Utf16) + { + dllImportArgs.Add(AttributeArgument( + NameEquals(nameof(DllImportAttribute.CharSet)), + null, + CreateEnumExpressionSyntax(CharSet.Unicode))); + } + (ParameterListSyntax parameterList, TypeSyntax returnType, AttributeListSyntax returnTypeAttributes) = stubGenerator.GenerateTargetMethodSignatureData(); LocalFunctionStatementSyntax localDllImport = LocalFunctionStatement(returnType, stubTargetName) .AddModifiers( @@ -333,23 +362,7 @@ private static LocalFunctionStatementSyntax CreateTargetDllImportAsLocalStatemen Attribute( NameSyntaxes.DllImportAttribute, AttributeArgumentList( - SeparatedList( - [ - AttributeArgument(LiteralExpression( - SyntaxKind.StringLiteralExpression, - Literal(libraryImportData.ModuleName))), - AttributeArgument( - NameEquals(nameof(DllImportAttribute.EntryPoint)), - null, - LiteralExpression( - SyntaxKind.StringLiteralExpression, - Literal(libraryImportData.EntryPoint ?? stubMethodName))), - AttributeArgument( - NameEquals(nameof(DllImportAttribute.ExactSpelling)), - null, - LiteralExpression(SyntaxKind.TrueLiteralExpression)) - ] - ))))))) + SeparatedList(dllImportArgs))))))) .WithParameterList(parameterList); if (returnTypeAttributes is not null) { @@ -379,10 +392,7 @@ private static AttributeSyntax CreateForwarderDllImport(LibraryImportData target { Debug.Assert(target.StringMarshalling == StringMarshalling.Utf16); NameEqualsSyntax name = NameEquals(nameof(DllImportAttribute.CharSet)); - ExpressionSyntax value = MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - AliasQualifiedName("global", IdentifierName(typeof(CharSet).FullName)), - IdentifierName(nameof(CharSet.Unicode))); + ExpressionSyntax value = CreateEnumExpressionSyntax(CharSet.Unicode); newAttributeArgs.Add(AttributeArgument(name, null, value)); } @@ -414,5 +424,13 @@ static ExpressionSyntax CreateStringExpressionSyntax(string str) } } + private static MemberAccessExpressionSyntax CreateEnumExpressionSyntax(T value) where T : Enum + { + return MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + AliasQualifiedName("global", IdentifierName(typeof(T).FullName)), + IdentifierName(value.ToString())); + } + } } diff --git a/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.cs b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.cs index b1f751cf5935b2..b959f7145e12db 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.cs @@ -47,7 +47,7 @@ public class LibraryImportDiagnosticsAnalyzer : DiagnosticAnalyzer public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(context => { @@ -94,6 +94,12 @@ private static bool AnalyzeMethod(SymbolAnalysisContext context, StubEnvironment { IMethodSymbol method = (IMethodSymbol)context.Symbol; + // With Analyze | ReportDiagnostics, the callback fires for both the partial definition + // and the partial implementation as separate symbols. Skip the implementation part to + // avoid reporting duplicate diagnostics — we only want to report from the definition. + if (method.PartialDefinitionPart is not null) + return false; + // Only analyze methods with LibraryImportAttribute AttributeData? libraryImportAttr = null; foreach (AttributeData attr in method.GetAttributes()) @@ -108,12 +114,20 @@ private static bool AnalyzeMethod(SymbolAnalysisContext context, StubEnvironment if (libraryImportAttr is null) return false; - // Find the method syntax + bool isGeneratedByOurGenerator = IsGeneratedByOurGenerator(method); + + // Find the method syntax - prefer the partial declaration (no body) over the generated implementation. + // With Analyze | ReportDiagnostics, both parts of a partial method appear in DeclaringSyntaxReferences. + // If we encounter the generated implementation (which has a body and is marked with [GeneratedCode] + // from our generator), skip it to find the user's partial declaration. foreach (SyntaxReference syntaxRef in method.DeclaringSyntaxReferences) { if (syntaxRef.GetSyntax(context.CancellationToken) is MethodDeclarationSyntax methodSyntax) { - AnalyzeMethodSyntax(context, methodSyntax, method, libraryImportAttr, env, options); + if (methodSyntax.Body is not null && isGeneratedByOurGenerator) + continue; + + AnalyzeMethodSyntax(context, methodSyntax, method, libraryImportAttr, env, options, isGeneratedByOurGenerator); break; } } @@ -121,20 +135,42 @@ private static bool AnalyzeMethod(SymbolAnalysisContext context, StubEnvironment return true; } + private static bool IsGeneratedByOurGenerator(IMethodSymbol method) + { + foreach (AttributeData attr in method.GetAttributes()) + { + // The generator marks its output with [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "...")] + if (attr.AttributeClass is { Name: "GeneratedCodeAttribute", ContainingNamespace.Name: "Compiler", ContainingNamespace.ContainingNamespace.Name: "CodeDom", ContainingNamespace.ContainingNamespace.ContainingNamespace.Name: "System" } + && attr.ConstructorArguments.Length >= 1 + && attr.ConstructorArguments[0].Value is string toolName + && toolName.StartsWith("Microsoft.Interop.LibraryImportGenerator", StringComparison.Ordinal)) + { + return true; + } + } + return false; + } + private static void AnalyzeMethodSyntax( SymbolAnalysisContext context, MethodDeclarationSyntax methodSyntax, IMethodSymbol method, AttributeData libraryImportAttr, StubEnvironment env, - LibraryImportGeneratorOptions options) + LibraryImportGeneratorOptions options, + bool skipInvalidMethodCheck) { - // Check for invalid method signature - DiagnosticInfo? invalidMethodDiagnostic = GetDiagnosticIfInvalidMethodForGeneration(methodSyntax, method); - if (invalidMethodDiagnostic is not null) + // When our generator has already produced an implementation, skip the signature validity check: + // the method must already have been valid for the generator to run. We still run the rest of + // the analysis (CalculateDiagnostics) to catch other issues. + if (!skipInvalidMethodCheck) { - context.ReportDiagnostic(invalidMethodDiagnostic.ToDiagnostic()); - return; // Don't continue analysis if the method is invalid + DiagnosticInfo? invalidMethodDiagnostic = GetDiagnosticIfInvalidMethodForGeneration(methodSyntax, method); + if (invalidMethodDiagnostic is not null) + { + context.ReportDiagnostic(invalidMethodDiagnostic.ToDiagnostic()); + return; // Don't continue analysis if the method is invalid + } } // Note: RequiresAllowUnsafeBlocks is reported once per compilation in Initialize method @@ -142,9 +178,18 @@ private static void AnalyzeMethodSyntax( // Calculate stub information and collect diagnostics var diagnostics = CalculateDiagnostics(methodSyntax, method, libraryImportAttr, env, options, context.CancellationToken); + SyntaxTree userSyntaxTree = methodSyntax.SyntaxTree; foreach (DiagnosticInfo diagnostic in diagnostics) { - context.ReportDiagnostic(diagnostic.ToDiagnostic()); + // Only report diagnostics located in the user's (non-generated) source. + // With Analyze | ReportDiagnostics, some diagnostics may have locations in + // generated source; we must filter those out to avoid duplicates. + if (diagnostic.Location is null + || !diagnostic.Location.IsInSource + || diagnostic.Location.SourceTree == userSyntaxTree) + { + context.ReportDiagnostic(diagnostic.ToDiagnostic()); + } } } diff --git a/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs index aee232c0d9d7b0..0b54b47f4987df 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs @@ -375,6 +375,35 @@ private static LocalFunctionStatementSyntax CreateTargetDllImportAsLocalStatemen { Debug.Assert(!options.GenerateForwarders, "GenerateForwarders should have already been handled to use a forwarder stub"); + var dllImportArgs = new List + { + AttributeArgument(LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal(libraryImportData.ModuleName))), + AttributeArgument( + NameEquals(nameof(DllImportAttribute.EntryPoint)), + null, + LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal(libraryImportData.EntryPoint ?? stubMethodName))), + AttributeArgument( + NameEquals(nameof(DllImportAttribute.ExactSpelling)), + null, + LiteralExpression(SyntaxKind.TrueLiteralExpression)) + }; + + // When StringMarshalling.Utf16 is specified, forward CharSet = Unicode to the inner + // DllImport. This ensures that any types forwarded to the runtime marshaller (e.g. + // StringBuilder) use the correct encoding instead of defaulting to Ansi. + if (libraryImportData.IsUserDefined.HasFlag(InteropAttributeMember.StringMarshalling) + && libraryImportData.StringMarshalling == StringMarshalling.Utf16) + { + dllImportArgs.Add(AttributeArgument( + NameEquals(nameof(DllImportAttribute.CharSet)), + null, + CreateEnumExpressionSyntax(CharSet.Unicode))); + } + (ParameterListSyntax parameterList, TypeSyntax returnType, AttributeListSyntax returnTypeAttributes) = stubGenerator.GenerateTargetMethodSignatureData(); LocalFunctionStatementSyntax localDllImport = LocalFunctionStatement(returnType, stubTargetName) .AddModifiers( @@ -388,24 +417,7 @@ private static LocalFunctionStatementSyntax CreateTargetDllImportAsLocalStatemen Attribute( NameSyntaxes.DllImportAttribute, AttributeArgumentList( - SeparatedList( - new[] - { - AttributeArgument(LiteralExpression( - SyntaxKind.StringLiteralExpression, - Literal(libraryImportData.ModuleName))), - AttributeArgument( - NameEquals(nameof(DllImportAttribute.EntryPoint)), - null, - LiteralExpression( - SyntaxKind.StringLiteralExpression, - Literal(libraryImportData.EntryPoint ?? stubMethodName))), - AttributeArgument( - NameEquals(nameof(DllImportAttribute.ExactSpelling)), - null, - LiteralExpression(SyntaxKind.TrueLiteralExpression)) - } - ))))))) + SeparatedList(dllImportArgs))))))) .WithParameterList(parameterList); if (returnTypeAttributes is not null) { @@ -465,14 +477,14 @@ static ExpressionSyntax CreateStringExpressionSyntax(string str) SyntaxKind.StringLiteralExpression, Literal(str)); } + } - static ExpressionSyntax CreateEnumExpressionSyntax(T value) where T : Enum - { - return MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - AliasQualifiedName("global", IdentifierName(typeof(T).FullName)), - IdentifierName(value.ToString())); - } + private static MemberAccessExpressionSyntax CreateEnumExpressionSyntax(T value) where T : Enum + { + return MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + AliasQualifiedName("global", IdentifierName(typeof(T).FullName)), + IdentifierName(value.ToString())); } } } diff --git a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs index 81b10efc82d4af..96f0128cf04b35 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs +++ b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs @@ -686,6 +686,67 @@ protected override void VerifyFinalCompilation(Compilation compilation) } } + [Theory] + [InlineData("StringMarshalling.Utf16")] + [InlineData("StringMarshalling.Utf8")] + public async Task ForwardedTypesWithStringMarshalling_InnerDllImportHasCharSet(string stringMarshalling) + { + bool expectCharSetUnicode = stringMarshalling == "StringMarshalling.Utf16"; + string source = $$""" + using System.Runtime.InteropServices; + partial class Test + { + [LibraryImport("DoesNotExist", StringMarshalling = {{stringMarshalling}})] + public static partial string Method(string s, int i); + } + """; + + var test = new InnerDllImportCharSetTest(expectCharSetUnicode) + { + TestCode = source, + TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck + }; + + await test.RunAsync(); + } + + class InnerDllImportCharSetTest : VerifyCS.Test + { + private readonly bool _expectCharSetUnicode; + + public InnerDllImportCharSetTest(bool expectCharSetUnicode) + : base(referenceAncillaryInterop: false) + { + _expectCharSetUnicode = expectCharSetUnicode; + } + + protected override void VerifyFinalCompilation(Compilation compilation) + { + SyntaxTree generatedCode = compilation.SyntaxTrees.Last(); + var localFunctions = generatedCode.GetRoot() + .DescendantNodes().OfType() + .ToList(); + + LocalFunctionStatementSyntax innerDllImport = Assert.Single(localFunctions); + AttributeSyntax dllImportAttr = innerDllImport.AttributeLists + .SelectMany(al => al.Attributes) + .Single(a => a.Name.ToString().EndsWith("DllImportAttribute") || a.Name.ToString() == "DllImport"); + + AttributeArgumentSyntax? charSetArgument = dllImportAttr.ArgumentList!.Arguments + .SingleOrDefault(a => a.NameEquals?.Name.Identifier.Text == nameof(DllImportAttribute.CharSet)); + + if (_expectCharSetUnicode) + { + Assert.NotNull(charSetArgument); + Assert.EndsWith($"{nameof(CharSet)}.{nameof(CharSet.Unicode)}", charSetArgument.Expression.ToString()); + } + else + { + Assert.Null(charSetArgument); + } + } + } + public static IEnumerable CodeSnippetsToCompileWithMarshalType() { yield break; diff --git a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Diagnostics.cs b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Diagnostics.cs index 9894e9141ed14d..15aeae1626a871 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Diagnostics.cs +++ b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Diagnostics.cs @@ -413,6 +413,60 @@ await VerifyCS.VerifyAnalyzerAsync(source, .WithArguments("Method", "Test")); } + [Theory] + [InlineData("StringMarshalling = StringMarshalling.Utf16")] + [InlineData("StringMarshalling = StringMarshalling.Utf8")] + [InlineData("")] + public async Task StringBuilderNotSupported_ReportsDiagnostic(string stringMarshallingArg) + { + string marshallingPart = string.IsNullOrEmpty(stringMarshallingArg) + ? "" + : $", {stringMarshallingArg}"; + + // StringBuilder as a simple parameter + string source = $$""" + + using System.Runtime.InteropServices; + using System.Text; + partial class Test + { + [LibraryImport("DoesNotExist"{{marshallingPart}})] + public static partial void Method(StringBuilder {|#0:sb|}); + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, + VerifyCS.Diagnostic(GeneratorDiagnostics.ParameterTypeNotSupported) + .WithLocation(0) + .WithArguments("System.Text.StringBuilder", "sb")); + } + + [Theory] + [InlineData("StringMarshalling = StringMarshalling.Utf16")] + [InlineData("StringMarshalling = StringMarshalling.Utf8")] + public async Task StringBuilderNotSupported_WithStringParam_ReportsDiagnostic(string stringMarshallingArg) + { + // StringBuilder with [Out] alongside a string parameter + string source = $$""" + + using System.Runtime.InteropServices; + using System.Text; + partial class Test + { + [LibraryImport("DoesNotExist", {{stringMarshallingArg}})] + internal static partial int Method( + string volumeMountPoint, + [Out] StringBuilder {|#0:volumeName|}, + int bufferLength); + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source, + VerifyCS.Diagnostic(GeneratorDiagnostics.ParameterTypeNotSupported) + .WithLocation(0) + .WithArguments("System.Text.StringBuilder", "volumeName")); + } + private static void VerifyDiagnostics(DiagnosticResult[] expectedDiagnostics, Diagnostic[] actualDiagnostics) { Assert.True(expectedDiagnostics.Length == actualDiagnostics.Length,