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        hir: &'hir hir::Hir<'hir>,
25        contract: &'hir hir::Contract<'hir>,
26    ) {
27        // Skip interfaces, they cannot have mutable state variables.
28        if contract.kind == ContractKind::Interface {
29            return;
30        }
31
32        // Collect state variable IDs, skipping constants and immutables
33        // (those are handled by the compiler and don't occupy storage slots).
34        let state_vars: Vec<hir::VariableId> = contract
35            .variables()
36            .filter(|&var_id| {
37                let var = hir.variable(var_id);
38                !var.is_constant() && !var.is_immutable()
39            })
40            .collect();
41
42        if state_vars.is_empty() {
43            return;
44        }
45
46        // Walk the full contract — functions (including modifier call args, parameters, returns,
47        // and bodies) and state variable initializers — to collect every variable referenced
48        // anywhere in this contract.
49        let mut collector = UsedVarCollector { hir, used: HashSet::new() };
50        for func_id in contract.all_functions() {
51            let _ = collector.visit_nested_function(func_id);
52        }
53        // State variables can reference other state variables in their initializers.
54        for var_id in contract.variables() {
55            let _ = collector.visit_nested_var(var_id);
56        }
57
58        // Report any state variable that was never referenced.
59        for var_id in state_vars {
60            if !collector.used.contains(&var_id) {
61                let var = hir.variable(var_id);
62                ctx.emit(&UNUSED_STATE_VARIABLES, var.span);
63            }
64        }
65    }
66}
67
68struct UsedVarCollector<'hir> {
69    hir: &'hir hir::Hir<'hir>,
70    used: HashSet<hir::VariableId>,
71}
72
73impl<'hir> hir::Visit<'hir> for UsedVarCollector<'hir> {
74    type BreakValue = Never;
75
76    fn hir(&self) -> &'hir hir::Hir<'hir> {
77        self.hir
78    }
79
80    fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
81        if let hir::ExprKind::Ident(resolutions) = &expr.kind {
82            for res in *resolutions {
83                if let hir::Res::Item(hir::ItemId::Variable(var_id)) = res {
84                    self.used.insert(*var_id);
85                }
86            }
87        }
88        self.walk_expr(expr)
89    }
90}