Skip to main content

forge_lint/sol/gas/
write_after_write.rs

1use super::WriteAfterWrite;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint, analysis::loop_update},
5};
6use solar::{
7    interface::Span,
8    sema::{
9        Gcx,
10        hir::{
11            BinOpKind, Block, CallArgs, CallOptions, Expr, ExprKind, Function, Stmt, StmtKind,
12            VariableId,
13        },
14    },
15};
16use std::collections::HashMap;
17
18declare_forge_lint!(
19    WRITE_AFTER_WRITE,
20    Severity::Gas,
21    "write-after-write",
22    "redundant storage write; value overwritten before being read"
23);
24
25impl<'gcx> LateLintPass<'gcx> for WriteAfterWrite {
26    fn check_function(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, func: &'gcx Function<'gcx>) {
27        if let Some(body) = func.body {
28            Analyzer { ctx, gcx, pending: HashMap::new() }.check_block(body);
29        }
30    }
31}
32
33/// Tracks state variable writes that no later read has observed yet; a second write to such a
34/// variable makes the pending one redundant.
35struct Analyzer<'a, 'gcx> {
36    ctx: &'a LintContext<'a, 'a>,
37    gcx: Gcx<'gcx>,
38    pending: HashMap<VariableId, Span>,
39}
40
41impl Analyzer<'_, '_> {
42    /// Returns whether control flow continues past the block.
43    fn check_block(&mut self, block: Block<'_>) -> bool {
44        block.stmts.iter().all(|stmt| self.check_stmt(stmt))
45    }
46
47    /// Returns whether control flow continues past the statement.
48    fn check_stmt(&mut self, stmt: &Stmt<'_>) -> bool {
49        match &stmt.kind {
50            StmtKind::Expr(expr) => self.process_expr(expr),
51            StmtKind::DeclSingle(var_id) => {
52                if let Some(init) = self.gcx.hir.variable(*var_id).initializer {
53                    self.reads(init);
54                }
55            }
56            StmtKind::DeclMulti(_, expr) => self.reads(expr),
57            // `emit` only logs, so unlike a call it cannot observe pending writes.
58            StmtKind::Emit(expr) => match &expr.peel_parens().kind {
59                ExprKind::Call(callee, args, opts) => self.read_call_parts(callee, args, *opts),
60                _ => self.reads(expr),
61            },
62            // Terminal statements: the code after them is unreachable and can never overwrite
63            // the pending writes.
64            StmtKind::Return(expr) => {
65                if let Some(expr) = expr {
66                    self.reads(expr);
67                }
68                self.pending.clear();
69                return false;
70            }
71            StmtKind::Revert(expr) => {
72                self.reads(expr);
73                self.pending.clear();
74                return false;
75            }
76            StmtKind::Break | StmtKind::Continue => {
77                self.pending.clear();
78                return false;
79            }
80            // Branches are analyzed in isolation so intra-branch pairs are still caught, while
81            // outer pending writes are dropped since any branch may observe or skip them.
82            StmtKind::If(cond, then_stmt, else_stmt) => {
83                self.reads(cond);
84                let then_continues = self.isolated(|this| this.check_stmt(then_stmt));
85                if let Some(else_stmt) = else_stmt {
86                    let else_continues = self.isolated(|this| this.check_stmt(else_stmt));
87                    return then_continues || else_continues;
88                }
89            }
90            // A loop may run zero times, so it never stops the outer flow.
91            StmtKind::Loop(block, source) => {
92                self.isolated(|this| {
93                    this.check_block(*block)
94                        && loop_update(*source).is_none_or(|update| this.check_stmt(update))
95                });
96            }
97            StmtKind::Try(try_stmt) => {
98                self.reads(&try_stmt.expr);
99                for clause in try_stmt.clauses {
100                    self.isolated(|this| this.check_block(clause.block));
101                }
102            }
103            // Nested blocks are sequential: they share pending writes and propagate terminal flow.
104            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
105                return self.check_block(*block);
106            }
107            // The placeholder runs the modified function; inline assembly and errors are opaque.
108            StmtKind::Placeholder
109            | StmtKind::AssemblyBlock(_)
110            | StmtKind::Switch(_)
111            | StmtKind::Err(_) => self.pending.clear(),
112        }
113        true
114    }
115
116    /// Runs `f` on a fresh pending set and discards its result set afterwards.
117    fn isolated<T>(&mut self, f: impl FnOnce(&mut Self) -> T) -> T {
118        self.pending.clear();
119        let result = f(self);
120        self.pending.clear();
121        result
122    }
123
124    /// Tracks the writes and reads of an expression evaluated for its side effects.
125    fn process_expr(&mut self, expr: &Expr<'_>) {
126        let expr = expr.peel_parens();
127        match &expr.kind {
128            ExprKind::Assign(lhs, op, rhs) => {
129                // The RHS is evaluated before the assignment takes effect; a compound assignment
130                // also reads the current LHS value.
131                self.reads(rhs);
132                if op.is_none() {
133                    self.write_lhs(lhs, expr.span);
134                } else {
135                    self.reads(lhs);
136                }
137            }
138            // Pre/post increment and decrement read the variable, then write it.
139            ExprKind::Unary(op, inner) if op.kind.has_side_effects() => {
140                self.reads(inner);
141                if let Some(var) = self.state_var(inner) {
142                    self.pending.insert(var, expr.span);
143                }
144            }
145            // `delete x` is a pure write.
146            ExprKind::Delete(inner) => match self.state_var(inner) {
147                Some(var) => self.write(var, expr.span),
148                None => self.reads(inner),
149            },
150            // Any call may observe storage through re-entrancy or view semantics.
151            ExprKind::Call(callee, args, opts) => {
152                self.read_call_parts(callee, args, *opts);
153                self.pending.clear();
154            }
155            _ => self.reads(expr),
156        }
157    }
158
159    /// Records a plain `=` write; tuple destructuring records each component with its own span.
160    fn write_lhs(&mut self, lhs: &Expr<'_>, span: Span) {
161        match &lhs.peel_parens().kind {
162            ExprKind::Tuple(exprs) => {
163                exprs.iter().flatten().for_each(|e| self.write_lhs(e, e.span));
164            }
165            _ => match self.state_var(lhs) {
166                Some(var) => self.write(var, span),
167                // Index/member access: computing the slot reads the base.
168                None => self.reads(lhs),
169            },
170        }
171    }
172
173    fn write(&mut self, var: VariableId, span: Span) {
174        if let Some(prev_span) = self.pending.insert(var, span) {
175            self.ctx.emit(&WRITE_AFTER_WRITE, prev_span);
176        }
177    }
178
179    /// Removes every state variable read by `expr` from the pending writes. Nested writes are
180    /// tracked through [`Self::process_expr`].
181    fn reads(&mut self, expr: &Expr<'_>) {
182        let expr = expr.peel_parens();
183        match &expr.kind {
184            ExprKind::Ident(_) => {
185                if let Some(var) = self.gcx.resolved_variable(expr) {
186                    self.pending.remove(&var);
187                }
188            }
189            ExprKind::Assign(..) | ExprKind::Delete(_) => self.process_expr(expr),
190            // Short-circuit operands and ternary arms may not execute, so they are isolated to
191            // avoid false positives on the conditional path.
192            ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
193                self.reads(lhs);
194                self.isolated(|this| this.reads(rhs));
195            }
196            ExprKind::Ternary(cond, then_expr, else_expr) => {
197                self.reads(cond);
198                self.isolated(|this| this.reads(then_expr));
199                self.isolated(|this| this.reads(else_expr));
200            }
201            ExprKind::Call(callee, args, opts) => {
202                self.read_call_parts(callee, args, *opts);
203                self.pending.clear();
204            }
205            ExprKind::Binary(lhs, _, rhs) => {
206                self.reads(lhs);
207                self.reads(rhs);
208            }
209            ExprKind::Unary(_, inner) | ExprKind::Payable(inner) | ExprKind::Member(inner, _) => {
210                self.reads(inner);
211            }
212            ExprKind::Index(base, index) => {
213                self.reads(base);
214                if let Some(index) = index {
215                    self.reads(index);
216                }
217            }
218            ExprKind::Slice(base, start, end) => {
219                self.reads(base);
220                for expr in [*start, *end].into_iter().flatten() {
221                    self.reads(expr);
222                }
223            }
224            ExprKind::Tuple(exprs) => exprs.iter().flatten().for_each(|e| self.reads(e)),
225            ExprKind::Array(exprs) => exprs.iter().for_each(|e| self.reads(e)),
226            ExprKind::Lit(_) | ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => {}
227            ExprKind::YulMember(..) | ExprKind::Err(_) => self.pending.clear(),
228        }
229    }
230
231    /// Callee, arguments and call options are all evaluated before the call itself.
232    fn read_call_parts(
233        &mut self,
234        callee: &Expr<'_>,
235        args: &CallArgs<'_>,
236        opts: Option<&CallOptions<'_>>,
237    ) {
238        self.reads(callee);
239        for arg in args.exprs() {
240            self.reads(arg);
241        }
242        for opt in opts.into_iter().flat_map(|opts| opts.args) {
243            self.reads(&opt.value);
244        }
245    }
246
247    /// The state variable a bare identifier refers to.
248    fn state_var(&self, expr: &Expr<'_>) -> Option<VariableId> {
249        self.gcx
250            .resolved_variable(expr)
251            .filter(|&var| self.gcx.hir.variable(var).is_state_variable())
252    }
253}