Skip to main content

forge_lint/sol/gas/
unused_state_variables.rs

1use super::UnusedStateVariables;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast::ContractKind,
8    interface::data_structures::Never,
9    sema::hir::{self, Visit as _},
10};
11use std::{collections::HashSet, ops::ControlFlow};
12
13declare_forge_lint!(
14    UNUSED_STATE_VARIABLES,
15    Severity::Gas,
16    "unused-state-variables",
17    "state variable is never used"
18);
19
20impl<'hir> LateLintPass<'hir> for UnusedStateVariables {
21    fn check_contract(
22        &mut self,
23        ctx: &LintContext,
24        _gcx: solar::sema::Gcx<'hir>,
25        hir: &'hir hir::Hir<'hir>,
26        contract: &'hir hir::Contract<'hir>,
27    ) {
28        // Skip interfaces, they cannot have mutable state variables.
29        if contract.kind == ContractKind::Interface {
30            return;
31        }
32
33        // Collect state variable IDs, skipping constants and immutables
34        // (those are handled by the compiler and don't occupy storage slots).
35        let state_vars: Vec<hir::VariableId> = contract
36            .variables()
37            .filter(|&var_id| {
38                let var = hir.variable(var_id);
39                !var.is_constant() && !var.is_immutable()
40            })
41            .collect();
42
43        if state_vars.is_empty() {
44            return;
45        }
46
47        // Walk the full contract — functions (including modifier call args, parameters, returns,
48        // and bodies) and state variable initializers — to collect every variable referenced
49        // anywhere in this contract.
50        let mut collector = UsedVarCollector { hir, used: HashSet::new() };
51        for func_id in contract.all_functions() {
52            let _ = collector.visit_nested_function(func_id);
53        }
54        // State variables can reference other state variables in their initializers.
55        for var_id in contract.variables() {
56            let _ = collector.visit_nested_var(var_id);
57        }
58
59        // Report any state variable that was never referenced.
60        for var_id in state_vars {
61            if !collector.used.contains(&var_id) {
62                let var = hir.variable(var_id);
63                ctx.emit(&UNUSED_STATE_VARIABLES, var.span);
64            }
65        }
66    }
67}
68
69struct UsedVarCollector<'hir> {
70    hir: &'hir hir::Hir<'hir>,
71    used: HashSet<hir::VariableId>,
72}
73
74impl<'hir> hir::Visit<'hir> for UsedVarCollector<'hir> {
75    type BreakValue = Never;
76
77    fn hir(&self) -> &'hir hir::Hir<'hir> {
78        self.hir
79    }
80
81    fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
82        if let hir::ExprKind::Ident(resolutions) = &expr.kind {
83            for res in *resolutions {
84                if let hir::Res::Item(hir::ItemId::Variable(var_id)) = res {
85                    self.used.insert(*var_id);
86                }
87            }
88        }
89        self.walk_expr(expr)
90    }
91}