Skip to main content

forge_lint/sol/info/
function_init_state.rs

1use super::FunctionInitState;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast::StateMutability,
8    sema::{
9        Gcx,
10        hir::{ContractId, Expr, FunctionId, Hir, VariableId, Visit},
11    },
12};
13use std::{convert::Infallible, ops::ControlFlow};
14
15declare_forge_lint!(
16    FUNCTION_INIT_STATE,
17    Severity::Info,
18    "function-init-state",
19    "state variable initializer depends on a non-pure function or another state variable"
20);
21
22impl<'gcx> LateLintPass<'gcx> for FunctionInitState {
23    fn check_nested_contract(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, id: ContractId) {
24        // State variable initializers run at construction, before the constructor body, in
25        // base-to-derived order: reading another non-constant state variable or calling a
26        // non-pure function there observes that partial state. Constants are fixed at compile
27        // time, so both constant declarations and references to constants are fine.
28        let contract = gcx.hir.contract(id);
29        for item_id in contract.items {
30            let Some(variable) = item_id.as_variable().map(|v| gcx.hir.variable(v)) else {
31                continue;
32            };
33            if variable.is_state_variable()
34                && !variable.is_constant()
35                && let Some(initializer) = variable.initializer
36            {
37                let mut finder = ImpureRefFinder { gcx, found: false };
38                let _ = finder.visit_expr(initializer);
39                if finder.found {
40                    ctx.emit(&FUNCTION_INIT_STATE, variable.span);
41                }
42            }
43        }
44    }
45}
46
47/// Looks for a reference to a non-constant state variable or to a non-pure function anywhere in
48/// an initializer expression, arguments of nested calls included.
49struct ImpureRefFinder<'gcx> {
50    gcx: Gcx<'gcx>,
51    found: bool,
52}
53
54impl<'gcx> Visit<'gcx> for ImpureRefFinder<'gcx> {
55    type BreakValue = Infallible;
56
57    fn hir(&self) -> &'gcx Hir<'gcx> {
58        &self.gcx.hir
59    }
60
61    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
62        if let Some(variable_id) = self.gcx.resolved_variable(expr) {
63            self.judge_variable(variable_id);
64        } else if let Some(function_id) = self.gcx.resolved_function(expr) {
65            self.judge_function(function_id);
66        }
67        self.walk_expr(expr)
68    }
69}
70
71impl ImpureRefFinder<'_> {
72    /// A read of another state variable: its initializer may not have run yet.
73    fn judge_variable(&mut self, variable_id: VariableId) {
74        let variable = self.gcx.hir.variable(variable_id);
75        self.found |= variable.is_state_variable() && !variable.is_constant();
76    }
77
78    /// A non-pure function observes the same partial state. A variable referenced through its
79    /// synthesized getter is judged as a read of the variable itself, so a public constant
80    /// stays fine.
81    fn judge_function(&mut self, function_id: FunctionId) {
82        let function = self.gcx.hir.function(function_id);
83        match function.gettee {
84            Some(variable_id) => self.judge_variable(variable_id),
85            None => self.found |= function.state_mutability != StateMutability::Pure,
86        }
87    }
88}