From 83d9eda1a276f17a75f759f6f2230f0633fc867a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:46:03 +0100 Subject: [PATCH 1/2] feat: add TUnit0080 analyzer for missing polyfill types on older TFMs On .NET Framework and netstandard targets, types like ModuleInitializerAttribute are not available. This analyzer detects missing required types at compilation time and directs users to install the Polyfill NuGet package. --- TUnit.Analyzers/AnalyzerReleases.Unshipped.md | 1 + TUnit.Analyzers/MissingPolyfillAnalyzer.cs | 34 +++++++++++++++++++ TUnit.Analyzers/Resources.resx | 9 +++++ TUnit.Analyzers/Rules.cs | 3 ++ 4 files changed, 47 insertions(+) create mode 100644 TUnit.Analyzers/MissingPolyfillAnalyzer.cs diff --git a/TUnit.Analyzers/AnalyzerReleases.Unshipped.md b/TUnit.Analyzers/AnalyzerReleases.Unshipped.md index f64b591235..747e029c77 100644 --- a/TUnit.Analyzers/AnalyzerReleases.Unshipped.md +++ b/TUnit.Analyzers/AnalyzerReleases.Unshipped.md @@ -4,6 +4,7 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- TUnit0061 | Usage | Error | ClassDataSource type requires parameterless constructor TUnit0062 | Usage | Warning | CancellationToken must be the last parameter +TUnit0080 | Usage | Error | Missing polyfill types required by TUnit ### Removed Rules diff --git a/TUnit.Analyzers/MissingPolyfillAnalyzer.cs b/TUnit.Analyzers/MissingPolyfillAnalyzer.cs new file mode 100644 index 0000000000..dd9bac79c5 --- /dev/null +++ b/TUnit.Analyzers/MissingPolyfillAnalyzer.cs @@ -0,0 +1,34 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace TUnit.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class MissingPolyfillAnalyzer : ConcurrentDiagnosticAnalyzer +{ + private static readonly string[] RequiredTypes = + [ + "System.Runtime.CompilerServices.ModuleInitializerAttribute", + ]; + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(Rules.MissingPolyfillPackage); + + protected override void InitializeInternal(AnalysisContext context) + { + context.RegisterCompilationAction(AnalyzeCompilation); + } + + private static void AnalyzeCompilation(CompilationAnalysisContext context) + { + foreach (var typeName in RequiredTypes) + { + if (context.Compilation.GetTypeByMetadataName(typeName) is null) + { + context.ReportDiagnostic( + Diagnostic.Create(Rules.MissingPolyfillPackage, Location.None, typeName)); + } + } + } +} diff --git a/TUnit.Analyzers/Resources.resx b/TUnit.Analyzers/Resources.resx index 7ae4425c17..ed3ca7df8d 100644 --- a/TUnit.Analyzers/Resources.resx +++ b/TUnit.Analyzers/Resources.resx @@ -525,6 +525,15 @@ Multiple constructors found without [TestConstructor] attribute + + TUnit requires certain types (such as ModuleInitializerAttribute) that are not available on older target frameworks. Install the 'Polyfill' NuGet package to provide these types. + + + Type '{0}' is required by TUnit but is not available. Install the 'Polyfill' NuGet package: dotnet add package Polyfill + + + Missing polyfill types required by TUnit + Tuple types require reflection for property/field access which is not AOT-compatible. Consider using concrete types or value deconstruction. diff --git a/TUnit.Analyzers/Rules.cs b/TUnit.Analyzers/Rules.cs index 489ede5331..a15bf4d612 100644 --- a/TUnit.Analyzers/Rules.cs +++ b/TUnit.Analyzers/Rules.cs @@ -168,6 +168,9 @@ public static class Rules public static readonly DiagnosticDescriptor NoAccessibleConstructor = CreateDescriptor("TUnit0061", UsageCategory, DiagnosticSeverity.Error); + public static readonly DiagnosticDescriptor MissingPolyfillPackage = + CreateDescriptor("TUnit0080", UsageCategory, DiagnosticSeverity.Error); + public static readonly DiagnosticDescriptor GenericTypeNotAotCompatible = CreateDescriptor("TUnit0300", UsageCategory, DiagnosticSeverity.Warning); From 4b45d088cc7cce9cec3b685b1987eee605f3aa97 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:58:29 +0100 Subject: [PATCH 2/2] fix: address PR review for polyfill analyzer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Renumber TUnit0080 → TUnit0073 (sequential after TUnit0072) - Add WellKnownDiagnosticTags.CompilationEnd custom tag - Add helpLinkUri pointing to Polyfill NuGet package - Change RequiredTypes from string[] to ImmutableArray - Add MissingPolyfillAnalyzerTests with modern TFM and net48 cases --- .../MissingPolyfillAnalyzerTests.cs | 51 +++++++++++++++++++ TUnit.Analyzers/AnalyzerReleases.Unshipped.md | 2 +- TUnit.Analyzers/MissingPolyfillAnalyzer.cs | 6 +-- TUnit.Analyzers/Resources.resx | 6 +-- TUnit.Analyzers/Rules.cs | 11 ++-- 5 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 TUnit.Analyzers.Tests/MissingPolyfillAnalyzerTests.cs diff --git a/TUnit.Analyzers.Tests/MissingPolyfillAnalyzerTests.cs b/TUnit.Analyzers.Tests/MissingPolyfillAnalyzerTests.cs new file mode 100644 index 0000000000..3ec986a8e5 --- /dev/null +++ b/TUnit.Analyzers.Tests/MissingPolyfillAnalyzerTests.cs @@ -0,0 +1,51 @@ +using Microsoft.CodeAnalysis.Testing; +using Verifier = TUnit.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier; + +namespace TUnit.Analyzers.Tests; + +public class MissingPolyfillAnalyzerTests +{ + [Test] + public async Task No_Error_On_Modern_Tfm() + { + await Verifier + .VerifyAnalyzerAsync( + """ + public class MyClass + { + } + """, + test => + { + test.TestState.AdditionalReferences.Clear(); + } + ); + } + + [Test] + public async Task Error_When_ModuleInitializerAttribute_Missing() + { + var net48References = new ReferenceAssemblies( + "net48", + new PackageIdentity("Microsoft.NETFramework.ReferenceAssemblies.net48", "1.0.3"), + System.IO.Path.Combine("ref", "net48")); + + await Verifier + .VerifyAnalyzerAsync( + """ + public class MyClass + { + } + """, + test => + { + test.ReferenceAssemblies = net48References; + test.TestState.AdditionalReferences.Clear(); + test.CompilerDiagnostics = CompilerDiagnostics.None; + }, + Verifier + .Diagnostic(Rules.MissingPolyfillPackage) + .WithArguments("System.Runtime.CompilerServices.ModuleInitializerAttribute") + ); + } +} diff --git a/TUnit.Analyzers/AnalyzerReleases.Unshipped.md b/TUnit.Analyzers/AnalyzerReleases.Unshipped.md index 747e029c77..7c65f71a62 100644 --- a/TUnit.Analyzers/AnalyzerReleases.Unshipped.md +++ b/TUnit.Analyzers/AnalyzerReleases.Unshipped.md @@ -4,7 +4,7 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- TUnit0061 | Usage | Error | ClassDataSource type requires parameterless constructor TUnit0062 | Usage | Warning | CancellationToken must be the last parameter -TUnit0080 | Usage | Error | Missing polyfill types required by TUnit +TUnit0073 | Usage | Error | Missing polyfill types required by TUnit ### Removed Rules diff --git a/TUnit.Analyzers/MissingPolyfillAnalyzer.cs b/TUnit.Analyzers/MissingPolyfillAnalyzer.cs index dd9bac79c5..3d84eb10c4 100644 --- a/TUnit.Analyzers/MissingPolyfillAnalyzer.cs +++ b/TUnit.Analyzers/MissingPolyfillAnalyzer.cs @@ -7,10 +7,8 @@ namespace TUnit.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class MissingPolyfillAnalyzer : ConcurrentDiagnosticAnalyzer { - private static readonly string[] RequiredTypes = - [ - "System.Runtime.CompilerServices.ModuleInitializerAttribute", - ]; + private static readonly ImmutableArray RequiredTypes = + ImmutableArray.Create("System.Runtime.CompilerServices.ModuleInitializerAttribute"); public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rules.MissingPolyfillPackage); diff --git a/TUnit.Analyzers/Resources.resx b/TUnit.Analyzers/Resources.resx index ed3ca7df8d..3462f5c018 100644 --- a/TUnit.Analyzers/Resources.resx +++ b/TUnit.Analyzers/Resources.resx @@ -525,13 +525,13 @@ Multiple constructors found without [TestConstructor] attribute - + TUnit requires certain types (such as ModuleInitializerAttribute) that are not available on older target frameworks. Install the 'Polyfill' NuGet package to provide these types. - + Type '{0}' is required by TUnit but is not available. Install the 'Polyfill' NuGet package: dotnet add package Polyfill - + Missing polyfill types required by TUnit diff --git a/TUnit.Analyzers/Rules.cs b/TUnit.Analyzers/Rules.cs index a15bf4d612..684b61e18d 100644 --- a/TUnit.Analyzers/Rules.cs +++ b/TUnit.Analyzers/Rules.cs @@ -169,7 +169,9 @@ public static class Rules CreateDescriptor("TUnit0061", UsageCategory, DiagnosticSeverity.Error); public static readonly DiagnosticDescriptor MissingPolyfillPackage = - CreateDescriptor("TUnit0080", UsageCategory, DiagnosticSeverity.Error); + CreateDescriptor("TUnit0073", UsageCategory, DiagnosticSeverity.Error, + customTags: [WellKnownDiagnosticTags.CompilationEnd], + helpLinkUri: "https://www.nuget.org/packages/Polyfill"); public static readonly DiagnosticDescriptor GenericTypeNotAotCompatible = CreateDescriptor("TUnit0300", UsageCategory, DiagnosticSeverity.Warning); @@ -178,7 +180,8 @@ public static class Rules CreateDescriptor("TUnit0301", UsageCategory, DiagnosticSeverity.Warning); - private static DiagnosticDescriptor CreateDescriptor(string diagnosticId, string category, DiagnosticSeverity severity) + private static DiagnosticDescriptor CreateDescriptor(string diagnosticId, string category, DiagnosticSeverity severity, + string[]? customTags = null, string? helpLinkUri = null) { return new DiagnosticDescriptor( id: diagnosticId, @@ -190,7 +193,9 @@ private static DiagnosticDescriptor CreateDescriptor(string diagnosticId, string defaultSeverity: severity, isEnabledByDefault: true, description: new LocalizableResourceString(diagnosticId + "Description", Resources.ResourceManager, - typeof(Resources)) + typeof(Resources)), + helpLinkUri: helpLinkUri, + customTags: customTags ?? [] ); } }