1use super::UninitializedStateVariables;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{Severity, SolLint},
5};
6use solar::{
7 ast::ContractKind,
8 interface::{data_structures::Never, sym},
9 sema::{
10 Hir,
11 hir::{
12 Block, CallArgs, CallArgsKind, ContractId, DataLocation, Expr, ExprKind, Function,
13 ItemId, Res, Stmt, StmtKind, TypeKind, VariableId, Visit,
14 },
15 },
16};
17use std::{collections::HashSet, ops::ControlFlow};
18
19declare_forge_lint!(
20 UNINITIALIZED_STATE_VARIABLES,
21 Severity::Med,
22 "uninitialized-state",
23 "state variable is read but never written"
24);
25
26impl<'hir> LateLintPass<'hir> for UninitializedStateVariables {
27 fn check_nested_contract(
28 &mut self,
29 ctx: &LintContext,
30 _gcx: solar::sema::Gcx<'hir>,
31 hir: &'hir Hir<'hir>,
32 contract_id: ContractId,
33 ) {
34 let contract = hir.contract(contract_id);
35
36 if matches!(contract.kind, ContractKind::Interface | ContractKind::AbstractContract) {
37 return;
38 }
39
40 if contract.linearization_failed() {
43 return;
44 }
45
46 let state_vars: Vec<VariableId> = contract
49 .linearized_bases
50 .iter()
51 .flat_map(|&cid| hir.contract(cid).variables())
52 .filter(|&var_id| {
53 let var = hir.variable(var_id);
54 !var.is_constant()
55 && !var.is_immutable()
56 && !matches!(var.ty.kind, TypeKind::Mapping(_))
57 })
58 .collect();
59
60 if state_vars.is_empty() {
61 return;
62 }
63
64 let candidate_set: HashSet<VariableId> = state_vars.iter().copied().collect();
65
66 let mut written: HashSet<VariableId> = HashSet::new();
67
68 for &var_id in &state_vars {
69 if hir.variable(var_id).initializer.is_some() {
70 written.insert(var_id);
71 }
72 }
73
74 let bases = contract.linearized_bases;
78
79 for &cid in bases {
80 for func_id in hir.contract(cid).all_functions() {
81 let function = hir.function(func_id);
82
83 for modifier in function.modifiers {
84 for expr in modifier.args.exprs() {
85 if collect_expr_writes_checked(
86 hir,
87 expr,
88 &candidate_set,
89 &mut written,
90 bases,
91 )
92 .is_err()
93 {
94 return;
95 }
96 }
97 }
98
99 if let Some(body) = function.body
100 && collect_block_writes_checked(hir, body, &candidate_set, &mut written, bases)
101 .is_err()
102 {
103 return;
104 }
105 }
106
107 for base_modifier in hir.contract(cid).bases_args {
108 for expr in base_modifier.args.exprs() {
109 if collect_expr_writes_checked(hir, expr, &candidate_set, &mut written, bases)
110 .is_err()
111 {
112 return;
113 }
114 }
115 }
116
117 for var_id in hir.contract(cid).variables() {
119 if let Some(init) = hir.variable(var_id).initializer
120 && collect_expr_writes_checked(hir, init, &candidate_set, &mut written, bases)
121 .is_err()
122 {
123 return;
124 }
125 }
126 }
127
128 let mut reader = ReadVarCollector { hir, read: HashSet::new() };
129 for &cid in contract.linearized_bases {
130 for func_id in hir.contract(cid).all_functions() {
131 let _ = reader.visit_nested_function(func_id);
132 }
133 for var_id in hir.contract(cid).variables() {
134 let _ = reader.visit_nested_var(var_id);
135 }
136 for base_modifier in hir.contract(cid).bases_args {
139 let _ = reader.visit_modifier(base_modifier);
140 }
141 }
142
143 for var_id in state_vars {
145 if reader.read.contains(&var_id) && !written.contains(&var_id) {
146 let var = hir.variable(var_id);
147 ctx.emit(&UNINITIALIZED_STATE_VARIABLES, var.span);
148 }
149 }
150 }
151}
152
153fn collect_block_writes_checked<'hir>(
154 hir: &'hir Hir<'hir>,
155 block: Block<'hir>,
156 candidates: &HashSet<VariableId>,
157 writes: &mut HashSet<VariableId>,
158 bases: &'hir [ContractId],
159) -> Result<(), ()> {
160 for stmt in block.stmts {
161 collect_stmt_writes_checked(hir, stmt, candidates, writes, bases)?;
162 }
163 Ok(())
164}
165
166fn collect_stmt_writes_checked<'hir>(
167 hir: &'hir Hir<'hir>,
168 stmt: &'hir Stmt<'hir>,
169 candidates: &HashSet<VariableId>,
170 writes: &mut HashSet<VariableId>,
171 bases: &'hir [ContractId],
172) -> Result<(), ()> {
173 match &stmt.kind {
174 StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) | StmtKind::Err(_) => return Err(()),
176 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
177 collect_block_writes_checked(hir, *block, candidates, writes, bases)?;
178 }
179 StmtKind::If(condition, then_stmt, else_stmt) => {
180 collect_expr_writes_checked(hir, condition, candidates, writes, bases)?;
181 collect_stmt_writes_checked(hir, then_stmt, candidates, writes, bases)?;
182 if let Some(else_stmt) = else_stmt {
183 collect_stmt_writes_checked(hir, else_stmt, candidates, writes, bases)?;
184 }
185 }
186 StmtKind::Try(stmt_try) => {
187 collect_expr_writes_checked(hir, &stmt_try.expr, candidates, writes, bases)?;
188 for clause in stmt_try.clauses {
189 collect_block_writes_checked(hir, clause.block, candidates, writes, bases)?;
190 }
191 }
192 StmtKind::DeclSingle(var_id) => {
193 if let Some(initializer) = hir.variable(*var_id).initializer {
194 collect_expr_writes_checked(hir, initializer, candidates, writes, bases)?;
195 }
196 }
197 StmtKind::DeclMulti(_, expr)
198 | StmtKind::Emit(expr)
199 | StmtKind::Revert(expr)
200 | StmtKind::Return(Some(expr))
201 | StmtKind::Expr(expr) => {
202 collect_expr_writes_checked(hir, expr, candidates, writes, bases)?
203 }
204 StmtKind::Return(None) | StmtKind::Break | StmtKind::Continue | StmtKind::Placeholder => {}
205 }
206 Ok(())
207}
208
209fn collect_expr_writes_checked<'hir>(
210 hir: &'hir Hir<'hir>,
211 expr: &'hir Expr<'hir>,
212 candidates: &HashSet<VariableId>,
213 writes: &mut HashSet<VariableId>,
214 bases: &'hir [ContractId],
215) -> Result<(), ()> {
216 match &expr.kind {
217 ExprKind::Assign(lhs, _, rhs) => {
218 collect_lvalue_writes(lhs, candidates, writes);
219 collect_expr_writes_checked(hir, lhs, candidates, writes, bases)?;
220 collect_expr_writes_checked(hir, rhs, candidates, writes, bases)?;
221 }
222 ExprKind::Delete(inner) => {
223 collect_lvalue_writes(inner, candidates, writes);
224 collect_expr_writes_checked(hir, inner, candidates, writes, bases)?;
225 }
226 ExprKind::Unary(op, inner) => {
227 if op.kind.has_side_effects() {
228 collect_lvalue_writes(inner, candidates, writes);
229 }
230 collect_expr_writes_checked(hir, inner, candidates, writes, bases)?;
231 }
232 ExprKind::Array(exprs) => {
233 for expr in *exprs {
234 collect_expr_writes_checked(hir, expr, candidates, writes, bases)?;
235 }
236 }
237 ExprKind::Binary(lhs, _, rhs) => {
238 collect_expr_writes_checked(hir, lhs, candidates, writes, bases)?;
239 collect_expr_writes_checked(hir, rhs, candidates, writes, bases)?;
240 }
241 ExprKind::Call(callee, args, named_args) => {
242 if let ExprKind::Member(base, _) = &callee.kind {
243 collect_lvalue_writes(base, candidates, writes);
247 }
248
249 let funcs = collect_callee_funcs(hir, callee, bases);
255 if !funcs.is_empty() {
256 mark_storage_args(&funcs, hir, args, candidates, writes);
257 }
258
259 collect_expr_writes_checked(hir, callee, candidates, writes, bases)?;
260 for expr in args.exprs() {
261 collect_expr_writes_checked(hir, expr, candidates, writes, bases)?;
262 }
263 if let Some(named_args) = named_args {
264 for arg in named_args.args {
265 collect_expr_writes_checked(hir, &arg.value, candidates, writes, bases)?;
266 }
267 }
268 }
269 ExprKind::Index(base, index) => {
270 collect_expr_writes_checked(hir, base, candidates, writes, bases)?;
271 if let Some(index) = index {
272 collect_expr_writes_checked(hir, index, candidates, writes, bases)?;
273 }
274 }
275 ExprKind::Slice(base, start, end) => {
276 collect_expr_writes_checked(hir, base, candidates, writes, bases)?;
277 if let Some(start) = start {
278 collect_expr_writes_checked(hir, start, candidates, writes, bases)?;
279 }
280 if let Some(end) = end {
281 collect_expr_writes_checked(hir, end, candidates, writes, bases)?;
282 }
283 }
284 ExprKind::Member(base, _) | ExprKind::Payable(base) => {
285 collect_expr_writes_checked(hir, base, candidates, writes, bases)?;
286 }
287 ExprKind::Ternary(condition, then_expr, else_expr) => {
288 collect_expr_writes_checked(hir, condition, candidates, writes, bases)?;
289 collect_expr_writes_checked(hir, then_expr, candidates, writes, bases)?;
290 collect_expr_writes_checked(hir, else_expr, candidates, writes, bases)?;
291 }
292 ExprKind::Tuple(exprs) => {
293 for expr in exprs.iter().flatten() {
294 collect_expr_writes_checked(hir, expr, candidates, writes, bases)?;
295 }
296 }
297 ExprKind::Ident(_)
298 | ExprKind::Lit(_)
299 | ExprKind::New(_)
300 | ExprKind::TypeCall(_)
301 | ExprKind::Type(_)
302 | ExprKind::YulMember(..)
303 | ExprKind::Err(_) => {}
304 }
305 Ok(())
306}
307
308fn collect_callee_funcs<'hir>(
316 hir: &'hir Hir<'hir>,
317 callee: &'hir Expr<'hir>,
318 bases: &[ContractId],
319) -> Vec<&'hir Function<'hir>> {
320 match &callee.kind {
321 ExprKind::Ident(resolutions) => resolutions
322 .iter()
323 .filter_map(|res| {
324 if let Res::Item(ItemId::Function(func_id)) = res {
325 Some(hir.function(*func_id))
326 } else {
327 None
328 }
329 })
330 .collect(),
331 ExprKind::Member(base, method) => {
332 if let ExprKind::Ident(resolutions) = &base.peel_parens().kind {
333 let is_super = resolutions
334 .iter()
335 .any(|r| matches!(r, Res::Builtin(b) if b.name() == sym::super_));
336
337 let contract_ids: Vec<ContractId> = if is_super {
338 bases.get(1..).unwrap_or_default().to_vec()
343 } else {
344 resolutions
345 .iter()
346 .filter_map(|res| {
347 if let Res::Item(ItemId::Contract(cid)) = res {
348 Some(*cid)
349 } else {
350 None
351 }
352 })
353 .collect()
354 };
355
356 contract_ids
357 .into_iter()
358 .flat_map(|cid| hir.contract(cid).all_functions())
359 .filter_map(|fid| {
360 let f = hir.function(fid);
361 f.name.is_some_and(|n| n == *method).then_some(f)
362 })
363 .collect()
364 } else {
365 vec![]
366 }
367 }
368 _ => vec![],
369 }
370}
371
372fn mark_storage_args<'hir>(
375 funcs: &[&Function<'hir>],
376 hir: &'hir Hir<'hir>,
377 args: &CallArgs<'hir>,
378 candidates: &HashSet<VariableId>,
379 writes: &mut HashSet<VariableId>,
380) {
381 if let CallArgsKind::Unnamed(_) = args.kind {
382 for (i, arg_expr) in args.exprs().enumerate() {
383 let any_storage = funcs.iter().any(|func| {
384 func.parameters.get(i).is_some_and(|&pid| {
385 matches!(hir.variable(pid).data_location, Some(DataLocation::Storage))
386 })
387 });
388 if any_storage {
389 collect_lvalue_writes(arg_expr, candidates, writes);
390 }
391 }
392 }
393
394 if let CallArgsKind::Named(named) = args.kind {
395 for named_arg in named {
396 let any_storage = funcs.iter().any(|func| {
397 let param = func
398 .parameters
399 .iter()
400 .find(|&&pid| hir.variable(pid).name.is_some_and(|n| n == named_arg.name));
401 param.is_some_and(|&pid| {
402 matches!(hir.variable(pid).data_location, Some(DataLocation::Storage))
403 })
404 });
405 if any_storage {
406 collect_lvalue_writes(&named_arg.value, candidates, writes);
407 }
408 }
409 }
410}
411
412fn collect_lvalue_writes(
413 expr: &Expr<'_>,
414 candidates: &HashSet<VariableId>,
415 writes: &mut HashSet<VariableId>,
416) {
417 match &expr.peel_parens().kind {
418 ExprKind::Ident([Res::Item(ItemId::Variable(id)), ..]) if candidates.contains(id) => {
419 writes.insert(*id);
420 }
421 ExprKind::Tuple(exprs) => {
422 for expr in exprs.iter().flatten() {
423 collect_lvalue_writes(expr, candidates, writes);
424 }
425 }
426 ExprKind::Index(base, _) | ExprKind::Slice(base, _, _) | ExprKind::Member(base, _) => {
427 collect_lvalue_writes(base, candidates, writes)
428 }
429 _ => {}
430 }
431}
432
433struct ReadVarCollector<'hir> {
434 hir: &'hir Hir<'hir>,
435 read: HashSet<VariableId>,
436}
437
438impl<'hir> Visit<'hir> for ReadVarCollector<'hir> {
439 type BreakValue = Never;
440
441 fn hir(&self) -> &'hir Hir<'hir> {
442 self.hir
443 }
444
445 fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
446 if let ExprKind::Ident(resolutions) = &expr.kind {
447 for res in *resolutions {
448 if let Res::Item(ItemId::Variable(var_id)) = res {
449 self.read.insert(*var_id);
450 }
451 }
452 }
453 self.walk_expr(expr)
454 }
455}