From b00aca30acb35a7829f9d4b8954eb4772347df78 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:08:32 +0000 Subject: [PATCH 01/12] Initial plan From c95a144ac32d1c2c41c9afe3475071f65263d46c Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 9 Apr 2026 09:45:40 -0700 Subject: [PATCH 02/12] Add regression tests for StringBuilder SYSLIB1051 with StringMarshalling Add tests verifying that SYSLIB1051 (ParameterTypeNotSupported) is correctly reported for StringBuilder parameters when StringMarshalling is set to Utf16 or Utf8, both as standalone parameters and alongside string parameters with [Out] attribute. Regression test for https://github.com/dotnet/runtime/issues/126687 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Diagnostics.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) 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..f9ad62ee530807 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 (repro from dotnet/runtime#126687) + 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, From d1caf828ff88fea1b658b0c20a41d8f3b988c96a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 9 Apr 2026 14:01:38 -0700 Subject: [PATCH 03/12] Forward CharSet=Unicode to inner DllImport when StringMarshalling.Utf16 is set When LibraryImportGenerator creates a non-forwarder stub with an inner local DllImport function, it did not forward StringMarshalling.Utf16 as CharSet=Unicode. This caused any types forwarded to the runtime marshaller (e.g. StringBuilder) to default to Ansi encoding, producing incorrect results for Unicode APIs. Add CharSet=CharSet.Unicode to the inner DllImport attribute when StringMarshalling.Utf16 is specified on the LibraryImport, matching the existing behavior in CreateForwarderDllImport. Fix for https://github.com/dotnet/runtime/issues/126687 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../LibraryImportGenerator.cs | 51 +++++++++++------- .../Compiles.cs | 53 +++++++++++++++++++ 2 files changed, 86 insertions(+), 18 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs index aee232c0d9d7b0..2945dc220f6e78 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs @@ -375,6 +375,38 @@ 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, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + AliasQualifiedName("global", IdentifierName(typeof(CharSet).FullName)), + IdentifierName(nameof(CharSet.Unicode))))); + } + (ParameterListSyntax parameterList, TypeSyntax returnType, AttributeListSyntax returnTypeAttributes) = stubGenerator.GenerateTargetMethodSignatureData(); LocalFunctionStatementSyntax localDllImport = LocalFunctionStatement(returnType, stubTargetName) .AddModifiers( @@ -388,24 +420,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) { 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..5018c38607b4aa 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,59 @@ 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().Contains("DllImport")); + + bool hasCharSet = dllImportAttr.ArgumentList!.Arguments + .Any(a => a.NameEquals?.Name.Identifier.Text == nameof(DllImportAttribute.CharSet)); + + Assert.Equal(_expectCharSetUnicode, hasCharSet); + } + } + public static IEnumerable CodeSnippetsToCompileWithMarshalType() { yield break; From 69a3b5ce4874b1dec2ec491aa22694b3a48c06d7 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 10 Apr 2026 10:51:43 -0700 Subject: [PATCH 04/12] Allow analyzing generated code. Since we put the GeneratedCodeAttribute on the stub implementation, the user part of the partial is also marked as generated code. We don't need to pass ReportDiagnostics here as we will report diagnostics in the user part. --- .../Analyzers/LibraryImportDiagnosticsAnalyzer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..18f63618519141 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); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(context => { From f135f3e8d1fce8bde2e4fe7687cd7e4ec13723b7 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 10 Apr 2026 10:54:13 -0700 Subject: [PATCH 05/12] Update src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Diagnostics.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../tests/LibraryImportGenerator.UnitTests/Diagnostics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f9ad62ee530807..15aeae1626a871 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Diagnostics.cs +++ b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Diagnostics.cs @@ -446,7 +446,7 @@ await VerifyCS.VerifyAnalyzerAsync(source, [InlineData("StringMarshalling = StringMarshalling.Utf8")] public async Task StringBuilderNotSupported_WithStringParam_ReportsDiagnostic(string stringMarshallingArg) { - // StringBuilder with [Out] alongside a string parameter (repro from dotnet/runtime#126687) + // StringBuilder with [Out] alongside a string parameter string source = $$""" using System.Runtime.InteropServices; From 4302baeda253b00c1ff6a150e3d8f9b2e8548e86 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 10 Apr 2026 14:05:19 -0700 Subject: [PATCH 06/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../LibraryImportGenerator.UnitTests/Compiles.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 5018c38607b4aa..53835b0310ccb2 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs +++ b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs @@ -732,10 +732,18 @@ protected override void VerifyFinalCompilation(Compilation compilation) .SelectMany(al => al.Attributes) .Single(a => a.Name.ToString().Contains("DllImport")); - bool hasCharSet = dllImportAttr.ArgumentList!.Arguments - .Any(a => a.NameEquals?.Name.Identifier.Text == nameof(DllImportAttribute.CharSet)); + AttributeArgumentSyntax? charSetArgument = dllImportAttr.ArgumentList!.Arguments + .SingleOrDefault(a => a.NameEquals?.Name.Identifier.Text == nameof(DllImportAttribute.CharSet)); - Assert.Equal(_expectCharSetUnicode, hasCharSet); + if (_expectCharSetUnicode) + { + Assert.NotNull(charSetArgument); + Assert.Equal($"{nameof(CharSet)}.{nameof(CharSet.Unicode)}", charSetArgument.Expression.ToString()); + } + else + { + Assert.Null(charSetArgument); + } } } From d52c93e1a8807d8ed5b4e6186c828e1a05be6362 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:06:49 +0000 Subject: [PATCH 07/12] Fix DownlevelLibraryImportGenerator: add GeneratedCodeAnalysisFlags.Analyze, forward CharSet=Unicode, fix test assertion Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/485e2525-7401-4b45-a60d-6f22059d1482 Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com> --- ...wnlevelLibraryImportDiagnosticsAnalyzer.cs | 2 +- .../DownlevelLibraryImportGenerator.cs | 50 ++++++++++++------- .../Compiles.cs | 2 +- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs index 64cc64aaa57796..3d6be3c92d6567 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); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(context => { diff --git a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs index aa84f8615811f9..cfc507950c1439 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs @@ -320,6 +320,38 @@ 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, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + AliasQualifiedName("global", IdentifierName(typeof(CharSet).FullName)), + IdentifierName(nameof(CharSet.Unicode))))); + } + (ParameterListSyntax parameterList, TypeSyntax returnType, AttributeListSyntax returnTypeAttributes) = stubGenerator.GenerateTargetMethodSignatureData(); LocalFunctionStatementSyntax localDllImport = LocalFunctionStatement(returnType, stubTargetName) .AddModifiers( @@ -333,23 +365,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) { 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 53835b0310ccb2..e12109558baa58 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs +++ b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs @@ -738,7 +738,7 @@ protected override void VerifyFinalCompilation(Compilation compilation) if (_expectCharSetUnicode) { Assert.NotNull(charSetArgument); - Assert.Equal($"{nameof(CharSet)}.{nameof(CharSet.Unicode)}", charSetArgument.Expression.ToString()); + Assert.EndsWith($"{nameof(CharSet)}.{nameof(CharSet.Unicode)}", charSetArgument.Expression.ToString()); } else { From 4702c5749cf771ff3a935297dce8edf8824ea931 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 16 Apr 2026 18:34:12 -0700 Subject: [PATCH 08/12] Apply suggestions from code review Co-authored-by: Jeremy Koritzinsky --- .../DownlevelLibraryImportDiagnosticsAnalyzer.cs | 2 +- .../Analyzers/LibraryImportDiagnosticsAnalyzer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs index 3d6be3c92d6567..b04f08d28aa40a 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.Analyze); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(context => { 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 18f63618519141..a4cfc8c40b0e9c 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.Analyze); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(context => { From 72e8dd03f9b2375c9d4b4d8527909897f56cb35b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 01:54:38 +0000 Subject: [PATCH 09/12] Address code review: extract CreateEnumExpressionSyntax to class-level, fix DllImport attribute predicate in test Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/3e667560-955d-4bf8-824a-2d812b16dc40 Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com> --- .../DownlevelLibraryImportGenerator.cs | 18 ++++++++++-------- .../LibraryImportGenerator.cs | 19 ++++++++----------- .../Compiles.cs | 2 +- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs index cfc507950c1439..f549251c4924c4 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs @@ -346,10 +346,7 @@ private static LocalFunctionStatementSyntax CreateTargetDllImportAsLocalStatemen dllImportArgs.Add(AttributeArgument( NameEquals(nameof(DllImportAttribute.CharSet)), null, - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - AliasQualifiedName("global", IdentifierName(typeof(CharSet).FullName)), - IdentifierName(nameof(CharSet.Unicode))))); + CreateEnumExpressionSyntax(CharSet.Unicode))); } (ParameterListSyntax parameterList, TypeSyntax returnType, AttributeListSyntax returnTypeAttributes) = stubGenerator.GenerateTargetMethodSignatureData(); @@ -395,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)); } @@ -430,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/LibraryImportGenerator.cs b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs index 2945dc220f6e78..0b54b47f4987df 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs @@ -401,10 +401,7 @@ private static LocalFunctionStatementSyntax CreateTargetDllImportAsLocalStatemen dllImportArgs.Add(AttributeArgument( NameEquals(nameof(DllImportAttribute.CharSet)), null, - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - AliasQualifiedName("global", IdentifierName(typeof(CharSet).FullName)), - IdentifierName(nameof(CharSet.Unicode))))); + CreateEnumExpressionSyntax(CharSet.Unicode))); } (ParameterListSyntax parameterList, TypeSyntax returnType, AttributeListSyntax returnTypeAttributes) = stubGenerator.GenerateTargetMethodSignatureData(); @@ -480,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 e12109558baa58..96f0128cf04b35 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs +++ b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs @@ -730,7 +730,7 @@ protected override void VerifyFinalCompilation(Compilation compilation) LocalFunctionStatementSyntax innerDllImport = Assert.Single(localFunctions); AttributeSyntax dllImportAttr = innerDllImport.AttributeLists .SelectMany(al => al.Attributes) - .Single(a => a.Name.ToString().Contains("DllImport")); + .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)); From b2baceef6ce53073c8859b6b6affcd899c4de554 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:30:50 +0000 Subject: [PATCH 10/12] Skip SYSLIB1050 in diagnostics analyzers when method has generated implementation from our generator Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/408e36a5-d92b-4944-8b37-13c9d31754fd Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com> --- ...wnlevelLibraryImportDiagnosticsAnalyzer.cs | 24 ++++++++++++++++++- .../LibraryImportDiagnosticsAnalyzer.cs | 24 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs index b04f08d28aa40a..ac96bebc1b29ff 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs @@ -110,11 +110,17 @@ private static bool AnalyzeMethod(SymbolAnalysisContext context, StubEnvironment if (libraryImportAttr is null) return false; - // Find the method syntax + // 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 avoid reporting SYSLIB1050 on it. foreach (SyntaxReference syntaxRef in method.DeclaringSyntaxReferences) { if (syntaxRef.GetSyntax(context.CancellationToken) is MethodDeclarationSyntax methodSyntax) { + if (methodSyntax.Body is not null && IsGeneratedByOurGenerator(method)) + continue; + AnalyzeMethodSyntax(context, methodSyntax, method, libraryImportAttr, env); break; } @@ -123,6 +129,22 @@ 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, 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 a4cfc8c40b0e9c..d3037176d7d0ac 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.cs @@ -108,11 +108,17 @@ private static bool AnalyzeMethod(SymbolAnalysisContext context, StubEnvironment if (libraryImportAttr is null) return false; - // Find the method syntax + // 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 avoid reporting SYSLIB1050 on it. foreach (SyntaxReference syntaxRef in method.DeclaringSyntaxReferences) { if (syntaxRef.GetSyntax(context.CancellationToken) is MethodDeclarationSyntax methodSyntax) { + if (methodSyntax.Body is not null && IsGeneratedByOurGenerator(method)) + continue; + AnalyzeMethodSyntax(context, methodSyntax, method, libraryImportAttr, env, options); break; } @@ -121,6 +127,22 @@ 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, From d4687fc5ebc5c76702447c4ca144277b9cfdd0a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:42:32 +0000 Subject: [PATCH 11/12] Only skip GetDiagnosticIfInvalidMethodForGeneration when method has generated implementation; always run CalculateDiagnostics Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/301361db-1d3f-4105-871a-90438d6a01df Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com> --- ...wnlevelLibraryImportDiagnosticsAnalyzer.cs | 26 ++++++++++++------- .../LibraryImportDiagnosticsAnalyzer.cs | 26 ++++++++++++------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs index ac96bebc1b29ff..f1fe2f11ef7dda 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs @@ -110,18 +110,20 @@ private static bool AnalyzeMethod(SymbolAnalysisContext context, StubEnvironment if (libraryImportAttr is null) return false; + 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 avoid reporting SYSLIB1050 on it. + // 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) { - if (methodSyntax.Body is not null && IsGeneratedByOurGenerator(method)) + if (methodSyntax.Body is not null && isGeneratedByOurGenerator) continue; - AnalyzeMethodSyntax(context, methodSyntax, method, libraryImportAttr, env); + AnalyzeMethodSyntax(context, methodSyntax, method, libraryImportAttr, env, isGeneratedByOurGenerator); break; } } @@ -150,14 +152,20 @@ private static void AnalyzeMethodSyntax( 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 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 d3037176d7d0ac..d2a59b4f3b0fcf 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.cs @@ -108,18 +108,20 @@ private static bool AnalyzeMethod(SymbolAnalysisContext context, StubEnvironment if (libraryImportAttr is null) return false; + 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 avoid reporting SYSLIB1050 on it. + // 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) { - if (methodSyntax.Body is not null && IsGeneratedByOurGenerator(method)) + if (methodSyntax.Body is not null && isGeneratedByOurGenerator) continue; - AnalyzeMethodSyntax(context, methodSyntax, method, libraryImportAttr, env, options); + AnalyzeMethodSyntax(context, methodSyntax, method, libraryImportAttr, env, options, isGeneratedByOurGenerator); break; } } @@ -149,14 +151,20 @@ private static void AnalyzeMethodSyntax( 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 From 351839b8db2d3d602ce3ba07d17737efc76064f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:12:25 +0000 Subject: [PATCH 12/12] Filter diagnostics to only report from non-generated source: skip partial implementation part and filter by syntax tree Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/9fbadd4b-a632-4127-9b12-2bad0f80873f Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com> --- ...DownlevelLibraryImportDiagnosticsAnalyzer.cs | 17 ++++++++++++++++- .../LibraryImportDiagnosticsAnalyzer.cs | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs index f1fe2f11ef7dda..ff85fad5fcf43b 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs @@ -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()) @@ -171,9 +177,18 @@ private static void AnalyzeMethodSyntax( // 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/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.cs b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.cs index d2a59b4f3b0fcf..b959f7145e12db 100644 --- a/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.cs +++ b/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.cs @@ -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()) @@ -172,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()); + } } }