The pattern
CLAUDE.md's "Common Pitfalls" already names this for one function:
Closure Captures — collect_local_refs_expr() must handle all expression types — catch-all silently skips refs
PR #10650 (Fixes #10445) is the same defect in a different walker. replace_this_in_expr /
replace_this_in_stmts (crates/perry-hir/src/analysis.rs) had no arm for Expr::GetIterator,
GetAsyncIterator, MapEntries or SetValues — the wrappers a for…of iterable lowers to when it cannot be
proven a plain Array/Map/Set. A this.gen() inside one fell through the catch-all, stayed unreplaced, and
evaluated to undefined outside any method body. The fix was four added arms mirroring the existing
Await/TypeOf/Void ones.
The shared cause is structural, not a coding mistake: these walkers match on Expr with a catch-all _ =>
arm, so adding a new Expr variant compiles cleanly everywhere and silently does the wrong thing in every
walker that should have recursed into it. The compiler cannot help, and the symptom appears far from the cause
— undefined at runtime, in a different function, with no diagnostic.
Suggested work
- Audit the existing walkers. Find every hand-written
Expr/Stmt traversal with a catch-all and check
which recently-added variants it silently skips. Known instances: collect_local_refs_expr,
replace_this_in_expr/replace_this_in_stmts. There are likely more.
- Remove the catch-alls where the walker is meant to be total. An exhaustive match makes adding an
Expr
variant a compile error in exactly the places that must be updated — which is the outcome we want. Where a
walker genuinely only cares about a few variants, an explicit _ => {} with a comment saying so is fine;
the problem is the ones that are supposed to be exhaustive and aren't.
- Consider a test or lint that enumerates
Expr variants and asserts each total walker handles them, for
cases where an exhaustive match isn't practical.
This is hardening rather than a user-visible bug, but the class has now produced at least two real defects
(#10445 and whatever motivated the collect_local_refs_expr note), and each one presented as an unrelated
runtime undefined far from the actual omission.
The pattern
CLAUDE.md's "Common Pitfalls" already names this for one function:
PR #10650 (
Fixes #10445) is the same defect in a different walker.replace_this_in_expr/replace_this_in_stmts(crates/perry-hir/src/analysis.rs) had no arm forExpr::GetIterator,GetAsyncIterator,MapEntriesorSetValues— the wrappers afor…ofiterable lowers to when it cannot beproven a plain Array/Map/Set. A
this.gen()inside one fell through the catch-all, stayed unreplaced, andevaluated to
undefinedoutside any method body. The fix was four added arms mirroring the existingAwait/TypeOf/Voidones.The shared cause is structural, not a coding mistake: these walkers match on
Exprwith a catch-all_ =>arm, so adding a new
Exprvariant compiles cleanly everywhere and silently does the wrong thing in everywalker that should have recursed into it. The compiler cannot help, and the symptom appears far from the cause
—
undefinedat runtime, in a different function, with no diagnostic.Suggested work
Expr/Stmttraversal with a catch-all and checkwhich recently-added variants it silently skips. Known instances:
collect_local_refs_expr,replace_this_in_expr/replace_this_in_stmts. There are likely more.Exprvariant a compile error in exactly the places that must be updated — which is the outcome we want. Where a
walker genuinely only cares about a few variants, an explicit
_ => {}with a comment saying so is fine;the problem is the ones that are supposed to be exhaustive and aren't.
Exprvariants and asserts each total walker handles them, forcases where an exhaustive match isn't practical.
This is hardening rather than a user-visible bug, but the class has now produced at least two real defects
(#10445 and whatever motivated the
collect_local_refs_exprnote), and each one presented as an unrelatedruntime
undefinedfar from the actual omission.