fix(sourcegen): drop covariant TActual when [GenerateAssertion] method has its own type parameters#5935
Conversation
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| CodeStyle | 2 minor |
🟢 Metrics 9 complexity
Metric Results Complexity 9
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Code Review
Summary
This is a clean, surgical bug fix with excellent test coverage. The root cause diagnosis and fix are both correct.
The Fix (MethodAssertionGenerator.cs line 999)
The single conjunct && genericParams.Count == 0 is the right approach. With covariance enabled on a generic method, the generator produces a two-type-parameter extension (e.g. HasItem<TActual, T>). C# forbids partial type-argument specification, so a call site supplying one explicit argument — the common case — fails with CS1929. Disabling covariance for this shape yields a single-generic extension that callers can bind without restriction, and the existing downstream logic (generic declaration, where clause, source type name) already handles the isCovariant = false path correctly.
One architectural question worth considering: the fix ties the covariance decision to a count (genericParams.Count == 0). This is a proxy for "the method has no user-visible type parameters of its own." If a future scenario needs to suppress covariance for a different reason, the condition may become harder to read. An alternative would be an explicit SuppressCovariance flag on AssertionMethodData, computed once during collection. That said, for the current scope this is not a blocker — the comment explains the WHY clearly enough.
Test Coverage
Nine integration cases in GenerateAssertionGenericMethodOnNonSealedReceiverTests.cs are well chosen:
- The failing case, two sealed/non-generic control cases, full-inference control, multi-param generics, interface receiver, constrained type param, async result, and the upcast workaround.
- The
GenericMethod_DerivedStaticReceiver_UpcastWorkaroundtest explicitly documents the tradeoff rather than hiding it, which is the right call.
Generator-level test (MethodAssertionGeneratorTests.MethodOnConcreteNonSealedReceiver) is appropriately specific: it asserts the single-parameter shape and negatively asserts TActual and where TActual : are absent.
Minor Observations (non-blocking)
-
Test type namespace pollution —
NonSealedContainer,SealedContainer,IContainerInterface, etc. are defined directly in theTUnit.Assertions.Testsnamespace root. Other test files follow this pattern so it is consistent, but a nested namespace (e.g.TUnit.Assertions.Tests.GenerateAssertionGenericMethodOnNonSealedReceiver) would reduce the chance of future name collisions with other test data files. -
Verbose class-level XML doc — The lengthy
<summary>onGenerateAssertionGenericMethodOnNonSealedReceiverTestsrestates the commit message. The test method names and individual test bodies already communicate intent; the class doc does not add information a future reader could not derive from the code. -
Four identical snapshot files — All four TFM snapshots are byte-identical (expected, since the generator emits the same output across TFMs). This is a project convention and not a problem, just worth being aware of when generator behaviour starts diverging per TFM in future.
Verdict
Approve. The fix is minimal and correct, the downstream logic handles the isCovariant = false path without modification, and the test suite covers the affected shape thoroughly including the documented tradeoff. No blocking issues.
…d has its own type parameters When [GenerateAssertion] applies to a generic method with a concrete non-sealed receiver, the generator previously prepended a covariant receiver-type parameter to the produced extension. The resulting two- type-parameter signature could not bind at a call site that named the method's own type arguments explicitly, because C# does not permit partial type-argument specification. The call failed with CS1929. Suppress the receiver-type covariance when the source method declares its own type parameters. Receivers of a more-derived static type can reach the assertion via upcast. Closes thomhurst#5934.
f64fb61 to
6efdc4a
Compare
|
Thanks! |
Updated [TUnit.Core](https://github.com/thomhurst/TUnit) from 1.44.0 to 1.45.8. <details> <summary>Release notes</summary> _Sourced from [TUnit.Core's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.45.8 <!-- Release notes generated using configuration in .github/release.yml at v1.45.8 --> ## What's Changed ### Other Changes * fix(aspire): route CreateHttpClient through IHttpClientFactory by @thomhurst in thomhurst/TUnit#5957 ### Dependencies * chore(deps): update tunit to 1.45.0 by @thomhurst in thomhurst/TUnit#5949 * chore(deps): update dependency dompurify to v3.4.5 by @thomhurst in thomhurst/TUnit#5951 * chore(deps): update dependency microsoft.testing.extensions.codecoverage to 18.7.0 by @thomhurst in thomhurst/TUnit#5953 * chore(deps): update dependency coverlet.collector to 10.0.1 by @thomhurst in thomhurst/TUnit#5952 * chore(deps): update dependency polyfill to 10.6.0 by @thomhurst in thomhurst/TUnit#5955 * chore(deps): update dependency polyfill to 10.6.0 by @thomhurst in thomhurst/TUnit#5954 **Full Changelog**: thomhurst/TUnit@v1.45.0...v1.45.8 ## 1.45.0 <!-- Release notes generated using configuration in .github/release.yml at v1.45.0 --> ## What's Changed ### Other Changes * fix(generator): place CallerArgumentExpression before params in [GenerateAssertion] emit by @JohnVerheij in thomhurst/TUnit#5940 * fix(sourcegen): drop covariant TActual when [GenerateAssertion] method has its own type parameters by @JohnVerheij in thomhurst/TUnit#5935 * feat(assertions): add CancellationToken overload to WaitsFor and Eventually by @JohnVerheij in thomhurst/TUnit#5938 * fix(html-report): extract categories using MTP Key=name convention by @thomhurst in thomhurst/TUnit#5946 * feat(html-report): rewrite as split-pane design template by @thomhurst in thomhurst/TUnit#5947 ### Dependencies * chore(deps): update microsoft.testing to 2.2.3 by @thomhurst in thomhurst/TUnit#5927 * chore(deps): update mstest to 4.2.3 by @thomhurst in thomhurst/TUnit#5928 * chore(deps): update tunit to 1.44.39 by @thomhurst in thomhurst/TUnit#5929 * chore(deps): update aspire to 13.3.3 by @thomhurst in thomhurst/TUnit#5933 * chore(deps): update dependency dompurify to v3.4.4 by @thomhurst in thomhurst/TUnit#5944 * chore(deps): update dependency qs to v6.15.2 by @thomhurst in thomhurst/TUnit#5941 **Full Changelog**: thomhurst/TUnit@v1.44.39...v1.45.0 ## 1.44.39 <!-- Release notes generated using configuration in .github/release.yml at v1.44.39 --> ## What's Changed ### Other Changes * fix(tests): retry trx read to dodge MTP post-exit flush race on Windows by @thomhurst in thomhurst/TUnit#5888 * fix(pipeline): timeout + retry InstallPlaywrightModule so a hung download fails fast by @thomhurst in thomhurst/TUnit#5889 * fix(otel): require two consecutive idle windows in DrainAsync to catch in-transit POSTs by @thomhurst in thomhurst/TUnit#5890 * test(assertions): drop flaky wall-clock upper bound on WaitsFor timeout test by @thomhurst in thomhurst/TUnit#5886 * fix(sourcegen): drop spurious ')' in MethodAssertionGenerator Task<bool> emit by @JohnVerheij in thomhurst/TUnit#5920 * fix(sourcegen): merge generic parameter lists in [AssertionExtension] emit by @JohnVerheij in thomhurst/TUnit#5921 * fix(aspnetcore): scope correlation processor per-factory to stop cross-factory tag leak by @thomhurst in thomhurst/TUnit#5891 * Changed FSharp.Core version to 10.1.300 by @licon4812 in thomhurst/TUnit#5909 * feat(mocks): add Mock.HttpClientFactory() helper by @thomhurst in thomhurst/TUnit#5894 * Harden WaitsFor timeout test by @thomhurst in thomhurst/TUnit#5926 * fix(sourcegen): emit `default` literal for value-type assertion parameters by @JohnVerheij in thomhurst/TUnit#5919 ### Dependencies * chore(deps): update dependency nunit to 4.6.0 by @thomhurst in thomhurst/TUnit#5826 * chore(deps): update tunit to 1.44.0 by @thomhurst in thomhurst/TUnit#5882 * chore(deps): update dependency mockolate to 3.2.0 by @thomhurst in thomhurst/TUnit#5892 * chore(deps): update dependency yaml to v2.9.0 by @thomhurst in thomhurst/TUnit#5887 * chore(deps): update dependency nuget.protocol to 7.6.0 by @thomhurst in thomhurst/TUnit#5897 * chore(deps): update dependency microsoft.entityframeworkcore to 10.0.8 by @thomhurst in thomhurst/TUnit#5898 * chore(deps): update dependency microsoft.templateengine.authoring.cli to v10.0.300 by @thomhurst in thomhurst/TUnit#5899 * chore(deps): update microsoft.extensions by @thomhurst in thomhurst/TUnit#5905 * chore(deps): update microsoft.aspnetcore to 10.0.8 by @thomhurst in thomhurst/TUnit#5904 * chore(deps): update dependency microsoft.templateengine.authoring.templateverifier to 10.0.300 by @thomhurst in thomhurst/TUnit#5902 * chore(deps): update aspire to 13.3.1 by @thomhurst in thomhurst/TUnit#5900 * chore(deps): update dependency system.commandline to 2.0.8 by @thomhurst in thomhurst/TUnit#5903 * chore(deps): update dependency azure.storage.blobs to 12.28.0 by @thomhurst in thomhurst/TUnit#5910 * chore(deps): update dependency dotnet-sdk to v10.0.300 by @thomhurst in thomhurst/TUnit#5901 * chore(deps): update dependency stackexchange.redis to 2.13.1 by @thomhurst in thomhurst/TUnit#5906 * chore(deps): update aspire to 13.3.2 by @thomhurst in thomhurst/TUnit#5924 * chore(deps): bump mermaid from 11.12.2 to 11.15.0 in /docs by @dependabot[bot] in thomhurst/TUnit#5893 * chore(deps): update dependency streamjsonrpc to 2.24.92 by @thomhurst in thomhurst/TUnit#5915 * chore(deps): update dependency dompurify to v3.4.3 by @thomhurst in thomhurst/TUnit#5913 * chore(deps): update microsoft.build to 18.6.3 by @thomhurst in thomhurst/TUnit#5914 **Full Changelog**: thomhurst/TUnit@v1.44.0...v1.44.39 Commits viewable in [compare view](thomhurst/TUnit@v1.44.0...v1.45.8). </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>
Description
Suppresses receiver-type covariance in
MethodAssertionGeneratorwhen the source method declares its own type parameters. The covariantTActualparameter previously prepended to the generated extension caused CS1929 at call sites supplying explicit type arguments, because C# does not permit partial type-argument specification. The fix is a single boolean conjunct (genericParams.Count == 0) plus an explanatory comment. Receivers of a more-derived static type can still reach the assertion via upcast.Related Issue
Fixes #5934
Type of Change
Checklist
Required
TUnit-Specific Requirements
TUnit.Core.SourceGenerator)TUnit.Engine)TUnit.Core.SourceGenerator.Testsand/orTUnit.PublicAPItests.received.txtfiles and accepted them as.verified.txt.verified.txtfiles[DynamicallyAccessedMembers]annotationsdotnet publish -p:PublishAot=trueTesting
dotnet test)Additional Notes
Decision matrix covered by the regression tests
TUnit.Assertions.Tests/GenerateAssertionGenericMethodOnNonSealedReceiverTests.csadds nine cases:<TFirst, TSecond>): uniform behaviour, asserts thegenericParams.Count > 0guard is not 1-specificIsCovariantCandidateaccepts both class and interface; the fix applies to bothwhere T : IParsable<T>constraint: the constraint itself is incidental to the trigger, but represents the dominant real-world consumer shapeTask<AssertionResult>) return type: orthogonal to the extension signatureGenerator-level coverage
MethodAssertionGeneratorTests.MethodOnConcreteNonSealedReceiverasserts the new emit shape: single<T>type parameter, exact-receiverIAssertionSource<NonSealedReceiverType>, noTActual, nowhere TActual : ....*.verified.txtsnapshots (one per TFM: net472, net8.0, net9.0, net10.0). All four are byte-identical.Impact on existing snapshots
None. The TUnit codebase has no
[GenerateAssertion]method that matches the affected shape (generic source method on a concrete non-sealed reference-type receiver). All existing snapshots regenerated identically. FullTUnit.Assertions.Tests,TUnit.Assertions.SourceGenerator.Tests,TUnit.Assertions.Should.Tests, andTUnit.Assertions.Should.SourceGenerator.Testspass green locally.Related
TUnit.Assertions.Should.SourceGeneratorwas inspected; it does not use the covariance helper, so the bug is not present there.The analogous case for the other generator path is covered by
AssertionExtensionGeneratorTests.ConcreteReceiverWithExtraGeneric, which asserts the single merged generic-parameter list (no adjacent<X><Y>blocks) for[AssertionExtension]classes. The bug inMethodAssertionGeneratoris the same family (two type parameters on the call signature versus one explicit argument at the call site), and the fix follows the same instinct.Issue #5922 reports the user-visible friction in the
[AssertionExtension]path; that is out of scope for this PR but tracked there.