From e65c7f81481789dec4529a88404d3fef4b0752ca Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Thu, 11 Jun 2026 19:27:14 -0700 Subject: [PATCH] Suppress CS3016 on internal COM wrappers without [CLSCompliant(false)] (#1703) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #1703 fix marked CCW-bearing COM struct wrappers [CLSCompliant(false)] to silence CS3016 (a Roslyn limitation that flags array-valued [UnmanagedCallersOnly] attribute arguments even on non-visible types, dotnet/roslyn#68526). That type-level attribute induces CS3019/CS3021, which Roslyn reports against the type *symbol* — so when a consumer adds their own friendly-helper partial to a generated COM struct (e.g. dotnet/winforms System.Private.Windows.Core's ITypeInfo.cs), the diagnostic lands on the consumer's file and escapes the generated file's pragma. Result: CS3021 (no assembly CLSCompliant attribute) or CS3019 (assembly is CLS-compliant) breaks the consumer build under TreatWarningsAsErrors. Fix: stop emitting [CLSCompliant(false)] entirely. Instead suppress CS3016 directly in the generated file's header pragma, where the offending thunk lives. CS3016 is reported at the thunk method (always in our generated file), so the file-scoped pragma silences it precisely without masking user code, and because no attribute is emitted, CS3019/CS3021 can never arise regardless of consumer partials or assembly CLS posture. The suppression is applied only to internal projections (the default). Public projections do not get it: a public COM interop surface is the consumer's CLS contract to own (and is inherently non-CLS-compliant anyway), so CsWin32 must not unilaterally silence it. Removes the now-unused CLSCompliantFalse() factory and the CS3019/CS3021 entries from the generated-file pragma. Adds regression coverage: the internal cross-TFM x assembly-CLS matrix, public-projection omits-suppression, friendly-helper-partial with and without an assembly CLSCompliant attribute, and a load-bearing test proving CS3016 still fires when the pragma is stripped. --- .../Generator.Com.cs | 14 -- .../Generator.Invariants.cs | 28 ++- src/Microsoft.Windows.CsWin32/Generator.cs | 9 +- .../SimpleSyntaxFactory.cs | 6 - .../COMTests.cs | 232 +++++++++++++++--- 5 files changed, 235 insertions(+), 54 deletions(-) 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))); }