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}