Add NETH005 analyzer: redundant Span<T>.ToArray() at call sites#11464
Conversation
|
Claude finished @asdacap's task in 5m 41s —— View job PR Review: NETH004 Span.ToArray() Analyzer
SummaryThe analyzer logic is sound for the common case, but there are two Medium issues — one correctness bug (accessibility not checked) and one coding-style violation (LINQ) — plus a few lower-severity gaps worth calling out. Medium Findings1. Candidate overload accessibility is not checked — potential false positives
Example false-positive scenario: // In some external assembly or subclass
class Foo {
public void M(byte[] x) { }
private void M(Span<byte> x) { } // inaccessible from outside
}
// External call site — analyzer incorrectly flags this:
foo.M(span.ToArray()); // Suggests "pass span directly", but M(Span<byte>) is privateThe candidate lookup should filter using 2. LINQ usage violates coding-style.md
Two violations: Line 75 — // Current (allocates IEnumerable wrapper + IEnumerable state machine)
IEnumerable<IMethodSymbol> overloads = containingType.GetMembers(method.Name).OfType<IMethodSymbol>();
// Fix: pass ImmutableArray<ISymbol> directly and cast with `is` inside the loopLine 132 — overloads ??= candidateOverloads.ToArray(); // LINQ ToArray on IEnumerableFor invocations, the enumerable is a lazy LINQ chain (from Low Findings3. Self-delegation test only covers constructors — no test for regular method chainingThe test public Box(ReadOnlySpan<byte> s) : this(s.ToArray()) { }But the same pattern can appear in regular methods: void Foo(byte[] x) { /* impl */ }
void Foo(Span<byte> s) { Foo(s.ToArray()); } // should NOT be flaggedThe code paths are identical ( 4. Extension-method overloads are not detected (false negatives)
static class SpanExtensions {
public static void Write(this Stream s, Span<byte> buf) { }
}
// stream.Write(span.ToArray()) — not flagged, even though Write(Span<byte>) exists5. Test
|
| File | Change | Verification |
|---|---|---|
Address.cs:38 |
hash.Bytes.Slice(12, Size) → Address(ReadOnlySpan<byte>) |
Hash256.Bytes is Span<byte> — implicit → ROS ✓ |
Address.cs:40 |
hash.BytesAsSpan.Slice(12, Size) → Address(ReadOnlySpan<byte>) |
ValueHash256.BytesAsSpan is Span<byte> ✓ |
Address.cs:402 |
new(Bytes) in AddressStructRef.ToAddress() |
Bytes is ReadOnlySpan<byte> ✓ |
PublicKey.cs:51 |
Hash.Bytes[12..] |
Hash256.Bytes is Span<byte>, used inside lambda body (not captured) — valid C# 11+ ✓ |
PublicKey.cs:80 |
hash[12..] where hash: Span<byte> |
Address(ReadOnlySpan<byte>) via implicit conversion ✓ |
ICodeInfoRepository.cs |
code[DelegationHeader.Length..] |
Passes through Address(ReadOnlySpan<byte>) ✓ |
TraceStack.cs |
Peek(index)[12..] where Peek returns ReadOnlySpan<byte> |
✓ |
FlatTrieVerifier.cs |
flatKey.Bytes[..20] |
✓ |
ShutterEventSimulator.cs |
identityPreimage[32..] |
✓ |
TrieDiffWalkerTests.cs |
Keccak.Compute(...).Bytes[..20] |
Hash256.Bytes is Span<byte>, range returns new Span<byte> ✓ |
TransactionProcessorEip7702Tests.cs |
new Address(cell) where cell: ReadOnlySpan<byte> |
✓ |
All behavioral equivalence is maintained — the Address(ReadOnlySpan<byte>) constructor calls .ToArray() internally, so the heap copy still happens exactly once.
Verdict: Two Medium issues (accessibility check missing, LINQ violation) should be addressed before merge.
| if (method.ContainingType is not INamedTypeSymbol containingType) | ||
| return; | ||
|
|
||
| IEnumerable<IMethodSymbol> overloads = containingType.GetMembers(method.Name).OfType<IMethodSymbol>(); |
There was a problem hiding this comment.
LINQ violation (coding-style.md): OfType<IMethodSymbol>() produces a LINQ lazy chain from ImmutableArray<ISymbol>. Analyzers run on every keystroke in the IDE — this allocates an enumerator on every invocation that hits this path.
| IEnumerable<IMethodSymbol> overloads = containingType.GetMembers(method.Name).OfType<IMethodSymbol>(); | |
| ImmutableArray<ISymbol> overloadSymbols = containingType.GetMembers(method.Name); | |
| AnalyzeArguments(context, spanTypes, invocation.Arguments, method, overloadSymbols); |
Then change AnalyzeArguments / FindSpanOverloadKind to accept ImmutableArray<ISymbol> and use is IMethodSymbol m inline, eliminating the LINQ dependency entirely.
| string? bestMatch = null; | ||
|
|
||
| foreach (IMethodSymbol candidate in overloads) | ||
| { |
There was a problem hiding this comment.
Missing accessibility check — potential false positive. GetMembers() returns private/internal members regardless of whether they're accessible at the call site. If a Span<T> overload exists but is private, the diagnostic fires — but applying the fix would not compile.
Add a guard like:
if (!IsAccessibleAt(candidate, context, currentMethod))
continue;where IsAccessibleAt checks candidate.DeclaredAccessibility against the enclosing type of the call site (context.ContainingSymbol?.ContainingType), or use compilation.IsSymbolAccessibleWithin(candidate, fromWithin).
Address claude-review medium findings on PR #11464: 1. Use compilation.IsSymbolAccessibleWithin to skip overloads that are not visible from the call site — suggesting them would produce code that does not compile. 2. Replace LINQ (OfType/ToArray) with manual loops per coding-style.md. Adds two regression tests: inaccessible span overload (must not flag) and method-level self-delegation (must not flag). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed both medium findings from the review:
Also addressed Low #3 (added Low #4 (extension methods) and Low #5 (test reference assemblies) are noted as known limitations and intentionally deferred. Pushed in 34f2fe3. Tests: 14/14 passing; full solution build clean. — Claude Opus 4.7 (1M context) |
|
Claude finished @asdacap's task in 3m 27s —— View job PR Review: NETH004 Span.ToArray() Analyzer (Follow-up)
SummaryBoth Medium findings from the prior review have been addressed. The round-2 call-site cleanups are all correct. No new Medium or High issues found. Previous Medium Issues — Resolved ✓Medium #1: Accessibility checkFixed in New regression test Medium #2: LINQ removalFixed in Round-2 Call-site Cleanups — Correctness Verified ✓
Remaining Known Limitations (Low — carry-overs from prior review)Low #4 (unchanged): Extension method overloads are not detected ( Low #5 (unchanged): Tests use Low #6 (new): Inherited overloads are not detected. If a base class declares Verdict: All Critical, High, and Medium findings resolved. No new blockers. This is ready to merge. |
LukaszRozmej
left a comment
There was a problem hiding this comment.
Maybe resolve low issues?
|
|
||
| namespace Nethermind.Analyzers.Test; | ||
|
|
||
| public class SpanToArrayAtCallSiteAnalyzerTests |
Address claude-review medium findings on PR #11464: 1. Use compilation.IsSymbolAccessibleWithin to skip overloads that are not visible from the call site — suggesting them would produce code that does not compile. 2. Replace LINQ (OfType/ToArray) with manual loops per coding-style.md. Adds two regression tests: inaccessible span overload (must not flag) and method-level self-delegation (must not flag). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8b7c1e5 to
117de8c
Compare
|
@claude would any of those |
|
Claude finished @LukaszRozmej's task in 7m 6s —— View job PR Review: NETH005 Span.ToArray() Analyzer
Were any removed
|
Flags Span<T>.ToArray() / ReadOnlySpan<T>.ToArray() arguments when the target method or constructor has a Span<T>/ReadOnlySpan<T> overload at the same parameter position. Pass the span directly instead of allocating. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A constructor like `Foo(ReadOnlySpan<byte> s) : this(s.ToArray())` is the canonical pattern for sharing storage with the array-typed primary ctor. Reporting it would suggest a self-recursive call. Skip when the matched candidate equals the call site's containing symbol. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each site passes a (ReadOnly)Span<byte> to a method/constructor that already has a span overload, so the array allocation is wasted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ImplicitUsings already includes it; lint flagged IDE0005. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address claude-review medium findings on PR #11464: 1. Use compilation.IsSymbolAccessibleWithin to skip overloads that are not visible from the call site — suggesting them would produce code that does not compile. 2. Replace LINQ (OfType/ToArray) with manual loops per coding-style.md. Adds two regression tests: inaccessible span overload (must not flag) and method-level self-delegation (must not flag). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the analyzer only fired when the parameter was T[] and a span overload existed. It missed the case where the parameter is already Span<T>/ReadOnlySpan<T> and ToArray() materializes a byte[] that the compiler then implicitly converts back to a span — pure allocation waste with no overload involved. Now flags both shapes. Direction check still applies: a ReadOnlySpan<T> caller cannot fit a Span<T> parameter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Confirms the analyzer flags Span.ToArray() and ReadOnlySpan.ToArray() when the parameter is already ReadOnlySpan<T> (both byte[]-conversion paths) and does not flag the unfixable ROS-caller-into-Span-param case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 2 — these only surface in a clean rebuild because incremental builds skip projects whose sources didn't change. Sites: - AddressStructRef ctor: chain to span overload like the sibling ctor. - Rlp bloom decoder: pass the ROS<byte> directly to Bloom(ReadOnlySpan). - BitArrayTests: 16 HashUtility.Chunk(...).ToArray() inside Hash() calls — Hash already takes ReadOnlySpan<byte>. Outer .ToArray() at the TestCaseData boundary kept (object-typed param needs a heap copy). - RlpTests: ValueDecoderContext now takes the stackalloc-backed span directly; the context never escapes the test method. - ShutterCryptoTests: msg.SequenceEqual(decryptedMessage) dispatches to MemoryExtensions.SequenceEqual rather than LINQ's. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
117de8c to
2e13d40
Compare
Summary
Span<T>.ToArray()/ReadOnlySpan<T>.ToArray()arguments at call sites whose target method or constructor has an overload acceptingSpan<T>/ReadOnlySpan<T>at the same parameter position. Pass the span directly instead of allocating.Span -> ReadOnlySpanimplicit conversion direction. Skipsparams, element-type mismatches, and the canonicalFoo(ReadOnlySpan<T> s) : this(s.ToArray())self-delegation pattern.Changes
Nethermind.Analyzers/SpanToArrayAtCallSiteAnalyzer.cs— analyzer.Nethermind.Analyzers/AnalyzerReleases.Unshipped.md— NETH004 release tracking row.Nethermind.Analyzers.Test/SpanToArrayAtCallSiteAnalyzerTests.cs— 12 unit tests covering positives, negatives, constructors, generic methods, ROS->Span direction, params, element-type mismatch, and self-delegation.Test plan
dotnet test --project Nethermind.Analyzers.Test/Nethermind.Analyzers.Test.csproj— 55/55 passing.dotnet build -c release Nethermind.slnx) — 0 warnings, 0 errors.🤖 Generated with Claude Code