fix(sourcegen): fully-qualify Linq calls in params array binding (#6140)#6141
Conversation
The dynamic-count params/array binding emitted
`Enumerable.Range(...).Select(...).ToArray()` using the extension-method
chain. Generated `.g.cs` files only import `System.Linq` when the project
has `ImplicitUsings` enabled, so projects without it failed to compile
with CS1061 ("IEnumerable<T> does not contain a definition for Select").
Rewrite as fully-qualified static calls
`Enumerable.ToArray(Enumerable.Select(Enumerable.Range(...), ...))` so the
generated code compiles regardless of the consuming project's usings.
Regressed in #6122. Snapshots regenerated.
There was a problem hiding this comment.
Code Review
Summary: This is a correct, targeted fix for the ImplicitUsings-off compilation failure (CS1061). The root cause is sound, the approach works, and the snapshots are properly updated.
A few observations worth considering:
1. Alternative: Array.ConvertAll instead of Enumerable.Select + Enumerable.ToArray
The generated code converts a segment of args[] into a typed array. System.Array.ConvertAll is designed exactly for this:
// Current (fully-qualified LINQ, nested calls):
global::System.Linq.Enumerable.ToArray(
global::System.Linq.Enumerable.Select(
global::System.Linq.Enumerable.Range(0, args.Length),
i => global::TUnit.Core.Helpers.CastHelper.Cast<T>(args[i])))
// Alternative (no LINQ dependency at all):
global::System.Array.ConvertAll(
global::System.Array.Copy / subarray approach...)Because args is object[], you can't directly pass it to Array.ConvertAll<object, T> without a sub-slice. For the start == 0 case it would be straightforward. For the offset case (Range(1, args.Length - 1)) you'd still need a sub-array or an index-arithmetic lambda.
A simpler LINQ-free alternative that avoids creating an intermediate IEnumerable<int> via Range:
// In TupleArgumentHelper.cs, emit:
var result = new T[args.Length - start];
for (int i = 0; i < result.Length; i++) result[i] = CastHelper.Cast<T>(args[start + i]);This is more allocation-efficient than Range + Select + ToArray (three allocations vs. one), and requires no LINQ at all. Since this runs at test discovery time rather than a hot path it is not critical, but the Performance First principle from CLAUDE.md suggests it is worth noting. The fix as-written is acceptable though.
2. Minor: The DynamicTestsGenerator still emits using global::System.Linq;
In DynamicTestsGenerator.cs line 92, the generated file still adds using global::System.Linq;. That's fine for its own generated code (it presumably uses LINQ elsewhere in those files), but it means the TupleArgumentHelper-generated binding code that ends up in those files would not have had the CS1061 bug anyway. The bug only affected the test-registration generated files (the .g.cs files from the main test source generator), not the dynamic-tests path.
The fix correctly targets TupleArgumentHelper.cs which feeds the test-registration path. Just noting this so the scope is clear.
3. The test harness blind-spot acknowledgement is good
The PR description correctly identifies that TestsBase.cs injects global using global::System.Linq;, masking this class of bug. The regenerated snapshots now lock the fully-qualified form. Consider adding a code comment in TestsBase.cs near that global using injection warning that it intentionally hides missing-using issues:
// WARNING: This global using means snapshot tests will NOT catch CS1061-style
// missing-using bugs in generated code. Generated code must use fully-qualified
// static calls (e.g. Enumerable.ToArray) rather than extension methods.This would make the constraint explicit for future contributors adding new LINQ calls to generated output.
Verdict
The fix is correct and complete. The approach (fully-qualifying the static method calls) is the standard idiom for source generators and matches what this codebase already does elsewhere (e.g. MetadataGenerationHelper.cs uses global::System.Linq.Enumerable.FirstOrDefault). The snapshots are properly updated. The note about Array.ConvertAll is a suggestion for a follow-up micro-optimization, not a blocker.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
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.
…6140) Compiles the generated output WITHOUT `using System.Linq` in scope and asserts the resulting C# compilation has no errors. Fails with CS1061 against the pre-fix extension-method chain; passes with the fully-qualified static calls. The existing snapshot harness can't catch this class of bug: TestsBase injects `global using global::System.Linq;` and only inspects generator diagnostics, never the resulting compilation's C# errors.
There was a problem hiding this comment.
Code Review (Updated)
Following up on the previous review — one real bug was found that the earlier pass missed.
Bug: Regression test never exercises the dynamic LINQ code path
File: TUnit.Core.SourceGenerator.Tests/ParamsArrayCompilationTests.cs, lines 267–298
The new test Generated_params_binding_compiles_without_System_Linq_in_scope is intended to verify that the generated params-array binding compiles without using System.Linq. However, the test cases provided never reach the default arm where the LINQ calls (Enumerable.ToArray(Enumerable.Select(Enumerable.Range(...), ...))) live.
Why: The generator creates static switch arms for args.Length == 0 through args.Length == 10 (cap is maxParamsCases = 10). Only the default: arm uses the dynamic LINQ path. The test provides:
[Arguments(RollingOrder.NewestFirst, 2, 1)]→ 3 total args → hitscase 3→ usesnew int[] { ... }(no LINQ)[Arguments("a", "b", "c")]→ 3 args → hitscase 3→ usesnew string[] { ... }(no LINQ)
The static case arms use new T[] construction with no LINQ at all. If this fix were reverted so the default: arm emitted .Select(...).ToArray() extension methods, this test would still pass because it never reaches that arm.
How to fix: Add a test invocation with more than 10 arguments to force the default arm:
// In Source string — add a method with 11+ [Arguments] values:
[Test]
[Arguments(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)]
public void ManyParams(params int[] values)
{
}This forces args.Length = 11 > 10 = maxParamsCases, which hits the default: arm and exercises the Enumerable.ToArray(Enumerable.Select(Enumerable.Range(...))) code that is the actual fix being guarded.
Previous findings (still relevant)
The suggestions from the prior review remain open:
Array.ConvertAll/ loop alternative — no LINQ dependency at all, one fewer allocation. Not a blocker but worth a follow-up.TestsBase.cswarning comment — the injectedglobal using System.Linqcontinues to mask this class of bug for all snapshot tests. A comment there would help future contributors.
Core fix verdict
The change to TupleArgumentHelper.cs itself is correct: the static Enumerable.ToArray(Enumerable.Select(Enumerable.Range(...))) form is the standard source-generator idiom, argument ordering is right, the rangeCount arithmetic is correct, and the absolute-index lambda is preserved. The snapshots are properly updated.
The one actionable item is the test coverage gap — the regression guard won't catch a revert to extension-method form without the fix above.
…6140) Addresses PR review feedback: document in TestsBase that the injected `global using System.Linq;` hides CS1061-style bugs from snapshot tests, so generated code must use fully-qualified static calls.
|
Thanks for the reviews. Responding to each: Review #2 — "regression test never exercises the dynamic LINQ path": This is incorrect. The premise conflates runtime reachability with compilation. The I verified empirically: reverting only the So the test does guard the fix. No change needed; an 11+ arg case would only add runtime-reachability coverage, which is irrelevant for a compile-only assertion. Review #1, point 3 — Review #1, point 1 — |
There was a problem hiding this comment.
Code Review
Overall: Clean, correct fix for a real bug. The root-cause diagnosis is accurate and the test strategy is well-thought-out. A few notes below.
Core Fix —
The one-line change is correct. Extension methods require using System.Linq in scope; generated .g.cs files only have that when ImplicitUsings is enabled. Switching to fully-qualified static invocation is exactly right.
Architectural suggestion: Consider eliminating LINQ from generated output entirely rather than qualifying it. The pattern is a simple index-based cast loop — emitting a for loop or a new T[count] + indexed init avoids any dependency on System.Linq in generated code and would be more explicit about what's happening:
// Instead of the current 3-level nested Enumerable call:
// var array = new T[count]; for (int i = 0; i < count; i++) array[i] = Cast<T>(args[start + i]);This approach:
- Is easier to read in the generated output (important for debugging)
- Removes the Enumerable dependency entirely rather than working around its surface-level form
- Avoids IEnumerable intermediate allocation (though this is minor since ToArray follows immediately)
The current fix is correct for the issue at hand — this is a suggestion for a follow-up if you want to clean up generated code ergonomics.
New Test —
This is the right kind of regression test. It specifically targets the blind spot you identified in TestsBase.cs: snapshot tests compile with injected global usings, so they can't catch missing-using bugs. Compiling the generated output separately without System.Linq in scope correctly isolates the regression.
One note: output.GetDiagnostics() on the updated Compilation runs semantic analysis across all syntax trees including generated ones, so CS1061 will surface here — the approach is sound.
Minor: the generated variable captured for failure output is a good debugging touch.
Comment
The warning explaining why snapshot tests won't catch this class of bug is a valuable addition. Future contributors will understand why ParamsArrayCompilationTests exists and why generated code must use fully-qualified calls.
Minor Notes
- The
new string[0]/new int[0]fallback in generated code could useArray.Empty<string>()/Array.Empty<int>()to avoid per-call allocations, but this is cosmetic. - The BOM removal (
// <auto-generated/>→// <auto-generated/>) in several snapshot files is a harmless bonus cleanup.
Verdict: The fix is correct and the test coverage addresses the specific failure mode well. The architectural note above is optional improvement, not a blocker. Good work tracking down the implicit-usings dependency.
There was a problem hiding this comment.
Code Review
Overall: Clean, correct fix for a real bug. The root-cause diagnosis is accurate and the test strategy is well-thought-out.
Core Fix — TupleArgumentHelper.cs
The one-line change is correct. Extension methods require using System.Linq in scope; generated .g.cs files only have that when ImplicitUsings is enabled. Switching to fully-qualified static invocation is exactly right.
Architectural suggestion: Consider eliminating LINQ from generated output entirely rather than qualifying it. The pattern is a simple index-based cast — emitting a for-loop avoids any dependency on System.Linq in the output code:
// Rather than: Enumerable.ToArray(Enumerable.Select(Enumerable.Range(...)))
// emit a direct loop: new T[count] with indexed Cast<T> assignmentsBenefits:
- Easier to read in generated output (important when debugging test failures)
- Removes the Enumerable dependency entirely rather than working around its surface syntax
- Avoids the IEnumerable intermediate allocation (minor, since ToArray follows immediately)
The current fix is correct for the issue at hand — this is a suggestion for a follow-up if generated-code ergonomics matter.
New Test — ParamsArrayCompilationTests.cs
This is exactly the right kind of regression test. It targets the blind spot identified in TestsBase: snapshot tests compile with injected global usings, so they cannot catch missing-using bugs in generated code. Compiling the generator output without System.Linq in scope correctly isolates this regression.
The approach is sound: output.GetDiagnostics() on the updated Compilation runs semantic analysis across all syntax trees including generated ones, so CS1061 surfaces here.
The captured generated variable in the failure message is a good diagnostics touch.
TestsBase.cs Comment
The warning explaining why snapshot tests won't catch this class of bug is a valuable addition. Future contributors will understand why ParamsArrayCompilationTests exists and why generated code must use fully-qualified calls.
Minor Notes
new string[0]/new int[0]in generated fallback code could useArray.Empty<string>()/Array.Empty<int>()to avoid per-call allocations, but this is cosmetic.- BOM removal in several snapshot files is a harmless bonus cleanup.
Verdict: Correct fix, well-scoped change, and the new compilation-level regression test addresses the specific gap in snapshot testing. The architectural note is optional follow-up, not a blocker.
Updated [TUnit.Core](https://github.com/thomhurst/TUnit) from 1.45.29 to 1.48.6. <details> <summary>Release notes</summary> _Sourced from [TUnit.Core's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.48.6 <!-- Release notes generated using configuration in .github/release.yml at v1.48.6 --> ## What's Changed ### Other Changes * fix(sourcegen): fully-qualify Linq calls in params array binding (#6140) by @thomhurst in thomhurst/TUnit#6141 ### Dependencies * chore(deps): update tunit to 1.48.0 by @thomhurst in thomhurst/TUnit#6135 * chore(deps): update dependency polyfill to 10.7.1 by @thomhurst in thomhurst/TUnit#6137 * chore(deps): update dependency polyfill to 10.7.1 by @thomhurst in thomhurst/TUnit#6138 * chore(deps): update verify to 31.19.0 by @thomhurst in thomhurst/TUnit#6139 **Full Changelog**: thomhurst/TUnit@v1.48.0...v1.48.6 ## 1.48.0 <!-- Release notes generated using configuration in .github/release.yml at v1.48.0 --> ## What's Changed ### Other Changes * feat(html-report): baked-in C# syntax highlighting on Source tab by @slang25 in thomhurst/TUnit#6132 * feat(analyzers): suppress VSTHRD200 on test and hook methods by @thomhurst in thomhurst/TUnit#6123 * fix(source-gen): correct source location for cross-project inherited tests by @slang25 in thomhurst/TUnit#6133 * feat(assertions): add WasCalled to tunit mocks assertions by @robertcoltheart in thomhurst/TUnit#6126 * feat(arguments): bind array values to a single array test parameter by @thomhurst in thomhurst/TUnit#6122 * fix: populate retry/flaky attempt history in HTML report (#6119) by @thomhurst in thomhurst/TUnit#6124 ### Dependencies * chore(deps): update tunit to 1.47.0 by @thomhurst in thomhurst/TUnit#6115 * chore(deps): update dependency microsoft.visualstudio.threading.analyzers to 17.14.15 by @thomhurst in thomhurst/TUnit#6134 **Full Changelog**: thomhurst/TUnit@v1.47.0...v1.48.0 ## 1.47.0 <!-- Release notes generated using configuration in .github/release.yml at v1.47.0 --> ## What's Changed ### Other Changes * perf(engine): hoist GetParameters and dict-dedup AfterTestDiscovery hooks by @thomhurst in thomhurst/TUnit#6062 * perf(engine): hoist GetParameters and drop LINQ in reflection discovery by @thomhurst in thomhurst/TUnit#6063 * perf(engine): cache treenode filter path on TestMetadata by @thomhurst in thomhurst/TUnit#6064 * perf: use is T pattern in ReflectionExtensions.HasAttribute fallback (#6060) by @thomhurst in thomhurst/TUnit#6066 * perf: replace OrderBy().ToArray() with Array.Sort in ConstraintKeyScheduler by @thomhurst in thomhurst/TUnit#6067 * perf: pool HashSet in WaitingTestIndex.GetCandidatesForReleasedKeys by @thomhurst in thomhurst/TUnit#6069 * perf: collapse OfType chains in JUnitXmlWriter (#6052) by @thomhurst in thomhurst/TUnit#6070 * perf(engine): avoid closure allocation in AfterHookPairTracker.GetOrCreateAfterAssemblyTask (#6041) by @thomhurst in thomhurst/TUnit#6071 * perf: avoid closure allocation in BeforeHookTaskCache.GetOrCreateBeforeAssemblyTask (#6040) by @thomhurst in thomhurst/TUnit#6073 * perf: use TryAdd in TestDependencyResolver dependency dedupe by @thomhurst in thomhurst/TUnit#6068 * perf: replace LINQ Any with foreach in TestGenericTypeResolver (#6044) by @thomhurst in thomhurst/TUnit#6072 * perf: avoid Cast<object>().FirstOrDefault() iterator alloc in CastHelper (#6029) by @thomhurst in thomhurst/TUnit#6074 * perf(engine): avoid string round-trip when building nested type names (#6049) by @thomhurst in thomhurst/TUnit#6075 * perf(engine): replace Select+ToArray with manual Type[] build (#6043) by @thomhurst in thomhurst/TUnit#6076 * perf(core): replace OfType().FirstOrDefault()/.Any() with foreach in ClassConstructorHelper by @thomhurst in thomhurst/TUnit#6078 * perf(engine): avoid FirstOrDefault iterator alloc in TestGenericTypeResolver by @thomhurst in thomhurst/TUnit#6079 * perf(engine): use SearchValues<char> for reporter filename sanitization by @thomhurst in thomhurst/TUnit#6090 * perf: dedupe TestDataFormatter.FormatArguments with pooled StringBuilder by @thomhurst in thomhurst/TUnit#6088 * perf(engine): use MemoryExtensions.Split for path parsing in MetadataFilterMatcher by @thomhurst in thomhurst/TUnit#6085 * perf(engine): use CollectionsMarshal.GetValueRefOrAddDefault for dictionary index builds by @thomhurst in thomhurst/TUnit#6086 * perf(engine): replace LINQ Where closure with inline filter in MetadataDependencyExpander BFS by @thomhurst in thomhurst/TUnit#6084 * perf(engine): pool StringBuilder in DisplayNameBuilder.FormatArguments by @thomhurst in thomhurst/TUnit#6082 * Preserve specialized chaining after null assertions by @thomhurst in thomhurst/TUnit#6008 * perf: use EnumerateLines for line splitting in HtmlReportGenerator by @thomhurst in thomhurst/TUnit#6089 * perf: collapse Replace chain in TestNameFormatter.BuildTestId by @thomhurst in thomhurst/TUnit#6083 * perf: use OrdinalIgnoreCase Contains in HtmlReportGenerator span mapping by @thomhurst in thomhurst/TUnit#6093 * perf(assertions): avoid eager interpolated-string alloc in assertion source ctors by @thomhurst in thomhurst/TUnit#6091 * perf: optimize TestNameFormatter argument and bool formatting by @thomhurst in thomhurst/TUnit#6095 * perf: use FrozenSet/FrozenDictionary for read-only static lookups by @thomhurst in thomhurst/TUnit#6099 * perf: avoid GetCustomAttributes() + LINQ chain for per-property attribute scans by @thomhurst in thomhurst/TUnit#6098 * perf(engine): replace magic-string RequiredAttribute match with type check in ConstructorHelper by @thomhurst in thomhurst/TUnit#6087 * perf(core): replace Select+Func factory chain in DataSourceHelpers by @thomhurst in thomhurst/TUnit#6081 * perf: replace LINQ dependency extraction with manual loop by @thomhurst in thomhurst/TUnit#6096 * perf(core): avoid string[] alloc in ArgumentFormatter.FormatArguments by @thomhurst in thomhurst/TUnit#6080 * perf: use [GeneratedRegex] in MetadataFilterMatcher by @thomhurst in thomhurst/TUnit#6094 * perf: dedupe GetSimpleTypeName into shared TypeNameFormatter by @thomhurst in thomhurst/TUnit#6097 * fix: remove GitVersion MSBuild task, pin local builds to 99.99.99 (#6077) by @thomhurst in thomhurst/TUnit#6101 * HTML Report: source link + code snippet on Source tab (#5993) by @thomhurst in thomhurst/TUnit#6100 * perf(sourcegen): Single-pass attribute classification by @thomhurst in thomhurst/TUnit#6111 * perf(core): eliminate per-test allocations in TestDetails/HookMethod by @thomhurst in thomhurst/TUnit#6109 * perf: hoist char[] alloc in FsCheckPropertyTestExecutor to static SearchValues by @thomhurst in thomhurst/TUnit#6108 * perf(core): de-LINQ data-source expansion by @thomhurst in thomhurst/TUnit#6110 * perf: avoid LINQ chains in TestDependency equality and MethodDataSourceAttribute method matching by @thomhurst in thomhurst/TUnit#6092 * perf(engine): reduce allocations in reflection-mode discovery/execution by @thomhurst in thomhurst/TUnit#6113 * perf(assertions): allocation-free passing path (TUnit.Assertions) by @thomhurst in thomhurst/TUnit#6112 ### Dependencies ... (truncated) ## 1.46.0 <!-- Release notes generated using configuration in .github/release.yml at v1.46.0 --> ## What's Changed ### Other Changes * docs: add Rider VSTest conflict troubleshooting by @smolchanovsky in thomhurst/TUnit#5989 * Populate generated test metadata with full source spans by @Copilot in thomhurst/TUnit#5991 * Add devcontainer configuration by @Copilot in thomhurst/TUnit#5995 * fix: treenode filter pre-filter rejects parenthesised segments (#6026) by @thomhurst in thomhurst/TUnit#6027 * fix(engine): isolate per-session state under MTP server-mode concurrency (#6001) by @thomhurst in thomhurst/TUnit#6025 ### Dependencies * chore(deps): update dependency stackexchange.redis to 2.13.10 by @thomhurst in thomhurst/TUnit#5985 * chore(deps): update tunit to 1.45.29 by @thomhurst in thomhurst/TUnit#5986 * chore(deps): update dependency mockolate to 3.2.1 by @thomhurst in thomhurst/TUnit#5987 * chore(deps): update dependency microsoft.playwright to 1.60.0 by @thomhurst in thomhurst/TUnit#5988 * chore(deps): update dependency messagepack to 3.1.6 by @thomhurst in thomhurst/TUnit#5992 * chore(deps): update dependency polyfill to 10.7.0 by @thomhurst in thomhurst/TUnit#5998 * chore(deps): update dependency polyfill to 10.7.0 by @thomhurst in thomhurst/TUnit#5997 * chore(deps): update verify to 31.17.0 by @thomhurst in thomhurst/TUnit#6000 * chore(deps): update verify to 31.18.0 by @thomhurst in thomhurst/TUnit#6013 * chore(deps): update dependency microsoft.net.test.sdk to 18.6.0 by @thomhurst in thomhurst/TUnit#6016 * chore(deps): update dependency dompurify to v3.4.6 by @thomhurst in thomhurst/TUnit#6015 * chore(deps): update dependency dompurify to v3.4.7 by @thomhurst in thomhurst/TUnit#6019 * chore(deps): update dependency npgsql to 10.0.3 by @thomhurst in thomhurst/TUnit#6020 * chore(deps): update dependency stackexchange.redis to 2.13.17 by @thomhurst in thomhurst/TUnit#6021 * chore(deps): update dependency npgsql.entityframeworkcore.postgresql to 10.0.2 by @thomhurst in thomhurst/TUnit#6022 ## New Contributors * @smolchanovsky made their first contribution in thomhurst/TUnit#5989 **Full Changelog**: thomhurst/TUnit@v1.45.29...v1.46.0 Commits viewable in [compare view](thomhurst/TUnit@v1.45.29...v1.48.6). </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>
Fixes #6140
Problem
After #6122, the source generator's dynamic-count params/array argument binding emits:
SelectandToArrayare used here as extension methods, which requireusing System.Linqto be in scope. Generated.g.csfiles only getSystem.Linqwhen the consuming project hasImplicitUsingsenabled (via the auto-generated global usings). Projects with implicit usings disabled fail to compile:(This is why the reporter couldn't make a minimal repro — it depends on the project's
ImplicitUsingssetting, not the test itself.)Fix
TupleArgumentHelper.csnow emits the fully-qualified static form:No extension-method resolution, so it compiles regardless of the consuming project's usings.
Tests
Affected snapshots regenerated and committed (ArgsAsArrayTests, DataDrivenTests, MethodDataSourceDrivenTests, Tests2112, ConflictingNamespaceTests across all TFMs).