Skip to main content

forge_lint/sol/med/
uninitialized_state_variables.rs

1use super::UninitializedStateVariables;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast::ContractKind,
8    interface::{data_structures::Never, sym},
9    sema::{
10        Hir,
11        hir::{
12            Block, CallArgs, CallArgsKind, ContractId, DataLocation, Expr, ExprKind, Function,
13            ItemId, Res, Stmt, StmtKind, TypeKind, VariableId, Visit,
14        },
15    },
16};
17use std::{collections::HashSet, ops::ControlFlow};
18
19declare_forge_lint!(
20    UNINITIALIZED_STATE_VARIABLES,
21    Severity::Med,
22    "uninitialized-state",
23    "state variable is read but never written"
24);
25
26impl<'hir> LateLintPass<'hir> for UninitializedStateVariables {
27    fn check_nested_contract(
28        &mut self,
29        ctx: &LintContext,
30        _gcx: solar::sema::Gcx<'hir>,
31        hir: &'hir Hir<'hir>,
32        contract_id: ContractId,
33    ) {
34        let contract = hir.contract(contract_id);
35
36        if matches!(contract.kind, ContractKind::Interface | ContractKind::AbstractContract) {
37            return;
38        }
39
40        // If C3 linearization failed the linearized_bases list is incomplete;
41        // skip rather than produce unsound results.
42        if contract.linearization_failed() {
43            return;
44        }
45
46        // Collect non-constant, non-immutable state variables from the whole
47        // inheritance chain (linearized_bases[0] is the contract itself).
48        let state_vars: Vec<VariableId> = contract
49            .linearized_bases
50            .iter()
51            .flat_map(|&cid| hir.contract(cid).variables())
52            .filter(|&var_id| {
53                let var = hir.variable(var_id);
54                !var.is_constant()
55                    && !var.is_immutable()
56                    && !matches!(var.ty.kind, TypeKind::Mapping(_))
57            })
58            .collect();
59
60        if state_vars.is_empty() {
61            return;
62        }
63
64        let candidate_set: HashSet<VariableId> = state_vars.iter().copied().collect();
65
66        let mut written: HashSet<VariableId> = HashSet::new();
67
68        for &var_id in &state_vars {
69            if hir.variable(var_id).initializer.is_some() {
70                written.insert(var_id);
71            }
72        }
73
74        // Walk every function in the inheritance chain.
75        // Bail out conservatively if any function body contains inline assembly,
76        // because we cannot soundly track reads or writes through it.
77        let bases = contract.linearized_bases;
78
79        for &cid in bases {
80            for func_id in hir.contract(cid).all_functions() {
81                let function = hir.function(func_id);
82
83                for modifier in function.modifiers {
84                    for expr in modifier.args.exprs() {
85                        if collect_expr_writes_checked(
86                            hir,
87                            expr,
88                            &candidate_set,
89                            &mut written,
90                            bases,
91                        )
92                        .is_err()
93                        {
94                            return;
95                        }
96                    }
97                }
98
99                if let Some(body) = function.body
100                    && collect_block_writes_checked(hir, body, &candidate_set, &mut written, bases)
101                        .is_err()
102                {
103                    return;
104                }
105            }
106
107            for base_modifier in hir.contract(cid).bases_args {
108                for expr in base_modifier.args.exprs() {
109                    if collect_expr_writes_checked(hir, expr, &candidate_set, &mut written, bases)
110                        .is_err()
111                    {
112                        return;
113                    }
114                }
115            }
116
117            // Walk state-vars initializer expressions for side-effect writes to other state vars
118            for var_id in hir.contract(cid).variables() {
119                if let Some(init) = hir.variable(var_id).initializer
120                    && collect_expr_writes_checked(hir, init, &candidate_set, &mut written, bases)
121                        .is_err()
122                {
123                    return;
124                }
125            }
126        }
127
128        let mut reader = ReadVarCollector { hir, read: HashSet::new() };
129        for &cid in contract.linearized_bases {
130            for func_id in hir.contract(cid).all_functions() {
131                let _ = reader.visit_nested_function(func_id);
132            }
133            for var_id in hir.contract(cid).variables() {
134                let _ = reader.visit_nested_var(var_id);
135            }
136            // Walk inheritance-specifier constructor args on the read side too
137            // (e.g. `contract B is A(owner)` reads `owner`).
138            for base_modifier in hir.contract(cid).bases_args {
139                let _ = reader.visit_modifier(base_modifier);
140            }
141        }
142
143        // Flag variables that are read but never written.
144        for var_id in state_vars {
145            if reader.read.contains(&var_id) && !written.contains(&var_id) {
146                let var = hir.variable(var_id);
147                ctx.emit(&UNINITIALIZED_STATE_VARIABLES, var.span);
148            }
149        }
150    }
151}
152
153fn collect_block_writes_checked<'hir>(
154    hir: &'hir Hir<'hir>,
155    block: Block<'hir>,
156    candidates: &HashSet<VariableId>,
157    writes: &mut HashSet<VariableId>,
158    bases: &'hir [ContractId],
159) -> Result<(), ()> {
160    for stmt in block.stmts {
161        collect_stmt_writes_checked(hir, stmt, candidates, writes, bases)?;
162    }
163    Ok(())
164}
165
166fn collect_stmt_writes_checked<'hir>(
167    hir: &'hir Hir<'hir>,
168    stmt: &'hir Stmt<'hir>,
169    candidates: &HashSet<VariableId>,
170    writes: &mut HashSet<VariableId>,
171    bases: &'hir [ContractId],
172) -> Result<(), ()> {
173    match &stmt.kind {
174        // Assembly can write storage directly; bail conservatively.
175        StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) | StmtKind::Err(_) => return Err(()),
176        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
177            collect_block_writes_checked(hir, *block, candidates, writes, bases)?;
178        }
179        StmtKind::If(condition, then_stmt, else_stmt) => {
180            collect_expr_writes_checked(hir, condition, candidates, writes, bases)?;
181            collect_stmt_writes_checked(hir, then_stmt, candidates, writes, bases)?;
182            if let Some(else_stmt) = else_stmt {
183                collect_stmt_writes_checked(hir, else_stmt, candidates, writes, bases)?;
184            }
185        }
186        StmtKind::Try(stmt_try) => {
187            collect_expr_writes_checked(hir, &stmt_try.expr, candidates, writes, bases)?;
188            for clause in stmt_try.clauses {
189                collect_block_writes_checked(hir, clause.block, candidates, writes, bases)?;
190            }
191        }
192        StmtKind::DeclSingle(var_id) => {
193            if let Some(initializer) = hir.variable(*var_id).initializer {
194                collect_expr_writes_checked(hir, initializer, candidates, writes, bases)?;
195            }
196        }
197        StmtKind::DeclMulti(_, expr)
198        | StmtKind::Emit(expr)
199        | StmtKind::Revert(expr)
200        | StmtKind::Return(Some(expr))
201        | StmtKind::Expr(expr) => {
202            collect_expr_writes_checked(hir, expr, candidates, writes, bases)?
203        }
204        StmtKind::Return(None) | StmtKind::Break | StmtKind::Continue | StmtKind::Placeholder => {}
205    }
206    Ok(())
207}
208
209fn collect_expr_writes_checked<'hir>(
210    hir: &'hir Hir<'hir>,
211    expr: &'hir Expr<'hir>,
212    candidates: &HashSet<VariableId>,
213    writes: &mut HashSet<VariableId>,
214    bases: &'hir [ContractId],
215) -> Result<(), ()> {
216    match &expr.kind {
217        ExprKind::Assign(lhs, _, rhs) => {
218            collect_lvalue_writes(lhs, candidates, writes);
219            collect_expr_writes_checked(hir, lhs, candidates, writes, bases)?;
220            collect_expr_writes_checked(hir, rhs, candidates, writes, bases)?;
221        }
222        ExprKind::Delete(inner) => {
223            collect_lvalue_writes(inner, candidates, writes);
224            collect_expr_writes_checked(hir, inner, candidates, writes, bases)?;
225        }
226        ExprKind::Unary(op, inner) => {
227            if op.kind.has_side_effects() {
228                collect_lvalue_writes(inner, candidates, writes);
229            }
230            collect_expr_writes_checked(hir, inner, candidates, writes, bases)?;
231        }
232        ExprKind::Array(exprs) => {
233            for expr in *exprs {
234                collect_expr_writes_checked(hir, expr, candidates, writes, bases)?;
235            }
236        }
237        ExprKind::Binary(lhs, _, rhs) => {
238            collect_expr_writes_checked(hir, lhs, candidates, writes, bases)?;
239            collect_expr_writes_checked(hir, rhs, candidates, writes, bases)?;
240        }
241        ExprKind::Call(callee, args, named_args) => {
242            if let ExprKind::Member(base, _) = &callee.kind {
243                // Covers push/pop and library dispatch (`using Lib for T` with `T storage self`);
244                // can't resolve callee without Gcx. Treat the receiver as a write target to avoid
245                // false positives.
246                collect_lvalue_writes(base, candidates, writes);
247            }
248
249            // Direct calls to internal functions that take a `storage` parameter
250            // mutate the corresponding argument in place; treat it as a write.
251            //
252            // Handles bare identifier callees (`_set(slot, v)`) and qualified member
253            // callees (`BaseSetter._set(slot, v)`, `super._set(slot, v)`).
254            let funcs = collect_callee_funcs(hir, callee, bases);
255            if !funcs.is_empty() {
256                mark_storage_args(&funcs, hir, args, candidates, writes);
257            }
258
259            collect_expr_writes_checked(hir, callee, candidates, writes, bases)?;
260            for expr in args.exprs() {
261                collect_expr_writes_checked(hir, expr, candidates, writes, bases)?;
262            }
263            if let Some(named_args) = named_args {
264                for arg in named_args.args {
265                    collect_expr_writes_checked(hir, &arg.value, candidates, writes, bases)?;
266                }
267            }
268        }
269        ExprKind::Index(base, index) => {
270            collect_expr_writes_checked(hir, base, candidates, writes, bases)?;
271            if let Some(index) = index {
272                collect_expr_writes_checked(hir, index, candidates, writes, bases)?;
273            }
274        }
275        ExprKind::Slice(base, start, end) => {
276            collect_expr_writes_checked(hir, base, candidates, writes, bases)?;
277            if let Some(start) = start {
278                collect_expr_writes_checked(hir, start, candidates, writes, bases)?;
279            }
280            if let Some(end) = end {
281                collect_expr_writes_checked(hir, end, candidates, writes, bases)?;
282            }
283        }
284        ExprKind::Member(base, _) | ExprKind::Payable(base) => {
285            collect_expr_writes_checked(hir, base, candidates, writes, bases)?;
286        }
287        ExprKind::Ternary(condition, then_expr, else_expr) => {
288            collect_expr_writes_checked(hir, condition, candidates, writes, bases)?;
289            collect_expr_writes_checked(hir, then_expr, candidates, writes, bases)?;
290            collect_expr_writes_checked(hir, else_expr, candidates, writes, bases)?;
291        }
292        ExprKind::Tuple(exprs) => {
293            for expr in exprs.iter().flatten() {
294                collect_expr_writes_checked(hir, expr, candidates, writes, bases)?;
295            }
296        }
297        ExprKind::Ident(_)
298        | ExprKind::Lit(_)
299        | ExprKind::New(_)
300        | ExprKind::TypeCall(_)
301        | ExprKind::Type(_)
302        | ExprKind::YulMember(..)
303        | ExprKind::Err(_) => {}
304    }
305    Ok(())
306}
307
308/// Collect the set of internal function candidates that a call expression may invoke.
309///
310/// Handles three callee shapes:
311/// - `f(...)` bare `Ident` with function resolutions
312/// - `Contract.f(...)` `Member` whose base resolves to a `ContractId`
313/// - `super.f(...)` `Member` whose base is the `super` builtin; searches all linearized bases
314///   except the current contract (`bases[0]`), matching Solidity's MRO dispatch semantics
315fn collect_callee_funcs<'hir>(
316    hir: &'hir Hir<'hir>,
317    callee: &'hir Expr<'hir>,
318    bases: &[ContractId],
319) -> Vec<&'hir Function<'hir>> {
320    match &callee.kind {
321        ExprKind::Ident(resolutions) => resolutions
322            .iter()
323            .filter_map(|res| {
324                if let Res::Item(ItemId::Function(func_id)) = res {
325                    Some(hir.function(*func_id))
326                } else {
327                    None
328                }
329            })
330            .collect(),
331        ExprKind::Member(base, method) => {
332            if let ExprKind::Ident(resolutions) = &base.peel_parens().kind {
333                let is_super = resolutions
334                    .iter()
335                    .any(|r| matches!(r, Res::Builtin(b) if b.name() == sym::super_));
336
337                let contract_ids: Vec<ContractId> = if is_super {
338                    // `super.f(...)` dispatches to the *parent* MRO entries, never to
339                    // the current contract (bases[0]).  Including bases[0] would let a
340                    // child-only storage overload of `f` suppress a warning even when
341                    // `super.f` actually resolves to a non-storage parent overload.
342                    bases.get(1..).unwrap_or_default().to_vec()
343                } else {
344                    resolutions
345                        .iter()
346                        .filter_map(|res| {
347                            if let Res::Item(ItemId::Contract(cid)) = res {
348                                Some(*cid)
349                            } else {
350                                None
351                            }
352                        })
353                        .collect()
354                };
355
356                contract_ids
357                    .into_iter()
358                    .flat_map(|cid| hir.contract(cid).all_functions())
359                    .filter_map(|fid| {
360                        let f = hir.function(fid);
361                        f.name.is_some_and(|n| n == *method).then_some(f)
362                    })
363                    .collect()
364            } else {
365                vec![]
366            }
367        }
368        _ => vec![],
369    }
370}
371
372/// For each call argument, if ANY resolved overload has a `storage` parameter at that
373/// position, treat the argument as a write target.
374fn mark_storage_args<'hir>(
375    funcs: &[&Function<'hir>],
376    hir: &'hir Hir<'hir>,
377    args: &CallArgs<'hir>,
378    candidates: &HashSet<VariableId>,
379    writes: &mut HashSet<VariableId>,
380) {
381    if let CallArgsKind::Unnamed(_) = args.kind {
382        for (i, arg_expr) in args.exprs().enumerate() {
383            let any_storage = funcs.iter().any(|func| {
384                func.parameters.get(i).is_some_and(|&pid| {
385                    matches!(hir.variable(pid).data_location, Some(DataLocation::Storage))
386                })
387            });
388            if any_storage {
389                collect_lvalue_writes(arg_expr, candidates, writes);
390            }
391        }
392    }
393
394    if let CallArgsKind::Named(named) = args.kind {
395        for named_arg in named {
396            let any_storage = funcs.iter().any(|func| {
397                let param = func
398                    .parameters
399                    .iter()
400                    .find(|&&pid| hir.variable(pid).name.is_some_and(|n| n == named_arg.name));
401                param.is_some_and(|&pid| {
402                    matches!(hir.variable(pid).data_location, Some(DataLocation::Storage))
403                })
404            });
405            if any_storage {
406                collect_lvalue_writes(&named_arg.value, candidates, writes);
407            }
408        }
409    }
410}
411
412fn collect_lvalue_writes(
413    expr: &Expr<'_>,
414    candidates: &HashSet<VariableId>,
415    writes: &mut HashSet<VariableId>,
416) {
417    match &expr.peel_parens().kind {
418        ExprKind::Ident([Res::Item(ItemId::Variable(id)), ..]) if candidates.contains(id) => {
419            writes.insert(*id);
420        }
421        ExprKind::Tuple(exprs) => {
422            for expr in exprs.iter().flatten() {
423                collect_lvalue_writes(expr, candidates, writes);
424            }
425        }
426        ExprKind::Index(base, _) | ExprKind::Slice(base, _, _) | ExprKind::Member(base, _) => {
427            collect_lvalue_writes(base, candidates, writes)
428        }
429        _ => {}
430    }
431}
432
433struct ReadVarCollector<'hir> {
434    hir: &'hir Hir<'hir>,
435    read: HashSet<VariableId>,
436}
437
438impl<'hir> Visit<'hir> for ReadVarCollector<'hir> {
439    type BreakValue = Never;
440
441    fn hir(&self) -> &'hir Hir<'hir> {
442        self.hir
443    }
444
445    fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
446        if let ExprKind::Ident(resolutions) = &expr.kind {
447            for res in *resolutions {
448                if let Res::Item(ItemId::Variable(var_id)) = res {
449                    self.read.insert(*var_id);
450                }
451            }
452        }
453        self.walk_expr(expr)
454    }
455}