Skip to main content

forge_lint/sol/med/
uninitialized_local.rs

1use super::UninitializedLocal;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{
5        Severity, SolLint,
6        analysis::{branch_always_exits, for_each_lhs_var, loop_stmts},
7    },
8};
9use solar::{
10    ast::ElementaryType,
11    interface::{Span, data_structures::Never},
12    sema::{
13        Gcx, Hir,
14        hir::{
15            BinOpKind, Block, Expr, ExprKind, Function, LoopSource, Stmt, StmtKind, TypeKind,
16            UnOpKind, VarKind, VariableId, Visit,
17        },
18    },
19};
20use std::{
21    collections::{HashMap, HashSet},
22    ops::ControlFlow,
23};
24
25declare_forge_lint!(
26    UNINITIALIZED_LOCAL,
27    Severity::Med,
28    "uninitialized-local",
29    "local variable is read before being initialized"
30);
31
32impl<'gcx> LateLintPass<'gcx> for UninitializedLocal {
33    fn check_function(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, func: &'gcx Function<'gcx>) {
34        let Some(body) = func.body else { return };
35        let mut checker =
36            Checker { gcx, hir: &gcx.hir, uninitialized: HashSet::new(), findings: HashMap::new() };
37        for stmt in body.stmts {
38            let _ = checker.visit_stmt(stmt);
39        }
40        for span in checker.findings.into_values() {
41            ctx.emit(&UNINITIALIZED_LOCAL, span);
42        }
43    }
44}
45
46struct Checker<'gcx> {
47    gcx: Gcx<'gcx>,
48    hir: &'gcx Hir<'gcx>,
49    /// Value-type locals declared without an initializer that have not yet been written on
50    /// every path.
51    uninitialized: HashSet<VariableId>,
52    /// First read span per variable that was read while uninitialized.
53    findings: HashMap<VariableId, Span>,
54}
55
56impl Checker<'_> {
57    fn mark_written(&mut self, lhs: &Expr<'_>) {
58        for_each_lhs_var(self.gcx, lhs, &mut |v| {
59            self.uninitialized.remove(&v);
60        });
61    }
62}
63
64/// The loop statement of a conventional `for (uint i; i < n; i++)` header whose counter relies on
65/// its implicit zero. The header lowers to `{ decl; loop { if cond { body } else break } }` with
66/// the update on the loop source; matching the wrapper's span keeps declarations outside the
67/// header distinct.
68fn defaulted_counter_loop<'gcx>(
69    gcx: Gcx<'gcx>,
70    block: &'gcx Block<'gcx>,
71) -> Option<&'gcx Stmt<'gcx>> {
72    if let [Stmt { kind: StmtKind::DeclSingle(vid), .. }, loop_stmt] = block.stmts
73        && block.span == loop_stmt.span
74        && let StmtKind::Loop(body, LoopSource::For { update: Some(update) }) = &loop_stmt.kind
75        && let var = gcx.hir.variable(*vid)
76        && var.initializer.is_none()
77        && matches!(var.ty.kind, TypeKind::Elementary(ElementaryType::UInt(_)))
78        && let [Stmt { kind: StmtKind::If(cond, _, Some(else_)), .. }] = body.stmts
79        && matches!(else_.kind, StmtKind::Break)
80        && let ExprKind::Binary(left, op, right) = &cond.peel_parens().kind
81        && ((matches!(op.kind, BinOpKind::Lt | BinOpKind::Le)
82            && gcx.resolved_variable(left) == Some(*vid))
83            || (matches!(op.kind, BinOpKind::Gt | BinOpKind::Ge)
84                && gcx.resolved_variable(right) == Some(*vid)))
85        && let StmtKind::Expr(update) = &update.kind
86        && let ExprKind::Unary(op, target) = &update.peel_parens().kind
87        && matches!(op.kind, UnOpKind::PreInc | UnOpKind::PostInc)
88        && gcx.resolved_variable(target) == Some(*vid)
89    {
90        Some(loop_stmt)
91    } else {
92        None
93    }
94}
95
96impl<'gcx> Visit<'gcx> for Checker<'gcx> {
97    type BreakValue = Never;
98
99    fn hir(&self) -> &'gcx Hir<'gcx> {
100        self.hir
101    }
102
103    fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Never> {
104        match &stmt.kind {
105            StmtKind::Block(block) => {
106                if let Some(loop_stmt) = defaulted_counter_loop(self.gcx, block) {
107                    // Skip only the counter's declaration; all reads in the loop still run
108                    // through the ordinary checker, including reads of other locals.
109                    return self.visit_stmt(loop_stmt);
110                }
111            }
112            StmtKind::DeclSingle(vid) => {
113                let v = self.hir.variable(*vid);
114                if v.kind == VarKind::Statement
115                    && v.initializer.is_none()
116                    && self.gcx.type_of_item((*vid).into()).is_value_type()
117                {
118                    self.uninitialized.insert(*vid);
119                }
120            }
121            // A variable stays uninitialized if any branch that falls through fails to write it.
122            StmtKind::If(cond, then, else_) => {
123                self.visit_expr(cond)?;
124                let before = self.uninitialized.clone();
125                self.visit_stmt(then)?;
126                let after_then = std::mem::replace(&mut self.uninitialized, before);
127                if let Some(else_) = else_ {
128                    self.visit_stmt(else_)?;
129                }
130                if branch_always_exits(self.gcx, then) {
131                    // Only the else path continues; keep its state.
132                } else if else_.is_some_and(|expr| branch_always_exits(self.gcx, expr)) {
133                    self.uninitialized = after_then;
134                } else {
135                    self.uninitialized.extend(after_then);
136                }
137                return ControlFlow::Continue(());
138            }
139            // `do-while` runs its body once, so its writes are guaranteed; `for`/`while` may run
140            // zero times, so theirs are discarded.
141            StmtKind::Loop(block, source) => {
142                let before = self.uninitialized.clone();
143                for s in loop_stmts(*block, *source) {
144                    self.visit_stmt(s)?;
145                }
146                if !matches!(source, LoopSource::DoWhile) {
147                    self.uninitialized = before;
148                }
149                return ControlFlow::Continue(());
150            }
151            // Each clause is an independent path, like `if`/`else` branches.
152            StmtKind::Try(t) => {
153                self.visit_expr(&t.expr)?;
154                let before = self.uninitialized.clone();
155                let mut merged = HashSet::new();
156                for clause in t.clauses {
157                    self.uninitialized = before.clone();
158                    for s in clause.block.stmts {
159                        self.visit_stmt(s)?;
160                    }
161                    merged.extend(self.uninitialized.drain());
162                }
163                self.uninitialized = merged;
164                return ControlFlow::Continue(());
165            }
166            _ => {}
167        }
168        self.walk_stmt(stmt)
169    }
170
171    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
172        match &expr.kind {
173            // Compound `op=` reads the lhs first; plain `=` reads only the rhs (catches `x = x`).
174            // The lhs is still walked afterwards for reads inside e.g. an index expression.
175            ExprKind::Assign(lhs, op, rhs) => {
176                if op.is_some() {
177                    self.visit_expr(lhs)?;
178                }
179                self.visit_expr(rhs)?;
180                self.mark_written(lhs);
181                if op.is_none() {
182                    self.visit_expr(lhs)?;
183                }
184                ControlFlow::Continue(())
185            }
186            // `delete x` is an explicit write to the zero value, not a read.
187            ExprKind::Delete(target) => {
188                self.mark_written(target);
189                self.visit_expr(target)
190            }
191            ExprKind::Ident(_) => {
192                if let Some(vid) =
193                    self.gcx.resolved_variable(expr).filter(|v| self.uninitialized.contains(v))
194                {
195                    self.findings.entry(vid).or_insert(expr.span);
196                }
197                ControlFlow::Continue(())
198            }
199            _ => self.walk_expr(expr),
200        }
201    }
202}