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::{
11            self, CallArgsKind, ContractId, Expr, ExprKind, FunctionId, Hir, ItemId, Res,
12            VariableId, Visit,
13        },
14        ty::TyKind,
15    },
16};
17use std::{convert::Infallible, ops::ControlFlow};
18
19declare_forge_lint!(
20    FUNCTION_INIT_STATE,
21    Severity::Info,
22    "function-init-state",
23    "state variable initializer depends on a non-pure function or another state variable"
24);
25
26impl<'hir> LateLintPass<'hir> for FunctionInitState {
27    fn check_nested_contract(
28        &mut self,
29        ctx: &LintContext,
30        gcx: Gcx<'hir>,
31        hir: &'hir Hir<'hir>,
32        id: ContractId,
33    ) {
34        // State variable initializers run at construction, before the constructor body, in
35        // base-to-derived order: reading another non-constant state variable or calling a
36        // non-pure function there observes that partial state. Constants are fixed at compile
37        // time, so both constant declarations and references to constants are fine.
38        let contract = hir.contract(id);
39        for item_id in contract.items {
40            if let ItemId::Variable(variable_id) = item_id {
41                let variable = hir.variable(*variable_id);
42                // A constant's initializer is restricted to compile-time constant expressions.
43                if variable.is_state_variable()
44                    && !variable.is_constant()
45                    && let Some(initializer) = variable.initializer
46                {
47                    let mut finder = ImpureRefFinder {
48                        gcx,
49                        hir,
50                        source: contract.source,
51                        contract: id,
52                        found: false,
53                    };
54                    let _ = finder.visit_expr(initializer);
55                    if finder.found {
56                        ctx.emit(&FUNCTION_INIT_STATE, variable.span);
57                    }
58                }
59            }
60        }
61    }
62}
63
64/// Looks for a reference to a non-constant state variable or to a non-pure function anywhere in
65/// an initializer expression, arguments of nested calls included.
66struct ImpureRefFinder<'hir> {
67    gcx: Gcx<'hir>,
68    hir: &'hir Hir<'hir>,
69    /// The source and contract of the initializer, the viewpoint for `using for` lookups.
70    source: hir::SourceId,
71    contract: ContractId,
72    found: bool,
73}
74
75impl<'hir> Visit<'hir> for ImpureRefFinder<'hir> {
76    type BreakValue = Infallible;
77
78    fn hir(&self) -> &'hir Hir<'hir> {
79        self.hir
80    }
81
82    fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
83        match &expr.kind {
84            // A call: the type checker already resolved the one function it dispatches to
85            // (overload selection by argument types, override shadowing, `super.` and the
86            // qualified forms), so judge that target alone and walk the rest manually,
87            // skipping the default walk that would re-judge the callee by name matching.
88            ExprKind::Call(callee, args, opts) => {
89                if let Some(function_id) = self.resolved_function(callee) {
90                    self.judge_function(function_id);
91                }
92                match &callee.peel_parens().kind {
93                    // The callee name can also resolve to a variable: a call through a
94                    // function pointer stored in state reads that variable.
95                    ExprKind::Ident(resolutions) => {
96                        for res in *resolutions {
97                            if let Res::Item(ItemId::Variable(variable_id)) = res {
98                                self.judge_variable(*variable_id);
99                            }
100                        }
101                    }
102                    // The receiver of a member call can read state itself.
103                    ExprKind::Member(base, _) => {
104                        let _ = self.visit_expr(base);
105                    }
106                    _ => {
107                        let _ = self.visit_expr(callee);
108                    }
109                }
110                if let Some(opts) = opts {
111                    for arg in opts.args {
112                        let _ = self.visit_expr(&arg.value);
113                    }
114                }
115                match &args.kind {
116                    CallArgsKind::Unnamed(exprs) => {
117                        for arg in *exprs {
118                            let _ = self.visit_expr(arg);
119                        }
120                    }
121                    CallArgsKind::Named(named) => {
122                        for arg in *named {
123                            let _ = self.visit_expr(&arg.value);
124                        }
125                    }
126                }
127                return ControlFlow::Continue(());
128            }
129            // A plain reference to a name outside a call position.
130            ExprKind::Ident(resolutions) => self.judge_resolutions(resolutions),
131            // A member reference used as a value has a resolved target too (`x.f` selects an
132            // override like `x.f()` would): judge it when the type checker gives one, and
133            // scan by name only when there is no resolved function.
134            ExprKind::Member(base, member) => {
135                if let Some(function_id) = self.resolved_function(expr) {
136                    self.judge_function(function_id);
137                } else {
138                    self.judge_member(base, member);
139                }
140            }
141            _ => {}
142        }
143        self.walk_expr(expr)
144    }
145}
146
147impl ImpureRefFinder<'_> {
148    /// The single function an expression resolves to, for a callee or for a member reference
149    /// used as a value. `type_of_expr` is the function the type checker resolved, so overload
150    /// selection by argument types, override shadowing, `super.` and the qualified and
151    /// `using for` forms are already accounted for.
152    fn resolved_function(&self, expr: &Expr<'_>) -> Option<FunctionId> {
153        let ty = self.gcx.type_of_expr(expr.peel_parens().id)?;
154        match ty.kind {
155            TyKind::Fn(function_ty) => function_ty.function_id,
156            _ => None,
157        }
158    }
159
160    fn judge_resolutions(&mut self, resolutions: &[Res]) {
161        for res in resolutions {
162            match res {
163                Res::Item(ItemId::Variable(variable_id)) => self.judge_variable(*variable_id),
164                Res::Item(ItemId::Function(function_id)) => {
165                    self.judge_function(*function_id);
166                }
167                _ => {}
168            }
169        }
170    }
171
172    /// Judges a member read with no resolved function type (`Base.stateVar`): the member
173    /// ident carries no resolution, so type the base and scan by name. Calls and function
174    /// references never come here, their target is resolved from the expression type.
175    fn judge_member(&mut self, base: &Expr<'_>, member: &solar::ast::Ident) {
176        let Some(ty) = self.gcx.type_of_expr(base.peel_parens().id) else { return };
177        // A contract name used as the base is a type-namespace item, so its type comes wrapped
178        // as `Type(Contract(..))`, while a contract-typed value comes bare.
179        let ty = ty.peel_refs();
180        let ty = match ty.kind {
181            TyKind::Type(inner) => inner.peel_refs(),
182            _ => ty,
183        };
184        if let TyKind::Contract(contract_id) = ty.kind {
185            // Walk the linearization: an inherited function or getter is not among the
186            // contract's own items.
187            for base_id in self.hir.contract(contract_id).linearized_bases {
188                for item_id in self.hir.contract(*base_id).items {
189                    match item_id {
190                        ItemId::Variable(variable_id)
191                            if self
192                                .hir
193                                .variable(*variable_id)
194                                .name
195                                .is_some_and(|name| name.name == member.name) =>
196                        {
197                            self.judge_variable(*variable_id);
198                        }
199                        ItemId::Function(function_id)
200                            if self
201                                .hir
202                                .function(*function_id)
203                                .name
204                                .is_some_and(|name| name.name == member.name) =>
205                        {
206                            self.judge_function(*function_id);
207                        }
208                        _ => {}
209                    }
210                }
211            }
212        } else {
213            // A `using for` binding read as a value: the bound library function is a member of
214            // the value type.
215            for member_entry in self.gcx.members_of(ty, self.source, Some(self.contract)) {
216                if member_entry.name == member.name
217                    && let TyKind::Fn(function_ty) = member_entry.ty.kind
218                    && let Some(function_id) = function_ty.function_id
219                {
220                    self.judge_function(function_id);
221                }
222            }
223        }
224    }
225
226    /// A read of another state variable: its initializer may not have run yet.
227    fn judge_variable(&mut self, variable_id: VariableId) {
228        let variable = self.hir.variable(variable_id);
229        if variable.is_state_variable() && !variable.is_constant() {
230            self.found = true;
231        }
232    }
233
234    /// A non-pure function observes the same partial state. A variable referenced through its
235    /// synthesized getter is judged as a read of the variable itself, so a public constant
236    /// stays fine.
237    fn judge_function(&mut self, function_id: FunctionId) {
238        let function = self.hir.function(function_id);
239        if let Some(variable_id) = function.gettee {
240            self.judge_variable(variable_id);
241        } else if function.state_mutability != StateMutability::Pure {
242            self.found = true;
243        }
244    }
245}