diff --git a/src/Microsoft.Windows.CsWin32/Generator.Com.cs b/src/Microsoft.Windows.CsWin32/Generator.Com.cs
index dc267a8c..9517c73b 100644
--- a/src/Microsoft.Windows.CsWin32/Generator.Com.cs
+++ b/src/Microsoft.Windows.CsWin32/Generator.Com.cs
@@ -769,20 +769,6 @@ static ExpressionSyntax ThisPointer(PointerTypeSyntax? typedPointer = null)
iface = iface.AddAttributeLists(AttributeList(GUID(DecodeGuidFromAttribute(guidAttribute.Value))));
}
- // CS3016 ("Arrays as attribute arguments is not CLS-compliant") fires under
- // [assembly: CLSCompliant(true)] on the CCW thunks we emit with
- // [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]. CCW thunks are
- // only emitted when canUseUnmanagedCallersOnlyAttribute is true (.NET 5+); on
- // net472 / netstandard2.0 the generator produces no array-valued attribute arguments and
- // [CLSCompliant(false)] is unnecessary. The attribute is also a no-op on public types
- // (which the consumer's CLS surface owns), so we only emit it on internal struct
- // wrappers that we know carry the array-valued attribute.
- // See https://github.com/microsoft/CsWin32/issues/1703.
- if (ccwThisParameter is not null && this.Visibility == SyntaxKind.InternalKeyword)
- {
- iface = iface.AddAttributeLists(AttributeList(CLSCompliantFalse()));
- }
-
if (this.GetSupportedOSPlatformAttribute(typeDef.GetCustomAttributes()) is AttributeSyntax supportedOSPlatformAttribute)
{
iface = iface.AddAttributeLists(AttributeList(supportedOSPlatformAttribute));
diff --git a/src/Microsoft.Windows.CsWin32/Generator.Invariants.cs b/src/Microsoft.Windows.CsWin32/Generator.Invariants.cs
index e0b7392b..2adb638e 100644
--- a/src/Microsoft.Windows.CsWin32/Generator.Invariants.cs
+++ b/src/Microsoft.Windows.CsWin32/Generator.Invariants.cs
@@ -296,16 +296,40 @@ public partial class Generator
"CS0436", // conflicts with the imported type (InternalsVisibleTo between two projects that both use CsWin32)
"CS8981", // The type name only contains lower-cased ascii characters
"SYSLIB1092", // The return value in the managed definition will be converted to an 'out' parameter when calling the unmanaged COM method
- "CS3021", // Type does not need a CLSCompliant attribute because the assembly does not have a CLSCompliant attribute (fires on generated COM struct wrappers that we mark [CLSCompliant(false)] to silence CS3016 — see https://github.com/microsoft/CsWin32/issues/1703)
- "CS3019", // CLS compliance checking will not be performed on '...' because it is not visible from outside this assembly (same root cause: our internal COM struct wrappers are marked [CLSCompliant(false)] to silence CS3016)
};
+ ///
+ /// The leading trivia for every generated file: the auto-generated banner plus a #pragma warning disable for the
+ /// warnings CsWin32-generated code is expected to trip. Used for public projections.
+ ///
private static readonly SyntaxTriviaList FileHeader = ParseLeadingTrivia(AutoGeneratedHeader).Add(
Trivia(PragmaWarningDirectiveTrivia(
disableOrRestoreKeyword: TokenWithSpace(SyntaxKind.DisableKeyword),
errorCodes: [.. WarningsToSuppressInGeneratedCode.Select(IdentifierName)],
isActive: true)));
+ ///
+ /// Same as , but additionally suppresses CS3016 ("Arrays as attribute arguments is not
+ /// CLS-compliant"). Used for internal projections (the default).
+ ///
+ ///
+ /// CS3016 fires under [assembly: CLSCompliant(true)] on the CCW thunks we emit with
+ /// [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] — a Roslyn limitation that reports the
+ /// diagnostic even for non-visible members (dotnet/roslyn#68526). The thunks only exist on .NET 5+ (net472 /
+ /// netstandard2.0 never trip it). Suppressing it via this header pragma — at the thunk site, always in our generated file —
+ /// masks no user-authored CS3016. We deliberately do NOT instead stamp [CLSCompliant(false)] on the struct: that
+ /// type-level attribute induces CS3019/CS3021, which Roslyn reports against the type symbol and therefore leak onto any
+ /// consumer-authored partial of the struct (e.g. a friendly-helper partial) where the generated pragma cannot reach.
+ /// We use this header only for internal projections; for public ones the consumer owns the CLS-compliance contract of
+ /// their public surface (which inherently exposes other non-CLS COM types), so CsWin32 must not unilaterally silence it.
+ /// See https://github.com/microsoft/CsWin32/issues/1703.
+ ///
+ private static readonly SyntaxTriviaList FileHeaderWithClsArrayAttributeSuppression = ParseLeadingTrivia(AutoGeneratedHeader).Add(
+ Trivia(PragmaWarningDirectiveTrivia(
+ disableOrRestoreKeyword: TokenWithSpace(SyntaxKind.DisableKeyword),
+ errorCodes: [.. WarningsToSuppressInGeneratedCode.Append("CS3016").Select(IdentifierName)],
+ isActive: true)));
+
private static readonly AttributeSyntax InAttributeSyntax = Attribute(IdentifierName("In")).WithArgumentList(null);
private static readonly AttributeSyntax OutAttributeSyntax = Attribute(IdentifierName("Out")).WithArgumentList(null);
private static readonly AttributeSyntax OptionalAttributeSyntax = Attribute(IdentifierName("Optional")).WithArgumentList(null);
diff --git a/src/Microsoft.Windows.CsWin32/Generator.cs b/src/Microsoft.Windows.CsWin32/Generator.cs
index d4c84e02..a0b3919c 100644
--- a/src/Microsoft.Windows.CsWin32/Generator.cs
+++ b/src/Microsoft.Windows.CsWin32/Generator.cs
@@ -42,6 +42,7 @@ public partial class Generator : IGenerator, IDisposable
private readonly GeneratorOptions options;
private readonly CSharpCompilation? compilation;
private readonly CSharpParseOptions? parseOptions;
+ private readonly SyntaxTriviaList fileHeader;
private readonly bool comIIDInterfacePredefined;
private readonly bool getDelegateForFunctionPointerGenericExists;
private readonly GeneratedCode committedCode = new();
@@ -107,6 +108,10 @@ public Generator(string metadataLibraryPath, Docs? docs, IEnumerable add
this.parseOptions = parseOptions;
this.volatileCode = new(this.committedCode);
+ // Suppress CS3016 (the CLS array-attribute-argument warning our CCW thunks trip) in the generated file header,
+ // but only for internal projections. For public projections the consumer owns their public surface's CLS contract.
+ this.fileHeader = this.options.Public ? FileHeader : FileHeaderWithClsArrayAttributeSuppression;
+
// UnscopedRefAttribute may be emitted to work on downlevel *runtimes*, but we can't use it
// on downlevel *compilers*. Only .NET 8+ SDK compilers support it. Since we cannot detect
// compiler version, we use language version instead.
@@ -854,7 +859,7 @@ .. this.committedCode.GeneratedTopLevelTypes
CompilationUnitSyntax? compilationUnit = ((CompilationUnitSyntax)kv.Value
.AddUsings(usingDirectives.ToArray())
.Accept(new WhitespaceRewriter())!)
- .WithLeadingTrivia(FileHeader);
+ .WithLeadingTrivia(this.fileHeader);
lock (normalizedResults)
{
@@ -871,7 +876,7 @@ .. this.committedCode.GeneratedTopLevelTypes
}
else
{
- normalizedResults.Add(string.Format(CultureInfo.InvariantCulture, FilenamePattern, "CsWin32Stamp"), CompilationUnit().AddAttributeLists(CsWin32StampAttribute).WithLeadingTrivia(FileHeader));
+ normalizedResults.Add(string.Format(CultureInfo.InvariantCulture, FilenamePattern, "CsWin32Stamp"), CompilationUnit().AddAttributeLists(CsWin32StampAttribute).WithLeadingTrivia(this.fileHeader));
}
}
diff --git a/src/Microsoft.Windows.CsWin32/SimpleSyntaxFactory.cs b/src/Microsoft.Windows.CsWin32/SimpleSyntaxFactory.cs
index 2ebc4b28..4958b907 100644
--- a/src/Microsoft.Windows.CsWin32/SimpleSyntaxFactory.cs
+++ b/src/Microsoft.Windows.CsWin32/SimpleSyntaxFactory.cs
@@ -197,12 +197,6 @@ internal static AttributeSyntax GUID(Guid guid)
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(guid.ToString().ToUpperInvariant()))));
}
- internal static AttributeSyntax CLSCompliantFalse()
- {
- return Attribute(IdentifierName("CLSCompliant")).AddArgumentListArguments(
- AttributeArgument(LiteralExpression(SyntaxKind.FalseLiteralExpression)));
- }
-
internal static AttributeSyntax InterfaceType(ComInterfaceType interfaceType)
{
return Attribute(IdentifierName("InterfaceType")).AddArgumentListArguments(
diff --git a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs
index d1de9eef..bb991461 100644
--- a/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs
+++ b/test/Microsoft.Windows.CsWin32.Tests/COMTests.cs
@@ -676,14 +676,15 @@ public void COMInterfaceStructReturn(bool allowMarshaling, string tfm)
///
/// Regression test for issue 1703:
/// generated COM struct wrappers contain CCW thunks annotated with
- /// [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })], which trips
- /// CS3016 "Arrays as attribute arguments is not CLS-compliant" under
- /// [assembly: CLSCompliant(true)]. The generator must mark such COM struct wrappers
- /// [CLSCompliant(false)] so consumers do not have to hand-author partials per type.
+ /// [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })], whose array-valued argument
+ /// trips CS3016 "Arrays as attribute arguments is not CLS-compliant" under
+ /// [assembly: CLSCompliant(true)] (a Roslyn limitation, dotnet/roslyn#68526). The generator suppresses
+ /// CS3016 in the generated file's header pragma — at the thunk site where it is reported — rather than by
+ /// stamping [CLSCompliant(false)] on the struct, so consumers do not have to hand-author partials per type.
///
[Theory]
[CombinatorialData]
- public void COMStructWrappers_AreCLSCompliantFalse_Issue1703(
+ public void COMStructWrappers_AreClsClean_Issue1703(
[CombinatorialValues("net8.0", "net9.0", "net10.0")] string tfm)
{
this.compilation = this.starterCompilations[tfm];
@@ -697,21 +698,19 @@ public void COMStructWrappers_AreCLSCompliantFalse_Issue1703(
this.CollectGeneratedCode(this.generator);
this.AssertNoDiagnostics();
- // Every generated COM struct wrapper that carries CCW thunks (annotated with
- // [UnmanagedCallersOnly(CallConvs = new[]{...})]) must itself bear [CLSCompliant(false)].
+ // The generator suppresses CS3016 at the thunk site; it does NOT stamp [CLSCompliant(false)] on the struct,
+ // because that type-level attribute would induce CS3019/CS3021 that leak onto consumer partials (see
+ // COMStructWrappers_FriendlyHelperPartial_*). No generated COM struct should carry a CLSCompliant attribute.
foreach (string structName in new[] { "ITypeInfo", "ITypeLib", "IRecordInfo" })
{
var type = Assert.IsType(this.FindGeneratedType(structName).Single());
- Assert.Contains(
+ Assert.DoesNotContain(
type.AttributeLists.SelectMany(al => al.Attributes),
- a => a.Name.ToString() is "CLSCompliant" or "System.CLSCompliant"
- && a.ArgumentList?.Arguments.Count == 1
- && a.ArgumentList.Arguments[0].Expression is LiteralExpressionSyntax { Token.ValueText: "false" });
+ a => a.Name.ToString() is "CLSCompliant" or "System.CLSCompliant");
}
// End-to-end: a consuming assembly marked [assembly: CLSCompliant(true)] must compile
- // entirely clean — no CS3016, no CS3019, no CS3021. The generator suppresses CS3019/CS3021
- // in the generated-file pragma so consumers do not have to author per-file overrides.
+ // entirely clean — no CS3016, no CS3019, no CS3021.
this.compilation = this.AddCode("""
using System;
@@ -732,47 +731,220 @@ public static void Touch()
}
///
- /// Negative coverage for #1703: on downlevel TFMs (net472, netstandard2.0)
- /// the generator does not emit [UnmanagedCallersOnly]-decorated CCW thunks, so there is
- /// no array-valued attribute argument and no CS3016 to suppress. The generator must therefore
- /// not emit [CLSCompliant(false)] in that case — the attribute would be unmotivated noise.
+ /// Cross-compilation matrix for issue 1703 over the
+ /// default (internal) projection: every combination of target framework (net472 / net10.0) and whether the
+ /// consuming assembly declares [assembly: CLSCompliant(true)] must compile free of the issue-1703 CLS diagnostics,
+ /// and no generated COM struct may carry a [CLSCompliant] attribute.
///
+ ///
+ /// The relevant diagnostics are genuinely enabled in every cell (see
+ /// ):
+ ///
+ /// - CS3016 (array-valued attribute arguments) fires on the CCW thunks when the consuming assembly is CLS-compliant; the generator suppresses it in the generated file's header pragma, at the thunk site where it is reported.
+ /// - CS3019/CS3021 never arise because the generator emits no [CLSCompliant(false)] attribute.
+ ///
+ /// The CCW thunks (and thus the array-valued attribute) exist only on modern .NET; downlevel TFMs emit neither and are
+ /// trivially clean. The public projection is intentionally excluded here and covered separately by
+ /// .
+ ///
[Theory]
[CombinatorialData]
- public void COMStructWrappers_NoCLSCompliantFalse_OnDownlevelTFMs_Issue1703(
- [CombinatorialValues("net472", "netstandard2.0")] string tfm)
+ public void COMStructWrappers_ClsComplianceMatrix_Issue1703(
+ [CombinatorialValues("net472", "net10.0")] string tfm,
+ bool assemblyClsCompliant)
{
this.compilation = this.starterCompilations[tfm];
+ if (GetLanguageVersionForTfm(tfm) is LanguageVersion languageVersion)
+ {
+ this.parseOptions = this.parseOptions.WithLanguageVersion(languageVersion);
+ }
+
this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false });
Assert.True(this.generator.TryGenerate("ITypeInfo", CancellationToken.None));
+ Assert.True(this.generator.TryGenerate("ITypeLib", CancellationToken.None));
+ Assert.True(this.generator.TryGenerate("IRecordInfo", CancellationToken.None));
this.CollectGeneratedCode(this.generator);
- var type = Assert.IsType(this.FindGeneratedType("ITypeInfo").Single());
+ // No generated COM struct (nor its nested CCW Interface subtype) carries a CLSCompliant attribute in any cell.
+ foreach (string structName in new[] { "ITypeInfo", "ITypeLib", "IRecordInfo" })
+ {
+ var type = Assert.IsType(this.FindGeneratedType(structName).Single());
+ Assert.DoesNotContain(
+ type.AttributeLists.SelectMany(al => al.Attributes),
+ a => a.Name.ToString() is "CLSCompliant" or "System.CLSCompliant");
+
+ var nestedInterface = type.Members.OfType().SingleOrDefault(i => i.Identifier.ValueText == "Interface");
+ Assert.NotNull(nestedInterface);
+ Assert.DoesNotContain(
+ nestedInterface!.AttributeLists.SelectMany(al => al.Attributes),
+ a => a.Name.ToString() is "CLSCompliant" or "System.CLSCompliant");
+ }
+
+ // End-to-end: the internal-projection consumer build is free of the issue-1703 diagnostics regardless of its
+ // assembly-level CLS posture.
+ // - With [assembly: CLSCompliant(true)]: CS3016 (array attr args) is at risk; suppressed at the thunk site.
+ // - Without it: nothing CLS-related fires (no attribute, and CS3016 only fires under assembly CLS-compliance).
+ string assemblyAttribute = assemblyClsCompliant ? "[assembly: System.CLSCompliant(true)]" : string.Empty;
+ this.compilation = this.AddCode($$"""
+ {{assemblyAttribute}}
+
+ internal static class ClsMatrixConsumer
+ {
+ public static void Touch()
+ {
+ _ = typeof(Windows.Win32.System.Com.ITypeInfo);
+ _ = typeof(Windows.Win32.System.Com.ITypeLib);
+ _ = typeof(Windows.Win32.System.Ole.IRecordInfo);
+ }
+ }
+ """);
Assert.DoesNotContain(
- type.AttributeLists.SelectMany(al => al.Attributes),
- a => a.Name.ToString() is "CLSCompliant" or "System.CLSCompliant");
+ this.compilation.GetDiagnostics(TestContext.Current.CancellationToken),
+ d => d.Id is "CS3016" or "CS3019" or "CS3021");
}
///
- /// Negative coverage for #1703: when the generator is configured to emit public types
- /// ( = ), the COM struct wrapper
- /// is part of the consumer's CLS surface and they own its CLS-compliance contract — the
- /// generator must not unilaterally stamp [CLSCompliant(false)] on it.
+ /// Coverage for issue 1703 when the generator emits
+ /// public types ( = ): the CS3016 suppression is
+ /// not baked into the generated file. A public projection is part of the consumer's CLS surface, which they own;
+ /// CsWin32 must not unilaterally silence a CLS diagnostic about it (and public COM interop is inherently non-CLS-compliant
+ /// anyway). The internal projection — the default and the #1703 scenario — does carry the suppression, asserted here as a
+ /// contrast so a regression in either direction is caught.
///
[Fact]
- public void COMStructWrappers_NoCLSCompliantFalse_WhenPublic_Issue1703()
+ public void COMStructWrappers_PublicProjection_OmitsClsSuppression_Issue1703()
{
this.compilation = this.starterCompilations["net10.0"];
this.parseOptions = this.parseOptions.WithLanguageVersion(GetLanguageVersionForTfm("net10.0") ?? LanguageVersion.Latest);
- this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false, Public = true });
- Assert.True(this.generator.TryGenerate("ITypeInfo", CancellationToken.None));
- this.CollectGeneratedCode(this.generator);
+ // Internal (default) projection: the generated file header suppresses CS3016.
+ var internalGenerator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false });
+ Assert.True(internalGenerator.TryGenerate("ITypeInfo", CancellationToken.None));
+ Assert.True(GeneratedFilesDisableWarning(internalGenerator, "CS3016"));
+ // Public projection: the generated file header does NOT suppress CS3016 — the consumer owns their public CLS surface.
+ var publicGenerator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false, Public = true });
+ Assert.True(publicGenerator.TryGenerate("ITypeInfo", CancellationToken.None));
+ Assert.False(GeneratedFilesDisableWarning(publicGenerator, "CS3016"));
+
+ // Neither projection emits a [CLSCompliant] attribute on the struct.
+ this.CollectGeneratedCode(publicGenerator);
var type = Assert.IsType(this.FindGeneratedType("ITypeInfo").Single());
Assert.DoesNotContain(
type.AttributeLists.SelectMany(al => al.Attributes),
a => a.Name.ToString() is "CLSCompliant" or "System.CLSCompliant");
+
+ bool GeneratedFilesDisableWarning(IGenerator generator, string warningId) =>
+ generator.GetCompilationUnits(TestContext.Current.CancellationToken)
+ .Any(unit => unit.Value.GetText(Encoding.UTF8).ToString() is string text
+ && text.Contains("#pragma warning disable", StringComparison.Ordinal)
+ && System.Text.RegularExpressions.Regex.IsMatch(text, $@"#pragma warning disable[^\r\n]*\b{warningId}\b"));
}
+
+ ///
+ /// Regression for the dotnet/winforms#14639 scenario behind issue 1703:
+ /// a consumer adds a friendly-helper partial to a generated CCW-bearing COM struct (extremely common) and the
+ /// consuming assembly does not declare [assembly: CLSCompliant]. The earlier
+ /// [CLSCompliant(false)] approach broke this: that type-level attribute induces CS3021, which Roslyn
+ /// reports against the type symbol at the consumer's partial — a file the generated header pragma cannot
+ /// reach. Suppressing CS3016 at the thunk site instead (and emitting no attribute) keeps this clean.
+ ///
+ [Theory]
+ [CombinatorialData]
+ public void COMStructWrappers_FriendlyHelperPartial_NoAssemblyClsAttribute_Issue1703(
+ [CombinatorialValues("net472", "net10.0")] string tfm)
+ {
+ this.compilation = this.starterCompilations[tfm];
+ if (GetLanguageVersionForTfm(tfm) is LanguageVersion languageVersion)
+ {
+ this.parseOptions = this.parseOptions.WithLanguageVersion(languageVersion);
+ }
+
+ this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false });
+ Assert.True(this.generator.TryGenerate("ITypeInfo", CancellationToken.None));
+ this.CollectGeneratedCode(this.generator);
+
+ // A consumer-authored partial of the generated struct, with no [assembly: CLSCompliant] anywhere.
+ this.compilation = this.AddCode("""
+ namespace Windows.Win32.System.Com
+ {
+ internal unsafe partial struct ITypeInfo
+ {
+ internal int FriendlyHelper() => 42;
+ }
+ }
+ """);
+ this.AssertNoDiagnostics(this.compilation, logAllGeneratedCode: false);
+ }
+
+ ///
+ /// Companion to for the
+ /// CLS-compliant consumer: a friendly-helper partial on a generated CCW-bearing COM struct under
+ /// [assembly: CLSCompliant(true)] must also stay clean. The earlier attribute approach induced CS3019
+ /// on the consumer's partial here; suppressing CS3016 at the thunk site (no attribute) avoids it.
+ ///
+ [Fact]
+ public void COMStructWrappers_FriendlyHelperPartial_WithAssemblyClsAttribute_Issue1703()
+ {
+ this.compilation = this.starterCompilations["net10.0"];
+ this.parseOptions = this.parseOptions.WithLanguageVersion(GetLanguageVersionForTfm("net10.0") ?? LanguageVersion.Latest);
+ this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false });
+ Assert.True(this.generator.TryGenerate("ITypeInfo", CancellationToken.None));
+ this.CollectGeneratedCode(this.generator);
+
+ this.compilation = this.AddCode("""
+ [assembly: System.CLSCompliant(true)]
+
+ namespace Windows.Win32.System.Com
+ {
+ internal unsafe partial struct ITypeInfo
+ {
+ internal int FriendlyHelper() => 42;
+ }
+ }
+ """);
+ this.AssertNoDiagnostics(this.compilation, logAllGeneratedCode: false);
+ }
+
+ ///
+ /// Proves the CS3016 suppression for issue 1703
+ /// is load-bearing: CS3016 is genuinely enabled by the compiler and would fire on the CCW thunks if the
+ /// generated file's header pragma were absent. This guards against the matrix tests passing vacuously because CLS
+ /// checking was somehow disabled in the harness.
+ ///
+ [Fact]
+ public void COMStructWrappers_ClsPragmaSuppressionsAreLoadBearing_Issue1703()
+ {
+ this.compilation = this.starterCompilations["net10.0"];
+ this.parseOptions = this.parseOptions.WithLanguageVersion(GetLanguageVersionForTfm("net10.0") ?? LanguageVersion.Latest);
+ this.generator = this.CreateGenerator(DefaultTestGeneratorOptions with { AllowMarshaling = false });
+ Assert.True(this.generator.TryGenerate("ITypeInfo", CancellationToken.None));
+
+ // Re-parse the generated code with the file-level "#pragma warning disable" directives removed.
+ SyntaxTree[] unsuppressedTrees = this.generator.GetCompilationUnits(CancellationToken.None)
+ .Select(unit => CSharpSyntaxTree.ParseText(
+ StripWarningDisablePragmas(unit.Value.GetText(Encoding.UTF8).ToString()),
+ this.parseOptions,
+ path: unit.Key))
+ .ToArray();
+
+ // With the pragma removed and the assembly marked CLS-compliant, the CCW thunks' array-valued
+ // [UnmanagedCallersOnly(CallConvs = new[]{...})] attribute surfaces CS3016.
+ CSharpCompilation unsuppressed = this.compilation
+ .AddSyntaxTrees(unsuppressedTrees)
+ .AddSyntaxTrees(CSharpSyntaxTree.ParseText("[assembly: System.CLSCompliant(true)]", this.parseOptions, path: "AssemblyInfo.cs", cancellationToken: TestContext.Current.CancellationToken));
+ Assert.Contains(unsuppressed.GetDiagnostics(TestContext.Current.CancellationToken), d => d.Id == "CS3016");
+
+ // And because the generator emits no [CLSCompliant] attribute, the unsuppressed code shows neither CS3019 nor CS3021.
+ Assert.DoesNotContain(unsuppressed.GetDiagnostics(TestContext.Current.CancellationToken), d => d.Id is "CS3019" or "CS3021");
+
+ // Sanity: as actually generated (pragma intact), a CLS-compliant consumer sees none of CS3016/3019/3021.
+ this.CollectGeneratedCode(this.generator);
+ this.compilation = this.AddCode("[assembly: System.CLSCompliant(true)]");
+ Assert.DoesNotContain(this.compilation.GetDiagnostics(TestContext.Current.CancellationToken), d => d.Id is "CS3016" or "CS3019" or "CS3021");
+ }
+
+ private static string StripWarningDisablePragmas(string code) =>
+ string.Join("\n", code.Split('\n').Where(line => !line.TrimStart().StartsWith("#pragma warning disable", StringComparison.Ordinal)));
}