Skip to content

Avoid some redundant list allocations#4963

Merged
thomhurst merged 13 commits intothomhurst:mainfrom
SimonCropp:avoid-some-redundant-list-allocations
Mar 8, 2026
Merged

Avoid some redundant list allocations#4963
thomhurst merged 13 commits intothomhurst:mainfrom
SimonCropp:avoid-some-redundant-list-allocations

Conversation

@SimonCropp
Copy link
Contributor

No description provided.

@claude
Copy link
Contributor

claude bot commented Feb 20, 2026

Code Review

Overall this is a clean, well-targeted performance improvement. All 18 files follow the same pattern consistently, every downstream consumer correctly accepts IEnumerable<T>, and no correctness issues were introduced.

Checked for: bugs (including multiple enumeration, deferred execution side effects, thread safety), CLAUDE.md compliance (Performance First, AOT compatibility, dual-mode requirements, snapshot testing rules).


Minor Observation: Double Enumeration in AspireFixture.cs

One removal stands out as slightly counter to the PR's intent. In TUnit.Aspire/AspireFixture.cs, the pending variable is enumerated twice after the .ToList() was removed:

var pending = resourceNames.Where(n => !readySet.Contains(n)); // now lazy

// First enumeration:
logger.LogInformation($"Waiting for {string.Join(", ", pending.Select(n => $"'{n}'"))} ...");

// Second enumeration:
foreach (var name in pending) { ... }

The original .ToList() here was actually intentional — it materializes the filtered set once and re-uses it twice. Without it, the Where predicate runs twice over resourceNames. While this is not a correctness bug (the state doesn't change between the two enumerations, and it's in a timeout/polling code path), it does introduce redundant work rather than eliminating it — the opposite of what the PR aims to do.

A simple fix would be to keep the .ToList() on this specific line, or restructure to iterate pending just once.


All other changes are genuinely beneficial and correct. Nice cleanup!

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

Follow-up Review

The concern raised in my previous review (double enumeration of pending in AspireFixture.cs) has been addressed — that file is no longer in the PR diff. Good.

Current State

All 17 remaining changes are correct. Every .ToList() / .ToArray() that was removed is consumed exactly once downstream, so deferred evaluation introduces no multiple-enumeration risk. Specific checks:

  • MigrationAnalyzer.cs / XUnitTwoPhaseAnalyzer.cs_originalRoot / OriginalRoot is treated as read-only throughout each method; the currentRoot variable is what evolves. No aliasing issue.
  • DisposableFieldPropertyAnalyzer.cs — each syntaxNodes is consumed in exactly one foreach. Removing .ToArray() is safe even though GetSyntax() is slightly expensive; re-execution is never triggered.
  • SingleTUnitAttributeAnalyzer.csGroupBy internalises the source immediately, so the upstream singleAttributes being lazy is inconsequential.
  • SyntaxFactory.List(convertedStatements) — accepts IEnumerable<T> and enumerates once internally.

One Subtle Note (not a blocker)

In CodeGenerationHelpers.cs, the elements select lambda has side effects:

var elements = typedConstant.Values.Select((v, i) =>
{
    syntaxIndex++;   // side effect
    elementIndex++;  // side effect
    return TypedConstantParser.GetRawTypedConstantValue(v, paramType);
}); // was .ToList(), now lazy
argStrings.AddRange(elements);

This is safe today because List.AddRange enumerates exactly once. However, the original .ToList() here was doing double duty — eliminating allocation and signalling "evaluate now because we have stateful side effects". The lazy version is slightly more fragile if someone later adds a second use of elements. A comment, or keeping this single .ToList(), would make the intent clearer. That said, it is not a correctness issue in the current code.

Summary

All changes are beneficial and correct. The regression identified in the first review was fixed. This is a clean, minimal cleanup that delivers real (if small) allocation savings across the analyzer and code-fixer hot paths.

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

Follow-up Review

The concern raised in my previous review (double enumeration of pending in AspireFixture.cs) has been addressed — that file is no longer in the PR diff. Good.

Current State

All 17 remaining changes are correct. Every .ToList() / .ToArray() that was removed is consumed exactly once downstream, so deferred evaluation introduces no multiple-enumeration risk. Specific checks:

  • MigrationAnalyzer.cs / XUnitTwoPhaseAnalyzer.cs_originalRoot / OriginalRoot is treated as read-only throughout each method; the currentRoot variable is what evolves. No aliasing issue.
  • DisposableFieldPropertyAnalyzer.cs — each syntaxNodes is consumed in exactly one foreach. Removing .ToArray() is safe even though GetSyntax() is slightly expensive; re-execution is never triggered.
  • SingleTUnitAttributeAnalyzer.csGroupBy internalises the source immediately, so the upstream singleAttributes being lazy is inconsequential.
  • SyntaxFactory.List(convertedStatements) — accepts IEnumerable<T> and enumerates once internally.

One Subtle Note (not a blocker)

In CodeGenerationHelpers.cs, the elements select lambda has side effects:

var elements = typedConstant.Values.Select((v, i) =>
{
    syntaxIndex++;   // side effect
    elementIndex++;  // side effect
    return TypedConstantParser.GetRawTypedConstantValue(v, paramType);
}); // was .ToList(), now lazy
argStrings.AddRange(elements);

This is safe today because List.AddRange enumerates exactly once. However, the original .ToList() here was doing double duty — eliminating allocation and signalling "evaluate now because we have stateful side effects". The lazy version is slightly more fragile if someone later adds a second use of elements. A comment, or keeping this single .ToList(), would make the intent clearer. That said, it is not a correctness issue in the current code.

Summary

All changes are beneficial and correct. The regression identified in the first review was fixed. This is a clean, minimal cleanup that delivers real (if small) allocation savings across the analyzer and code-fixer hot paths.

The .ToList() removal at BaseMigrationCodeFixProvider.cs was incorrect
because the result is subsequently used with .Add(), which requires a List<T>.
Also merges latest main to resolve CI failures.
intellitect-bot pushed a commit to IntelliTect/EssentialCSharp.Web that referenced this pull request Mar 9, 2026
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.18.37 to
1.19.16.

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

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

## 1.19.16

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

## What's Changed
### Other Changes
* Truncate exceptions in GitHub summary tables by @​thomhurst in
thomhurst/TUnit#5108
### Dependencies
* chore(deps): update tunit to 1.19.11 by @​thomhurst in
thomhurst/TUnit#5106
* chore(deps): bump dompurify from 3.3.0 to 3.3.2 in /docs by
@​dependabot[bot] in thomhurst/TUnit#5096
* chore(deps): bump svgo from 3.2.0 to 3.3.3 in /docs by
@​dependabot[bot] in thomhurst/TUnit#5086


**Full Changelog**:
thomhurst/TUnit@v1.19.11...v1.19.16

## 1.19.11

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

## What's Changed
### Other Changes
* Fix HTML report sort to also reorder groups by @​thomhurst in
thomhurst/TUnit#5103
* fix: correct expand-all icon SVG in HTML report by @​slang25 in
thomhurst/TUnit#5105
* Avoid some redundant list allocations by @​SimonCropp in
thomhurst/TUnit#4963
* avoid some redundant enumerable alloc by @​SimonCropp in
thomhurst/TUnit#4972
* use char instead of string for joins by @​SimonCropp in
thomhurst/TUnit#4989
### Dependencies
* chore(deps): update dependency nunit to 4.5.1 by @​thomhurst in
thomhurst/TUnit#5097
* chore(deps): update tunit to 1.19.0 by @​thomhurst in
thomhurst/TUnit#5099
* chore(deps): update dependency humanizer to 3.0.10 by @​thomhurst in
thomhurst/TUnit#5101
* chore(deps): update dependency nunit.analyzers to 4.12.0 by
@​thomhurst in thomhurst/TUnit#5102


**Full Changelog**:
thomhurst/TUnit@v1.19.0...v1.19.11

## 1.19.0

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

## What's Changed
### Other Changes
* fix: improve CreateTestVariant API and fix void/ValueTask return types
by @​thomhurst in thomhurst/TUnit#5095
### Dependencies
* chore(deps): update tunit to 1.18.37 by @​thomhurst in
thomhurst/TUnit#5094


**Full Changelog**:
thomhurst/TUnit@v1.18.37...v1.19.0

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

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=TUnit&package-manager=nuget&previous-version=1.18.37&new-version=1.19.16)](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>
This was referenced Mar 9, 2026
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.

2 participants