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) => {
60                    let (callee, opts) = callee.split_call_options();
61                    self.read_call_parts(callee, args, opts);
62                }
63                _ => self.reads(expr),
64            },
65            // Terminal statements: the code after them is unreachable and can never overwrite
66            // the pending writes.
67            StmtKind::Return(expr) => {
68                if let Some(expr) = expr {
69                    self.reads(expr);
70                }
71                self.pending.clear();
72                return false;
73            }
74            StmtKind::Revert(expr) => {
75                self.reads(expr);
76                self.pending.clear();
77                return false;
78            }
79            StmtKind::Break | StmtKind::Continue => {
80                self.pending.clear();
81                return false;
82            }
83            // Branches are analyzed in isolation so intra-branch pairs are still caught, while
84            // outer pending writes are dropped since any branch may observe or skip them.
85            StmtKind::If(cond, then_stmt, else_stmt) => {
86                self.reads(cond);
87                let then_continues = self.isolated(|this| this.check_stmt(then_stmt));
88                if let Some(else_stmt) = else_stmt {
89                    let else_continues = self.isolated(|this| this.check_stmt(else_stmt));
90                    return then_continues || else_continues;
91                }
92            }
93            // A loop may run zero times, so it never stops the outer flow.
94            StmtKind::Loop(block, source) => {
95                self.isolated(|this| {
96                    this.check_block(*block)
97                        && loop_update(*source).is_none_or(|update| this.check_stmt(update))
98                });
99            }
100            StmtKind::Try(try_stmt) => {
101                self.reads(&try_stmt.expr);
102                for clause in try_stmt.clauses {
103                    self.isolated(|this| this.check_block(clause.block));
104                }
105            }
106            // Nested blocks are sequential: they share pending writes and propagate terminal flow.
107            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
108                return self.check_block(*block);
109            }
110            // The placeholder runs the modified function; inline assembly and errors are opaque.
111            StmtKind::Placeholder
112            | StmtKind::AssemblyBlock(_)
113            | StmtKind::Switch(_)
114            | StmtKind::Err(_) => self.pending.clear(),
115        }
116        true
117    }
118
119    /// Runs `f` on a fresh pending set and discards its result set afterwards.
120    fn isolated<T>(&mut self, f: impl FnOnce(&mut Self) -> T) -> T {
121        self.pending.clear();
122        let result = f(self);
123        self.pending.clear();
124        result
125    }
126
127    /// Tracks the writes and reads of an expression evaluated for its side effects.
128    fn process_expr(&mut self, expr: &Expr<'_>) {
129        let expr = expr.peel_parens();
130        match &expr.kind {
131            ExprKind::Assign(lhs, op, rhs) => {
132                // Compound assignments read the LHS before writing it.
133                self.reads(rhs);
134                if op.is_some() {
135                    self.reads(lhs);
136                    if let Some(var) = self.state_var(lhs) {
137                        self.pending.insert(var, expr.span);
138                    }
139                } else {
140                    self.write_lhs(lhs, expr.span);
141                }
142            }
143            // Pre/post increment and decrement read the variable, then write it.
144            ExprKind::Unary(op, inner) if op.kind.has_side_effects() => {
145                self.reads(inner);
146                if let Some(var) = self.state_var(inner) {
147                    self.pending.insert(var, expr.span);
148                }
149            }
150            // `delete x` is a pure write.
151            ExprKind::Delete(inner) => match self.state_var(inner) {
152                Some(var) => self.write(var, expr.span),
153                None => self.reads(inner),
154            },
155            // Any call may observe storage through re-entrancy or view semantics.
156            ExprKind::Call(callee, args) => {
157                let (callee, opts) = callee.split_call_options();
158                self.read_call_parts(callee, args, opts);
159                self.pending.clear();
160            }
161            _ => self.reads(expr),
162        }
163    }
164
165    /// Records assignment writes, preserving each tuple component's span.
166    fn write_lhs(&mut self, lhs: &Expr<'_>, span: Span) {
167        match &lhs.peel_parens().kind {
168            ExprKind::Tuple(exprs) => {
169                exprs.iter().flatten().for_each(|e| self.write_lhs(e, e.span));
170            }
171            _ => match self.state_var(lhs) {
172                Some(var) => self.write(var, span),
173                // Index/member access: computing the slot reads the base.
174                None => self.reads(lhs),
175            },
176        }
177    }
178
179    fn write(&mut self, var: VariableId, span: Span) {
180        if let Some(prev_span) = self.pending.insert(var, span) {
181            self.ctx.emit(&WRITE_AFTER_WRITE, prev_span);
182        }
183    }
184
185    /// Removes every state variable read by `expr` from the pending writes. Nested writes are
186    /// tracked through [`Self::process_expr`].
187    fn reads(&mut self, expr: &Expr<'_>) {
188        let expr = expr.peel_parens();
189        match &expr.kind {
190            ExprKind::Ident(_) => {
191                if let Some(var) = self.gcx.resolved_variable(expr) {
192                    self.pending.remove(&var);
193                }
194            }
195            ExprKind::Assign(..) | ExprKind::Delete(_) => self.process_expr(expr),
196            // Short-circuit operands and ternary arms may not execute, so they are isolated to
197            // avoid false positives on the conditional path.
198            ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
199                self.reads(lhs);
200                self.isolated(|this| this.reads(rhs));
201            }
202            ExprKind::Ternary(cond, then_expr, else_expr) => {
203                self.reads(cond);
204                self.isolated(|this| this.reads(then_expr));
205                self.isolated(|this| this.reads(else_expr));
206            }
207            ExprKind::Call(callee, args) => {
208                let (callee, opts) = callee.split_call_options();
209                self.read_call_parts(callee, args, opts);
210                self.pending.clear();
211            }
212            ExprKind::CallOptions(callee, opts) => {
213                self.reads(callee);
214                for opt in opts.args {
215                    self.reads(&opt.value);
216                }
217            }
218            ExprKind::Binary(lhs, _, rhs) => {
219                self.reads(lhs);
220                self.reads(rhs);
221            }
222            ExprKind::Member(inner, _) => {
223                if let Some(var) = self.state_var(expr) {
224                    self.pending.remove(&var);
225                }
226                self.reads(inner);
227            }
228            ExprKind::Unary(_, inner) | ExprKind::Payable(inner) => {
229                self.reads(inner);
230            }
231            ExprKind::Index(base, index) => {
232                self.reads(base);
233                if let Some(index) = index {
234                    self.reads(index);
235                }
236            }
237            ExprKind::Slice(base, start, end) => {
238                self.reads(base);
239                for expr in [*start, *end].into_iter().flatten() {
240                    self.reads(expr);
241                }
242            }
243            ExprKind::Tuple(exprs) => exprs.iter().flatten().for_each(|e| self.reads(e)),
244            ExprKind::Array(exprs) => exprs.iter().for_each(|e| self.reads(e)),
245            ExprKind::Lit(_) | ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => {}
246            ExprKind::YulMember(..) | ExprKind::Err(_) => self.pending.clear(),
247        }
248    }
249
250    /// Callee, arguments and call options are all evaluated before the call itself.
251    fn read_call_parts(
252        &mut self,
253        callee: &Expr<'_>,
254        args: &CallArgs<'_>,
255        opts: Option<&CallOptions<'_>>,
256    ) {
257        self.reads(callee);
258        for arg in args.exprs() {
259            self.reads(arg);
260        }
261        for opt in opts.into_iter().flat_map(|opts| opts.args) {
262            self.reads(&opt.value);
263        }
264    }
265
266    /// The state variable a bare identifier refers to.
267    fn state_var(&self, expr: &Expr<'_>) -> Option<VariableId> {
268        self.gcx
269            .resolved_variable(expr)
270            .filter(|&var| self.gcx.hir.variable(var).is_state_variable())
271    }
272}