feat(lint): add write-after-write#14915
Conversation
Detects redundant consecutive storage writes to the same variable where the first value is overwritten before being read, wasting an SSTORE.
write-after-write
mattsse
left a comment
There was a problem hiding this comment.
Requesting changes for a few correctness gaps in write-after-write.
P2: Binary and Ternary are walked linearly here: write_after_write.rs#L204-L215. This can false-positive across &&/|| short-circuiting and ternary arms, and conflicts with the docs' “flat sequence” scope: book#L16-L20. Suggest treating conditional/short-circuit expression boundaries like branches: process condition reads, then clear or analyze each reachable arm with separate pending state.
P2: The lint only implements check_function: write_after_write.rs#L22-L33, so modifier bodies are not analyzed even though placeholder handling exists: write_after_write.rs#L114-L117. Suggest adding modifier-body traversal or removing/de-scoping placeholder logic and tests until modifiers are actually checked.
P3: Calls clear pending without first walking the callee/args: write_after_write.rs#L171-L174, write_after_write.rs#L216-L218. Solidity evaluates call arguments before the call, so nested overwrites in args are missed. Suggest walking callee/arguments first, then clearing pending before modeling the call itself.
- Fix tuple/destructuring LHS: add `process_assignment_lhs` helper that
recurses through tuple components, tracking each as a write rather
than falling through to `collect_reads` which treated them as reads
- Fix pre/post inc/dec: record the operator's write in pending after the
read so `++x; x = v` correctly flags the dead `++x` write
- Fix named call args: walk `{value:, gas:, salt:}` options via a shared
`walk_named_args` helper before clearing pending in both
`process_expr` and `collect_reads`
- Fix emit over-clearing: peel the call in `StmtKind::Emit` and walk
args directly without hitting the `ExprKind::Call` clear path
- Fix nested `delete` in `collect_reads`: delegate to `process_expr` so
the write is tracked correctly, matching how nested `Assign` is
handled
- Fix `ExprKind::Err` in `collect_reads`: clear pending for consistency
with `StmtKind::Err`
- Fix unreachable code false positives: break from `check_block` after
any terminal statement so writes in unreachable code are not analysed
- Update docs to reflect actual analysis scope (drops "flat sequence"
claim)
|
A few things worth addressing before merge, mostly around the data-flow precision in Real false positives from nested terminators
{ return; }
x = 1;
x = 2; // unreachable, but x = 1 will be flagged
if (c) return; else revert E();
x = 1;
x = 2; // unreachable on both paths, but x = 1 will be flagged
while (c) {
{ break; }
x = 1;
x = 2; // unreachable after break in this iteration
}Fix: thread a small Inline assembly handlingThe x = 1;
assembly { /* may read or overwrite x.slot */ }
x = 2; // currently x = 1 likely flagged — unsoundSafe behavior is Tuple diagnostic span is misleading(x, y) = (1, 2);
x = v;emits one warning pointing at the whole tuple statement, but only the Likely false negatives worth fixtures (or documenting)These are the conservative-clear trade-offs; mostly defensible, but should at least be visible as tests:
Minor soundness / style notes
Suggested additional fixtures// Currently FP (or will be after fix) — unreachable code should not flag
function nestedReturn() public {
{ return; }
x = 1;
x = 2;
}
function bothBranchesExit(bool c) public {
if (c) return; else revert();
x = 1;
x = 2;
}
function nestedBreak(bool c) public {
while (c) {
{ break; }
x = 1;
x = 2;
}
}
// Inline assembly — verify soundness
function asmReadsX() public {
x = 1;
assembly { let v := sload(0) }
x = 2;
}
// UX: span should point at the `x` component, not the whole tuple
function tuplePartial(uint256 v) public {
(x, y) = (1, 2);
x = v;
}
// Known FNs — document as limitations
function branchMiss(bool c, uint256 v) public {
x = 1;
if (c) { y = 2; }
x = v;
}
function shortCircuitMiss(bool a, bool b) public {
x = 1;
bool z = a && b;
x = 2;
}
function pureCallMiss() public {
x = 1;
abi.encode(uint256(0));
x = 2;
}Minimal fix suggestionThe three items above ( |
write-after-write
Thread a `Flow` enum through `check_stmt`/`check_block` so nested
terminators
(`{ return; }`, both-branch exits, nested `break`) propagate correctly
and
unreachable code is no longer flagged as a false positive.
Fix tuple LHS diagnostic spans to point at the individual component
(`x`) rather
than the whole tuple expression.
Document the inline-assembly false positive (assembly is not yet lowered
to solar's
HIR) and add known-false-negative fixtures for branch-miss,
short-circuit, and
pure-call cases so design intent is visible to future maintainers.
mablr
left a comment
There was a problem hiding this comment.
@grandizzy I fixed your findings in 06e3f72.
Description
Closes OSS-84
Add
write-after-writelint.Detects redundant consecutive storage writes to the same variable where the first value is overwritten before being read, wasting an SSTORE.
PR Checklist