Skip to content

feat(lint): add enumerable loop removal#15542

Open
0xMars42 wants to merge 10 commits into
foundry-rs:masterfrom
0xMars42:feat/lint-enumerable-loop-removal
Open

feat(lint): add enumerable loop removal#15542
0xMars42 wants to merge 10 commits into
foundry-rs:masterfrom
0xMars42:feat/lint-enumerable-loop-removal

Conversation

@0xMars42

@0xMars42 0xMars42 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

This adds the remaining High-severity enumerable-loop-removal lint from #14381. It reports a semantically resolved EnumerableSet.remove when the same storage set is read with at at a bare ascending cadence and the loop body matches the conservative flow-free shape: the cadence has no unsupported writes and the body has no branches, jumps, reverts, inline assembly, try, or nested loops. Both method and library-qualified calls are recognized, while storage aliases, struct fields, literal mapping keys, and unknown operands are handled conservatively.

Review feedback tightened cadence invalidation across declarations, tuple assignments, resets, decrements, and deletes; restricted at to the bare cadence identifier; excluded every unmodeled exit shape; and documented descending, control-flow, expression-arm, and remove-argument precision limits. Permanent positive and negative fixtures cover each corrected case.

The user documentation is in foundry-rs/book#1987, with the review-aligned follow-up in foundry-rs/book#1992.

Prepared with assistance from OpenAI Codex.

Flags `remove` on an EnumerableSet inside a loop whose subtree also calls
`at` on an EnumerableSet. Removal is swap-and-pop, so removing while
iterating by index skips the swapped-in elements or reads out-of-bounds
indices. Neither half fires alone: `remove` without `at` is the
recommended collect-then-remove pattern, `at` without `remove` is a plain
read, and the same method names on a type that is not an EnumerableSet
are out of scope.

Part of foundry-rs#14381.
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/docs/enumerable-loop-removal.md
0xMars42 added 2 commits July 3, 2026 08:14
…calls through the type checker

Two review findings:

- tracking any `at` on any EnumerableSet flagged safe patterns: a drain
  reading at the literal index 0, and a removal from a set distinct
  from the iterated one. `remove` and `at` calls now carry the identity
  of their set (the variable it is stored in) and only the same-set
  combination reports; `at` at a literal index does not count as
  iteration, and an operand too complex to name a single variable
  conservatively matches either set.
- only the `using for` method form was recognized: the library-qualified
  `EnumerableSet.remove(set, EnumerableSet.at(set, i))` has the same
  swap-and-pop bug but kept the library as receiver. Calls are now
  resolved through the type checker (gcx.type_of_expr on the callee)
  and matched on functions declared in a library named EnumerableSet,
  which also covers import aliases and keeps same-name functions of
  user libraries attached to the set type out.
mattsse
mattsse previously approved these changes Jul 8, 2026

@mattsse mattsse left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No actionable findings from my pass.

Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
…oop-removal

A removal was reported as soon as a loop held an `at` on any set. Four judgements
now stand between the two calls and the diagnostic, each with a fixture on both
sides of its boundary.

Ownership: an `at` follows the loop whose cadence its index paces, wherever it is
written. A loop's cadence is a variable it advances from its old value (`i++`,
`i += n`, `i = i + 1`) and names in a termination condition or a direct step; a
reset each turn does not progress and is no cadence. A nested loop's own cursor,
advanced and tested inside it, stays the nested loop's even when it is a function
parameter or a hoisted local. A loop whose condition hides its counter behind an
opaque call, the counter stepped only in a nested loop, is beyond a syntactic pass
and stays a documented limit.

Continuation: a removal the loop never comes back from corrupts nothing. `break`,
`return`, a revert and a `try` whose every clause leaves end the iteration;
`continue`, a `break` ending only a nested loop, and a `catch` that falls through
do not.

Identity: two operands name the same set when they name the same storage path, a
base variable then struct fields and literal mapping keys. A `storage` reference
resolves to what it was bound to, and to nothing readable once rebound.

Dispatch: named arguments are matched to parameters by name, and the method form
shifts the argument positions by one. A fixed index (a literal, a cast of one, a
`constant`) is a drain and not iteration.

The documented parity with Aderyn is corrected. Aderyn filters `remove` on a type
string containing `EnumerableSet` and matches any `at` by name under the closest
ancestor of each loop kind, so this detector is narrower on what earns a report
and wider on which loop the two calls may belong to.
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
…in enumerable-loop-removal

An at read walks a loop only when its index names the loop's own moving
cadence or a straight-line copy of it, a cadence only stepped downward
drains without skipping, a removal deciding an exiting branch mutates
only on the path that leaves, try clauses carry their own trailing exit
state, and local storage references resolve to their last straight-line
binding where the loop runs, a reference bound no way being read as any
set. Helpers handed the same set through two storage parameters are a
documented interprocedural limit.
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
@mattsse mattsse changed the title feat(lint): add enumerable-loop-removal detector feat(lint): add enumerable loop removal Jul 15, 2026
@mattsse mattsse requested a review from mablr July 15, 2026 18:39
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
The prior rounds grew a cadence-direction analysis, a copy propagation, and
an exit-and-branch exemption, each layer trading a false positive for a false
negative the next review found. Replace them with a single rule the detector
can judge without a flow analysis: an unconditional ascending cadence
(`i++`, `i += 1`, `i = i + 1`), an `at` read at that cadence, a `remove` on the
same set, and a straight-line body (no `if`, `try`, `break`, `continue`,
`return`, or nested loop).

Anything a control-flow construct could change is left clean rather than
guessed at, which removes the reported false positives (swap-and-pop filters,
short-circuit exits, killed copies, no-op steps, nested-then-exit removals) at
the cost of not reporting the genuine corruptions those same constructs hide.
Those become documented limitations; the canonical ascending remove-as-you-go
is still reported, and the safe swap-and-pop and drain-and-exit forms are no
longer warned on. Over-reporting on a statically-unreadable set operand is
unchanged from the earlier rounds.

The type-checker call resolution and the storage-path set identity from the
earlier rounds are kept. The synthetic condition guard the lowering wraps every
loop in is peeled so the guard's `break` and the `for`'s next-step are read for
what they are.
@0xMars42 0xMars42 force-pushed the feat/lint-enumerable-loop-removal branch from 4f18aea to a611819 Compare July 16, 2026 13:34

@0xMars42 0xMars42 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thorough passes. Each round I added a layer (cadence direction, copy propagation, an exit-and-branch exemption) to keep a report while dodging the last false positive, and each layer grew the next false negative you then found. Rather than add a fourth, I reworked the detector around a single rule it can judge without a flow analysis, pushed in 9db3decca.

Reported now: an unconditional ascending cadence (i++, i += 1, i = i + 1), an at read at that cadence, a remove on the same set, and a straight-line body (no if, try, break, continue, return, or nested loop). The type-checker call resolution and the storage-path set identity from the earlier rounds are kept. The synthetic condition guard the lowering wraps every loop in is peeled so its break and the for's next-step read for what they are.

This drops every false positive you raised, by construction:

  • swap-and-pop filter (i++ only in the no-removal branch): the cadence is under a branch, so it is not an unconditional ascending cadence.
  • short-circuit exit (remove(t) && flag then break) and negated exit (!remove(v) then break): the body has a break.
  • no-op step (i += 0): not a positive step, so no ascending cadence.
  • killed copy (idx = i reassigned before the read): copy propagation is gone; the index must be the cadence variable.
  • nested removal then outer exit: a nested loop is not straight-line.

The false negatives you flagged become documented limitations, in the docs and the fixture: the descending drain of a different value, an eventual exit reading a shifted slot, composite index arithmetic, and a member/opaque cursor are all cases where telling the corruption from a safe lookalike needs the flow analysis this pass does not run, so it stays silent instead of guessing. The canonical ascending remove-as-you-go is still reported.

This is the conservative-but-precise version on the control-flow axis: it never warns on the safe swap-and-pop and drain-and-exit forms it can read from the loop's structure, and in exchange it misses the corruptions that hide behind control flow. It can still over-report on the set-identity axis, unchanged from the earlier rounds: an operand it cannot read statically (a varying mapping key, a conditionally-rebound alias) is matched against every set, reporting a possibly-safe removal rather than missing an unsafe one.

If you would rather have the control-flow corruptions caught and accept the flow analysis it takes (reaching definitions for the cadence, correlating the mutating path with the exiting branch, polarity, short-circuit), I am happy to build that out instead. Your call on which trade you want.

Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/docs/enumerable-loop-removal.md Outdated
mattsse added 2 commits July 16, 2026 17:42
Require every write to a cadence to be a supported ascending step and only accept a bare cadence identifier as the EnumerableSet at index. Treat builtin and custom-error reverts as non-fallthrough loop bodies.

Document descending traversal as an unreported limit and describe the detector's conservative recall and aliasing tradeoffs.
Treat declaration initializers, delete expressions, and tuple lvalues as cadence writes, and reject inline assembly from straight-line loop bodies. Document the lint's syntactic matching and value-analysis limits.
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs
Ignore set operations in literal-proven dead expression arms and accept commuted ascending loop cadences. Keep the public lint documentation and UI fixtures aligned with both behaviors.
@mattsse mattsse requested a review from mablr July 16, 2026 20:36
Comment thread crates/lint/src/sol/high/enumerable_loop_removal.rs Outdated
Reuse shared terminal-flow analysis so require(false), assert(false), and parenthesized exit calls prevent impossible next-iteration warnings. Keep warning controls for nonterminal builtin calls.
@mattsse mattsse requested a review from mablr July 16, 2026 23:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants