Skip to main content

forge_lint/sol/med/
uninitialized_local.rs

1use super::UninitializedLocal;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    interface::{Span, data_structures::Never},
8    sema::{
9        Hir,
10        hir::{
11            Expr, ExprKind, Function, ItemId, LoopSource, Res, Stmt, StmtKind, TypeKind, VarKind,
12            VariableId, Visit,
13        },
14    },
15};
16use std::{
17    collections::{HashMap, HashSet},
18    ops::ControlFlow,
19};
20
21declare_forge_lint!(
22    UNINITIALIZED_LOCAL,
23    Severity::Med,
24    "uninitialized-local",
25    "local variable is read before being initialized"
26);
27
28impl<'hir> LateLintPass<'hir> for UninitializedLocal {
29    fn check_function(
30        &mut self,
31        ctx: &LintContext,
32        _gcx: solar::sema::Gcx<'hir>,
33        hir: &'hir Hir<'hir>,
34        func: &'hir Function<'hir>,
35    ) {
36        let Some(body) = func.body else { return };
37
38        let mut checker = Checker { hir, uninitialized: HashSet::new(), findings: HashMap::new() };
39        for stmt in body.stmts {
40            let _ = checker.visit_stmt(stmt);
41        }
42
43        for (_vid, read_span) in checker.findings {
44            ctx.emit(&UNINITIALIZED_LOCAL, read_span);
45        }
46    }
47}
48
49struct Checker<'hir> {
50    hir: &'hir Hir<'hir>,
51    /// Locals declared without an initializer that have not yet been written.
52    uninitialized: HashSet<VariableId>,
53    /// First read span per variable that was read while uninitialized.
54    findings: HashMap<VariableId, Span>,
55}
56
57impl<'hir> Visit<'hir> for Checker<'hir> {
58    type BreakValue = Never;
59
60    fn hir(&self) -> &'hir Hir<'hir> {
61        self.hir
62    }
63
64    fn visit_stmt(&mut self, stmt: &'hir Stmt<'hir>) -> ControlFlow<Self::BreakValue> {
65        match &stmt.kind {
66            StmtKind::DeclSingle(vid) => {
67                let v = self.hir.variable(*vid);
68                let is_value_type =
69                    matches!(v.ty.kind, TypeKind::Elementary(ty) if ty.is_value_type());
70                if matches!(v.kind, VarKind::Statement) && v.initializer.is_none() && is_value_type
71                {
72                    self.uninitialized.insert(*vid);
73                }
74                // Walk initializer (if any) to catch reads of other uninitialized vars.
75                if let Some(init) = v.initializer {
76                    let _ = self.visit_expr(init);
77                }
78                return ControlFlow::Continue(());
79            }
80
81            // For if/else: visit condition, then snapshot and walk each branch independently,
82            // then union the post-branch sets (conservative: still uninitialized if any path
83            // fails to write the variable).
84            StmtKind::If(cond, then, else_) => {
85                let _ = self.visit_expr(cond);
86
87                let before = self.uninitialized.clone();
88
89                let _ = self.visit_stmt(then);
90                let after_then = self.uninitialized.clone();
91
92                self.uninitialized = before;
93                if let Some(else_stmt) = else_ {
94                    let _ = self.visit_stmt(else_stmt);
95                }
96                let after_else = self.uninitialized.clone();
97
98                let then_exits = branch_always_exits(then);
99                let else_exits = else_.is_some_and(branch_always_exits);
100                self.uninitialized = match (then_exits, else_exits) {
101                    (true, _) => after_else,
102                    (_, true) => after_then,
103                    _ => after_then.union(&after_else).copied().collect(),
104                };
105                return ControlFlow::Continue(());
106            }
107
108            // do-while always executes the body once, so writes are guaranteed.
109            // for/while may execute zero times, so writes must be discarded.
110            StmtKind::Loop(block, source) => {
111                let before = self.uninitialized.clone();
112                for s in block.stmts {
113                    let _ = self.visit_stmt(s);
114                }
115                if !matches!(source, LoopSource::DoWhile) {
116                    self.uninitialized = before;
117                }
118                return ControlFlow::Continue(());
119            }
120
121            // Each try/catch clause is an independent execution path; treat like if/else branches.
122            StmtKind::Try(t) => {
123                let _ = self.visit_expr(&t.expr);
124                let mut clause_states: Vec<HashSet<VariableId>> = Vec::new();
125                for clause in t.clauses {
126                    let before = self.uninitialized.clone();
127                    for s in clause.block.stmts {
128                        let _ = self.visit_stmt(s);
129                    }
130                    clause_states.push(self.uninitialized.clone());
131                    self.uninitialized = before;
132                }
133                // Union across all clause post-states: variable stays uninitialized if any clause
134                // fails to write it.
135                self.uninitialized = clause_states
136                    .iter()
137                    .fold(HashSet::new(), |acc, s| acc.union(s).copied().collect());
138                return ControlFlow::Continue(());
139            }
140
141            _ => {}
142        }
143        self.walk_stmt(stmt)
144    }
145
146    fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
147        match &expr.kind {
148            // Plain `=`: visit RHS first (catches `x = x`), then mark LHS as written.
149            ExprKind::Assign(lhs, None, rhs) => {
150                let _ = self.visit_expr(rhs);
151                mark_written(lhs, &mut self.uninitialized);
152                // Still walk lhs in case it's a complex expression (e.g. array index).
153                let _ = self.visit_expr(lhs);
154                return ControlFlow::Continue(());
155            }
156
157            // Compound `op=`: both sides are read first, then lhs is written.
158            ExprKind::Assign(lhs, Some(_), rhs) => {
159                let _ = self.visit_expr(lhs);
160                let _ = self.visit_expr(rhs);
161                mark_written(lhs, &mut self.uninitialized);
162                return ControlFlow::Continue(());
163            }
164
165            // `delete x` is an explicit write to the zero value — not a read.
166            ExprKind::Delete(target) => {
167                mark_written(target, &mut self.uninitialized);
168                let _ = self.visit_expr(target);
169                return ControlFlow::Continue(());
170            }
171
172            ExprKind::Ident(reses) => {
173                for res in *reses {
174                    if let Res::Item(ItemId::Variable(vid)) = res
175                        && self.uninitialized.contains(vid)
176                    {
177                        self.findings.entry(*vid).or_insert(expr.span);
178                        break;
179                    }
180                }
181            }
182
183            _ => {}
184        }
185        self.walk_expr(expr)
186    }
187}
188
189fn branch_always_exits(stmt: &Stmt<'_>) -> bool {
190    match &stmt.kind {
191        StmtKind::Return(_) | StmtKind::Revert(_) => true,
192        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
193            block.stmts.last().is_some_and(branch_always_exits)
194        }
195        StmtKind::If(_, t, Some(e)) => branch_always_exits(t) && branch_always_exits(e),
196        _ => false,
197    }
198}
199
200/// Remove `expr` from `uninitialized` if it is a direct identifier or a tuple of identifiers.
201fn mark_written(expr: &Expr<'_>, uninitialized: &mut HashSet<VariableId>) {
202    match &expr.kind {
203        ExprKind::Ident(reses) => {
204            for res in *reses {
205                if let Res::Item(ItemId::Variable(vid)) = res {
206                    uninitialized.remove(vid);
207                }
208            }
209        }
210        ExprKind::Tuple(elems) => {
211            for elem in elems.iter().flatten() {
212                mark_written(elem, uninitialized);
213            }
214        }
215        _ => {}
216    }
217}