Skip to content

feat: add TUnit0080 analyzer for missing polyfill types#5292

Merged
thomhurst merged 2 commits intomainfrom
feat/polyfill-analyzer
Mar 29, 2026
Merged

feat: add TUnit0080 analyzer for missing polyfill types#5292
thomhurst merged 2 commits intomainfrom
feat/polyfill-analyzer

Conversation

@thomhurst
Copy link
Copy Markdown
Owner

Summary

  • Adds a new MissingPolyfillAnalyzer (TUnit0080) that checks if required types like ModuleInitializerAttribute are present in the compilation
  • On older target frameworks (.NET Framework, netstandard), these types are missing and users need the Polyfill NuGet package
  • The diagnostic only fires when the type is genuinely absent, so no false positives on modern TFMs

Test plan

  • Verify analyzer builds across all Roslyn variants (4.4, 4.7, 4.14)
  • Confirm TUnit0080 fires when targeting .NET Framework without Polyfill package
  • Confirm TUnit0080 does not fire on net8.0/net9.0/net10.0

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.
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: TUnit0080 – Missing Polyfill Analyzer

The intent here is solid — surfacing a clear, actionable diagnostic when ModuleInitializerAttribute (or similar types) is missing is much friendlier than leaving users to decipher cryptic source-generator errors. A few concerns worth addressing before merge:


1. Rule numbering gap: TUnit0080 skips TUnit0073–TUnit0079

Rules.cs currently has TUnit0070–TUnit0072, then TUnit0080. The existing unshipped rules end at TUnit0062. The jump to 0080 is unexplained. If there are reserved IDs for planned rules that's fine, but it should be documented, or the rule should be numbered sequentially (TUnit0073 or next available). Leaving an unexplained gap makes it hard for future contributors to know which IDs are safe to use.


2. Missing WellKnownDiagnosticTags.CompilationEnd custom tag

The very similar LanguageVersionAnalyzer (which also uses RegisterCompilationAction) decorates its descriptor with customTags: [WellKnownDiagnosticTags.CompilationEnd]:

public static readonly DiagnosticDescriptor CSharp12Required = new(
    ...
    customTags: [WellKnownDiagnosticTags.CompilationEnd]);

RegisterCompilationAction callbacks run at the end of compilation, so diagnostics they produce should carry the CompilationEnd tag. This is important because:

  • Some IDE/tooling experiences use this tag to delay display until the compilation is finished.
  • Roslyn's analyzer rules (RS2003) explicitly warn about missing this tag on compilation-end actions.

The CreateDescriptor helper in Rules.cs does not accept customTags, so either the helper needs overloading, or the descriptor should be created inline (as LanguageVersionAnalyzer already does for its own rule), or a customTags parameter should be added to CreateDescriptor.


3. No tests

Every other analyzer in this codebase has a corresponding *Tests.cs in TUnit.Analyzers.Tests. This PR adds none. At minimum, two tests are needed:

  • Should fire: Compilation targeting a TFM that lacks ModuleInitializerAttribute (e.g. net48 or netstandard2.0 without Polyfill) should produce TUnit0080.
  • Should not fire: Compilation targeting net8.0+ should produce no diagnostic.

AnalyzerTestHelpers.cs already contains a GetReferenceAssembliesForCurrentVersion helper that knows how to construct net48 reference assemblies, which would make this straightforward.

Without tests there is no automated protection against regressions or false positives.


4. RequiredTypes is a string[] — consider making it ImmutableArray<string> or ReadOnlySpan<string>

Minor, but consistent with the performance-first principle in this codebase. string[] is mutable and allocated on the heap. Since this is accessed on every compilation, an ImmutableArray<string> or static readonly ReadOnlySpan (C# 13+) would be marginally cleaner and signals intent better.


5. Location.None is correct, but the diagnostic message could be more actionable

Using Location.None is expected for compilation-wide diagnostics with no single source location. The message format ("Type '{0}' is required by TUnit but is not available. Install the 'Polyfill' NuGet package: dotnet add package Polyfill") is good. Consider adding helpLinkUri to the DiagnosticDescriptor pointing to the Polyfill package page (e.g. https://www.nuget.org/packages/Polyfill) so IDEs can surface a direct link. The DiagnosticDescriptor constructor has a helpLinkUri parameter, and the CreateDescriptor helper would need to be extended to pass it through.


Summary

Issue Severity
No tests Blocking
Missing CompilationEnd tag Should fix
Rule ID gap (0073–0079 skipped) Should explain or renumber
string[] mutability Minor / nitpick
No helpLinkUri Nice-to-have

The implementation itself is clean and the approach (checking GetTypeByMetadataName at compilation time) is exactly right — this is the idiomatic Roslyn way to detect missing types. The main gaps are the missing test coverage and the CompilationEnd tag.

- Renumber TUnit0080 → TUnit0073 (sequential after TUnit0072)
- Add WellKnownDiagnosticTags.CompilationEnd custom tag
- Add helpLinkUri pointing to Polyfill NuGet package
- Change RequiredTypes from string[] to ImmutableArray<string>
- Add MissingPolyfillAnalyzerTests with modern TFM and net48 cases
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: TUnit0073 – Missing Polyfill Analyzer (follow-up)

All five issues from the previous review have been addressed:

  • Rule renumbered from TUnit0080 → TUnit0073 ✅
  • WellKnownDiagnosticTags.CompilationEnd added via new customTags parameter ✅
  • Tests added (both happy-path and error-path) ✅
  • RequiredTypes is now ImmutableArray<string>
  • helpLinkUri added ✅

The core implementation is clean and idiomatic. A few remaining observations:


1. PR description still references TUnit0080

The PR summary and title both say "TUnit0080" but the code now uses "TUnit0073". Worth updating the PR description to avoid confusion in the git history.


2. No_Error_On_Modern_Tfm implicitly relies on default reference assemblies

test.TestState.AdditionalReferences.Clear();
// ReferenceAssemblies is never set — relies on the test framework default

The test works because the analyzer test framework defaults to the current SDK's reference assemblies (net8+), which contain ModuleInitializerAttribute. But this is implicit. If the test framework ever changes its default, the test would silently start verifying nothing meaningful. A more robust form would explicitly set the TFM:

test.ReferenceAssemblies = ReferenceAssemblies.Net.Net80;
test.TestState.AdditionalReferences.Clear();

This is not blocking — the current test does validate the right behaviour — but the explicit version is clearer about what "modern TFM" means.


3. CreateDescriptor helper now passes [] to all existing descriptors

The change to CreateDescriptor adds customTags: customTags ?? [] where it previously passed nothing. Callers that don't supply customTags now pass an empty array to DiagnosticDescriptor. Roslyn treats null and [] identically here, so there is no behavioural change — just worth being aware that all existing rules are now slightly different objects. If Roslyn ever changes that contract this could matter.


Summary

The implementation is correct and the previous feedback has been fully incorporated. Only the PR description update is worth doing before merge; the other two points are cosmetic/defensive. Good addition to the analyzer suite — this will save users real pain when targeting older TFMs.

@thomhurst thomhurst merged commit 2c1c17c into main Mar 29, 2026
14 of 15 checks passed
@thomhurst thomhurst deleted the feat/polyfill-analyzer branch March 29, 2026 21:36
intellitect-bot pushed a commit to IntelliTect/EssentialCSharp.Web that referenced this pull request Mar 30, 2026
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.21.30 to
1.23.7.

<details>
<summary>Release notes</summary>

_Sourced from [TUnit's
releases](https://github.com/thomhurst/TUnit/releases)._

## 1.23.7

<!-- Release notes generated using configuration in .github/release.yml
at v1.23.7 -->

## What's Changed
### Other Changes
* feat: use results directory provided by Microsoft Testing Platform in
HtmlReporter by @​DavidZidar in
thomhurst/TUnit#5294
* feat: add benchmarks for Imposter and Mockolate mocking frameworks by
@​vbreuss in thomhurst/TUnit#5295
* feat: add TUnit0080 analyzer for missing polyfill types by @​thomhurst
in thomhurst/TUnit#5292
* fix: respect user-set TUnitImplicitUsings from Directory.Build.props
by @​thomhurst in thomhurst/TUnit#5280
* perf: optimize TUnit.Mocks hot paths by @​thomhurst in
thomhurst/TUnit#5300
### Dependencies
* chore(deps): update tunit to 1.22.19 by @​thomhurst in
thomhurst/TUnit#5296

## New Contributors
* @​DavidZidar made their first contribution in
thomhurst/TUnit#5294

**Full Changelog**:
thomhurst/TUnit@v1.22.19...v1.23.7

## 1.22.19

<!-- Release notes generated using configuration in .github/release.yml
at v1.22.19 -->

## What's Changed
### Other Changes
* Add mock library benchmarks: TUnit.Mocks vs Moq, NSubstitute,
FakeItEasy by @​Copilot in thomhurst/TUnit#5284
* perf: lazily initialize optional MockEngine collections by @​thomhurst
in thomhurst/TUnit#5289
* Always emit TUnit.Mocks.Generated namespace from source generator by
@​Copilot in thomhurst/TUnit#5282
### Dependencies
* chore(deps): update tunit to 1.22.6 by @​thomhurst in
thomhurst/TUnit#5285


**Full Changelog**:
thomhurst/TUnit@v1.22.6...v1.22.19

## 1.22.6

<!-- Release notes generated using configuration in .github/release.yml
at v1.22.6 -->

## What's Changed
### Other Changes
* fix: use IComputeResource to filter waitable Aspire resources by
@​thomhurst in thomhurst/TUnit#5278
* fix: preserve StateBag when creating per-test TestBuilderContext by
@​thomhurst in thomhurst/TUnit#5279
### Dependencies
* chore(deps): update tunit to 1.22.3 by @​thomhurst in
thomhurst/TUnit#5275


**Full Changelog**:
thomhurst/TUnit@v1.22.3...v1.22.6

## 1.22.3

<!-- Release notes generated using configuration in .github/release.yml
at v1.22.3 -->

## What's Changed
### Other Changes
* fix: pass assembly version properties to dotnet pack by @​thomhurst in
thomhurst/TUnit#5274
### Dependencies
* chore(deps): update tunit to 1.22.0 by @​thomhurst in
thomhurst/TUnit#5272


**Full Changelog**:
thomhurst/TUnit@v1.22.0...v1.22.3

## 1.22.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.22.0 -->

## What's Changed
### Other Changes
* perf: run GitVersion once in CI instead of per-project by @​slang25 in
thomhurst/TUnit#5259
* perf: disable GitVersion MSBuild task globally by @​thomhurst in
thomhurst/TUnit#5266
* fix: skip IResourceWithoutLifetime resources in Aspire fixture wait
logic by @​thomhurst in thomhurst/TUnit#5268
* fix: relax docs site Node.js engine constraint to >=24 by @​thomhurst
in thomhurst/TUnit#5269
* fix: catch unhandled exceptions in ExecuteRequestAsync to prevent IDE
RPC crashes by @​thomhurst in
thomhurst/TUnit#5271
* feat: register HTML report as MTP session artifact by @​thomhurst in
thomhurst/TUnit#5270
### Dependencies
* chore(deps): update tunit to 1.21.30 by @​thomhurst in
thomhurst/TUnit#5254
* chore(deps): update opentelemetry to 1.15.1 by @​thomhurst in
thomhurst/TUnit#5258
* chore(deps): bump node-forge from 1.3.1 to 1.4.0 in /docs by
@​dependabot[bot] in thomhurst/TUnit#5255
* chore(deps): bump picomatch from 2.3.1 to 2.3.2 in /docs by
@​dependabot[bot] in thomhurst/TUnit#5256
* chore(deps): update react by @​thomhurst in
thomhurst/TUnit#5261
* chore(deps): update node.js to >=18.20.8 by @​thomhurst in
thomhurst/TUnit#5262
* chore(deps): update node.js to v24 by @​thomhurst in
thomhurst/TUnit#5264


**Full Changelog**:
thomhurst/TUnit@v1.21.30...v1.22.0

Commits viewable in [compare
view](thomhurst/TUnit@v1.21.30...v1.23.7).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=TUnit&package-manager=nuget&previous-version=1.21.30&new-version=1.23.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
github-actions bot pushed a commit to IntelliTect/CodingGuidelines that referenced this pull request Mar 30, 2026
Updated [TUnit.Core](https://github.com/thomhurst/TUnit) from 1.21.6 to
1.23.7.

<details>
<summary>Release notes</summary>

_Sourced from [TUnit.Core's
releases](https://github.com/thomhurst/TUnit/releases)._

## 1.23.7

<!-- Release notes generated using configuration in .github/release.yml
at v1.23.7 -->

## What's Changed
### Other Changes
* feat: use results directory provided by Microsoft Testing Platform in
HtmlReporter by @​DavidZidar in
thomhurst/TUnit#5294
* feat: add benchmarks for Imposter and Mockolate mocking frameworks by
@​vbreuss in thomhurst/TUnit#5295
* feat: add TUnit0080 analyzer for missing polyfill types by @​thomhurst
in thomhurst/TUnit#5292
* fix: respect user-set TUnitImplicitUsings from Directory.Build.props
by @​thomhurst in thomhurst/TUnit#5280
* perf: optimize TUnit.Mocks hot paths by @​thomhurst in
thomhurst/TUnit#5300
### Dependencies
* chore(deps): update tunit to 1.22.19 by @​thomhurst in
thomhurst/TUnit#5296

## New Contributors
* @​DavidZidar made their first contribution in
thomhurst/TUnit#5294

**Full Changelog**:
thomhurst/TUnit@v1.22.19...v1.23.7

## 1.22.19

<!-- Release notes generated using configuration in .github/release.yml
at v1.22.19 -->

## What's Changed
### Other Changes
* Add mock library benchmarks: TUnit.Mocks vs Moq, NSubstitute,
FakeItEasy by @​Copilot in thomhurst/TUnit#5284
* perf: lazily initialize optional MockEngine collections by @​thomhurst
in thomhurst/TUnit#5289
* Always emit TUnit.Mocks.Generated namespace from source generator by
@​Copilot in thomhurst/TUnit#5282
### Dependencies
* chore(deps): update tunit to 1.22.6 by @​thomhurst in
thomhurst/TUnit#5285


**Full Changelog**:
thomhurst/TUnit@v1.22.6...v1.22.19

## 1.22.6

<!-- Release notes generated using configuration in .github/release.yml
at v1.22.6 -->

## What's Changed
### Other Changes
* fix: use IComputeResource to filter waitable Aspire resources by
@​thomhurst in thomhurst/TUnit#5278
* fix: preserve StateBag when creating per-test TestBuilderContext by
@​thomhurst in thomhurst/TUnit#5279
### Dependencies
* chore(deps): update tunit to 1.22.3 by @​thomhurst in
thomhurst/TUnit#5275


**Full Changelog**:
thomhurst/TUnit@v1.22.3...v1.22.6

## 1.22.3

<!-- Release notes generated using configuration in .github/release.yml
at v1.22.3 -->

## What's Changed
### Other Changes
* fix: pass assembly version properties to dotnet pack by @​thomhurst in
thomhurst/TUnit#5274
### Dependencies
* chore(deps): update tunit to 1.22.0 by @​thomhurst in
thomhurst/TUnit#5272


**Full Changelog**:
thomhurst/TUnit@v1.22.0...v1.22.3

## 1.22.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.22.0 -->

## What's Changed
### Other Changes
* perf: run GitVersion once in CI instead of per-project by @​slang25 in
thomhurst/TUnit#5259
* perf: disable GitVersion MSBuild task globally by @​thomhurst in
thomhurst/TUnit#5266
* fix: skip IResourceWithoutLifetime resources in Aspire fixture wait
logic by @​thomhurst in thomhurst/TUnit#5268
* fix: relax docs site Node.js engine constraint to >=24 by @​thomhurst
in thomhurst/TUnit#5269
* fix: catch unhandled exceptions in ExecuteRequestAsync to prevent IDE
RPC crashes by @​thomhurst in
thomhurst/TUnit#5271
* feat: register HTML report as MTP session artifact by @​thomhurst in
thomhurst/TUnit#5270
### Dependencies
* chore(deps): update tunit to 1.21.30 by @​thomhurst in
thomhurst/TUnit#5254
* chore(deps): update opentelemetry to 1.15.1 by @​thomhurst in
thomhurst/TUnit#5258
* chore(deps): bump node-forge from 1.3.1 to 1.4.0 in /docs by
@​dependabot[bot] in thomhurst/TUnit#5255
* chore(deps): bump picomatch from 2.3.1 to 2.3.2 in /docs by
@​dependabot[bot] in thomhurst/TUnit#5256
* chore(deps): update react by @​thomhurst in
thomhurst/TUnit#5261
* chore(deps): update node.js to >=18.20.8 by @​thomhurst in
thomhurst/TUnit#5262
* chore(deps): update node.js to v24 by @​thomhurst in
thomhurst/TUnit#5264


**Full Changelog**:
thomhurst/TUnit@v1.21.30...v1.22.0

## 1.21.30

<!-- Release notes generated using configuration in .github/release.yml
at v1.21.30 -->

## What's Changed
### Other Changes
* feat: add test discovery Activity span for tracing by @​thomhurst in
thomhurst/TUnit#5246
* Fix mock generator not preserving nullable annotations on reference
types by @​Copilot in thomhurst/TUnit#5251
* Fix ITestSkippedEventReceiver not firing for [Skip]-attributed tests
by @​thomhurst in thomhurst/TUnit#5253
* Use CallerArgumentExpression for TestDataRow by default. by @​m-gasser
in thomhurst/TUnit#5135
### Dependencies
* chore(deps): update tunit to 1.21.24 by @​thomhurst in
thomhurst/TUnit#5247


**Full Changelog**:
thomhurst/TUnit@v1.21.24...v1.21.30

## 1.21.24

<!-- Release notes generated using configuration in .github/release.yml
at v1.21.24 -->

## What's Changed
### Other Changes
* Fix OpenTelemetry missing root span by reordering session activity
lifecycle by @​Copilot in thomhurst/TUnit#5245
### Dependencies
* chore(deps): update tunit to 1.21.20 by @​thomhurst in
thomhurst/TUnit#5241
* chore(deps): update dependency stackexchange.redis to 2.12.8 by
@​thomhurst in thomhurst/TUnit#5243


**Full Changelog**:
thomhurst/TUnit@v1.21.20...v1.21.24

## 1.21.20

<!-- Release notes generated using configuration in .github/release.yml
at v1.21.20 -->

## What's Changed
### Other Changes
* fix: respect TUnitImplicitUsings set in Directory.Build.props by
@​thomhurst in thomhurst/TUnit#5225
* feat: covariant assertions for interfaces and non-sealed classes by
@​thomhurst in thomhurst/TUnit#5226
* feat: support string-to-parseable type conversions in [Arguments] by
@​thomhurst in thomhurst/TUnit#5227
* feat: add string length range assertions by @​thomhurst in
thomhurst/TUnit#4935
* Fix BeforeEvery/AfterEvery hooks for Class and Assembly not being
executed by @​Copilot in thomhurst/TUnit#5239
### Dependencies
* chore(deps): update tunit to 1.21.6 by @​thomhurst in
thomhurst/TUnit#5228
* chore(deps): update dependency gitversion.msbuild to 6.7.0 by
@​thomhurst in thomhurst/TUnit#5229
* chore(deps): update dependency gitversion.tool to v6.7.0 by
@​thomhurst in thomhurst/TUnit#5230
* chore(deps): update aspire to 13.2.0 - autoclosed by @​thomhurst in
thomhurst/TUnit#5232
* chore(deps): update dependency typescript to v6 by @​thomhurst in
thomhurst/TUnit#5233
* chore(deps): update dependency polyfill to 9.23.0 by @​thomhurst in
thomhurst/TUnit#5235
* chore(deps): update dependency polyfill to 9.23.0 by @​thomhurst in
thomhurst/TUnit#5236


**Full Changelog**:
thomhurst/TUnit@v1.21.6...v1.21.20

Commits viewable in [compare
view](thomhurst/TUnit@v1.21.6...v1.23.7).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=TUnit.Core&package-manager=nuget&previous-version=1.21.6&new-version=1.23.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant