fix: apply 'unmanaged' constraint handling to remaining source generators - #6474
#6474fix: apply 'unmanaged' constraint handling to remaining source generators#6474thomhurst merged 1 commit intomainthomhurst/TUnit:mainfrom fix/unmanaged-constraint-sibling-generatorsthomhurst/TUnit:fix/unmanaged-constraint-sibling-generatorsCopy head branch name to clipboard
Conversation
Follow-up to #6473 (review feedback): the same Roslyn quirk — 'unmanaged' also sets HasValueTypeConstraint — was latent in sibling generators: - CollectionShapeAssertionGenerator: emitted 'where T : struct, unmanaged' (CS0449) for forwarded methods with an 'unmanaged' type parameter. - PropertyInjectionSourceGenerator: 'struct' branch of the else-if chain shadowed the 'unmanaged' branch, silently downgrading the constraint. - AssertionExtensionGenerator, AssertionMethodGenerator, MethodAssertionGenerator, ShouldExtensionGenerator: never emitted 'unmanaged' at all, downgrading it to 'struct' (CS8377 when the generated code forwards to the unmanaged-constrained original). All sites now emit 'struct' only when the type parameter is not 'unmanaged'-constrained, and emit 'unmanaged' where it was missing. Refs #6471
Greptile SummaryThis PR propagates the Roslyn quirk fix from #6473 — where
Confidence Score: 4/5Safe to merge — all constraint-emission sites that were producing invalid or silently-downgraded constraints are now corrected, and the intentionally-untouched sites (MockImplBuilder explicit-impl, TestMetadataGenerator checking) are well-reasoned. The logic changes are straightforward and self-consistent across all six patched sites. The only gap is that new snapshot tests were added only for The generator files themselves look correct; Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[ITypeParameterSymbol] --> B{HasReferenceTypeConstraint?}
B -- Yes --> C[emit class]
B -- No --> D{HasValueTypeConstraint AND NOT HasUnmanagedTypeConstraint?}
D -- Yes --> E[emit struct]
D -- No --> F{HasUnmanagedTypeConstraint?}
F -- Yes --> G[emit unmanaged]
F -- No --> H{HasNotNullConstraint?}
H -- Yes --> I[emit notnull]
H -- No --> J[no primary constraint]
C --> K[emit ConstraintTypes loop]
E --> K
G --> K
I --> K
J --> K
K --> L{HasConstructorConstraint?}
L -- Yes --> M[emit new constraint]
L -- No --> N[done]
M --> N
Reviews (1): Last reviewed commit: "fix: apply 'unmanaged' constraint handli..." | Re-trigger Greptile |
|
|
||
| [Test] | ||
| public Task MethodWithUnmanagedConstraint() => RunTest( | ||
| Path.Combine(Git.TestsDirectory.FullName, | ||
| "TUnit.Assertions.SourceGenerator.Tests", | ||
| "TestData", | ||
| "MethodWithUnmanagedConstraint.cs"), | ||
| async generatedFiles => | ||
| { | ||
| await Assert.That(generatedFiles).Count().IsEqualTo(1); | ||
|
|
||
| var mainFile = generatedFiles.First(); | ||
| await Assert.That(mainFile).IsNotNull(); | ||
|
|
||
| // 'unmanaged' preserved as-is: not combined with 'struct' (CS0449) and | ||
| // not silently downgraded to 'struct' (#6471) | ||
| await Assert.That(mainFile).Contains("Int_IsBlittableDefault_T_Assertion<T>"); | ||
| await Assert.That(mainFile).Contains("where T : unmanaged"); | ||
| await Assert.That(mainFile).DoesNotContain("struct, unmanaged"); | ||
| await Assert.That(mainFile).DoesNotContain("where T : struct"); | ||
| }); | ||
|
|
||
| [Test] |
There was a problem hiding this comment.
Test coverage limited to one generator
The new test exercises MethodAssertionGenerator, but five other generators were also fixed in this PR (AssertionExtensionGenerator, CollectionShapeAssertionGenerator, ShouldExtensionGenerator, PropertyInjectionSourceGenerator, and AssertionMethodGenerator). If any of those sites regresses, no snapshot test would catch it. Parallel test fixtures for the other generators would make this guard complete.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Code reviewReviewed the diff (5 source generators + PropertyInjectionSourceGenerator, plus the new Correctness: Traced through each of the six changed sites against Roslyn's
CLAUDE.md compliance: Scope note (non-blocking): Only No issues found. |
Updated [TUnit.Core](https://github.com/thomhurst/TUnit) from 1.61.15 to 1.61.38. <details> <summary>Release notes</summary> _Sourced from [TUnit.Core's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.61.38 <!-- Release notes generated using configuration in .github/release.yml at v1.61.38 --> ## What's Changed ### Other Changes * fix: apply 'unmanaged' constraint handling to remaining source generators by @thomhurst in thomhurst/TUnit#6474 * feat: expose data source attributes on TestContext by @thomhurst in thomhurst/TUnit#6477 ### Dependencies * chore(deps): update tunit to 1.61.35 by @thomhurst in thomhurst/TUnit#6475 **Full Changelog**: thomhurst/TUnit@v1.61.35...v1.61.38 ## 1.61.35 <!-- Release notes generated using configuration in .github/release.yml at v1.61.35 --> ## What's Changed ### Other Changes * fix(mocks): omit 'struct' constraint when type parameter has 'unmanaged' constraint by @thomhurst in thomhurst/TUnit#6473 ### Dependencies * chore(deps): bump fast-uri from 3.1.2 to 3.1.4 in /docs by @dependabot[bot] in thomhurst/TUnit#6466 * chore(deps): update tunit to 1.61.29 by @thomhurst in thomhurst/TUnit#6467 * chore(deps): update dependency verify.tool to v0.8.0 by @thomhurst in thomhurst/TUnit#6470 * chore(deps): update dependency docusaurus-plugin-llms to ^0.5.1 by @thomhurst in thomhurst/TUnit#6472 **Full Changelog**: thomhurst/TUnit@v1.61.29...v1.61.35 ## 1.61.29 <!-- Release notes generated using configuration in .github/release.yml at v1.61.29 --> ## What's Changed ### Other Changes * perf: remove EnumerableAsyncProcessor dependency by @thomhurst in thomhurst/TUnit#6465 ### Dependencies * chore(deps): bump shell-quote from 1.8.4 to 1.10.0 in /docs by @dependabot[bot] in thomhurst/TUnit#6462 * chore(deps): bump body-parser from 1.20.5 to 1.20.6 in /docs by @dependabot[bot] in thomhurst/TUnit#6461 * chore(deps): bump webpack-dev-server from 5.2.5 to 5.2.6 in /docs by @dependabot[bot] in thomhurst/TUnit#6460 * chore(deps): update dependency microsoft.kiota.abstractions to v2 by @thomhurst in thomhurst/TUnit#6464 * chore(deps): update tunit to 1.61.23 by @thomhurst in thomhurst/TUnit#6463 **Full Changelog**: thomhurst/TUnit@v1.61.23...v1.61.29 ## 1.61.23 <!-- Release notes generated using configuration in .github/release.yml at v1.61.23 --> ## What's Changed ### Other Changes * fix(mocks): emit 'where T : default' for interface-constrained generic methods by @thomhurst in thomhurst/TUnit#6458 ### Dependencies * chore(deps): update tunit to 1.61.15 by @thomhurst in thomhurst/TUnit#6450 * chore(deps): update actions/checkout action to v7.0.1 by @thomhurst in thomhurst/TUnit#6453 * chore(deps): update verify to 31.26.0 by @thomhurst in thomhurst/TUnit#6454 * chore(deps): update verify to 31.27.0 by @thomhurst in thomhurst/TUnit#6457 * chore(deps): update react to ^19.2.8 by @thomhurst in thomhurst/TUnit#6459 **Full Changelog**: thomhurst/TUnit@v1.61.15...v1.61.23 Commits viewable in [compare view](thomhurst/TUnit@v1.61.15...v1.61.38). </details> [](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>
Summary
Follow-up to the review feedback on #6473: the Roslyn quirk fixed there (
unmanagedalso setsHasValueTypeConstraint) was still latent in sibling generators. This PR fixes every remaining constraint-emission site.Fixes
CS0449 (invalid
struct, unmanaged):CollectionShapeAssertionGenerator.Constraint— the exact bug from [TUnit.Mocks] Generated mock for interface with 'unmanaged' type constraint does not compile (emits 'where T : struct, unmanaged') #6471, reachable via forwarded collection-assertion methods with anunmanaged-constrained type parameter.structnow gated on!HasUnmanagedTypeConstraint(also reordered sounmanagedprecedesnotnull, matching the other emitters).Silent downgrade to
struct(droppedunmanaged):PropertyInjectionSourceGenerator— theelse ifchain checkedHasValueTypeConstraintbeforeHasUnmanagedTypeConstraint, making theunmanagedbranch unreachable. Reordered sounmanagedis checked first.AssertionExtensionGenerator(both sites),AssertionMethodGenerator,MethodAssertionGenerator,ShouldExtensionGenerator— never emittedunmanagedat all, so it was downgraded to barestruct(CS8377 when generated code forwards to theunmanaged-constrained original). All now emit the guardedstruct/unmanagedpair.Intentionally untouched:
MockImplBuilderexplicit-impl clauses — restatingstructthere is correct: onlyclass/struct/defaultare legal on explicit interface implementations.TestMetadataGeneratorsites — constraint checking, not emission; the value-type check remains valid forunmanagedparameters.Full consolidation of the ~7 duplicated constraint builders into a shared helper (as suggested in the review) is left as a separate refactor — the generator projects don't currently share a common library.
Tests
MethodWithUnmanagedConstrainttest (TestData + all 4 TFM Verify snapshots) assertingwhere T : unmanagedis emitted — notstruct, unmanagedand not a downgradedstruct.unmanaged-constrained parameters.Refs #6471