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