1use super::UninitializedStateVariables;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{
5 Severity, SolLint,
6 analysis::{referenced_item, tuple_elems},
7 },
8};
9use solar::{
10 ast::ContractKind,
11 interface::data_structures::Never,
12 sema::{
13 Gcx, Hir,
14 hir::{
15 Contract, ContractId, DataLocation, Expr, ExprKind, Function, Stmt, StmtKind, TypeKind,
16 VariableId, Visit,
17 },
18 },
19};
20use std::{
21 collections::{HashMap, HashSet},
22 ops::ControlFlow,
23};
24
25declare_forge_lint!(
26 UNINITIALIZED_STATE_VARIABLES,
27 Severity::Med,
28 "uninitialized-state",
29 "state variable is read but never written"
30);
31
32impl<'gcx> LateLintPass<'gcx> for UninitializedStateVariables {
33 fn check_nested_contract(
34 &mut self,
35 ctx: &LintContext,
36 gcx: Gcx<'gcx>,
37 contract_id: ContractId,
38 ) {
39 let contract = gcx.hir.contract(contract_id);
40 if matches!(contract.kind, ContractKind::Interface | ContractKind::AbstractContract)
43 || contract.linearization_failed()
44 {
45 return;
46 }
47
48 let bases = contract.linearized_bases;
51 let mut collector = Collector {
52 hir: &gcx.hir,
53 gcx,
54 read: HashSet::new(),
55 written: HashSet::new(),
56 aliases: HashMap::new(),
57 };
58 if bases.iter().any(|&cid| collector.visit_contract_items(gcx.hir.contract(cid)).is_break())
60 {
61 return;
62 }
63
64 for var_id in bases.iter().flat_map(|&cid| gcx.hir.contract(cid).variables()) {
65 let var = gcx.hir.variable(var_id);
66 if !var.is_constant()
67 && !var.is_immutable()
68 && !matches!(var.ty.kind, TypeKind::Mapping(_))
69 && var.initializer.is_none()
70 && collector.read.contains(&var_id)
71 && !collector.written.contains(&var_id)
72 {
73 ctx.emit(&UNINITIALIZED_STATE_VARIABLES, var.span);
74 }
75 }
76 }
77}
78
79struct Collector<'gcx> {
80 hir: &'gcx Hir<'gcx>,
81 gcx: Gcx<'gcx>,
82 read: HashSet<VariableId>,
83 written: HashSet<VariableId>,
84 aliases: Aliases,
86}
87
88type Aliases = HashMap<VariableId, HashSet<VariableId>>;
90
91impl<'gcx> Collector<'gcx> {
92 fn visit_contract_items(&mut self, contract: &'gcx Contract<'gcx>) -> ControlFlow<()> {
93 contract.all_functions().try_for_each(|fid| self.visit_nested_function(fid))?;
94 contract.variables().try_for_each(|vid| self.visit_nested_var(vid))?;
95 contract.bases_args.iter().try_for_each(|m| self.visit_modifier(m))
96 }
97
98 fn mark_written(&mut self, expr: &Expr<'_>) {
101 match &expr.peel_parens().kind {
102 ExprKind::Ident(_) => {
103 if let Some(id) = self.gcx.resolved_variable(expr) {
104 self.written.insert(id);
105 if let Some(targets) = self.aliases.get(&id) {
106 self.written.extend(targets);
107 }
108 }
109 }
110 ExprKind::Tuple(exprs) => exprs.iter().flatten().for_each(|e| self.mark_written(e)),
111 ExprKind::Index(base, _) | ExprKind::Slice(base, ..) | ExprKind::Member(base, _) => {
112 self.mark_written(base)
113 }
114 _ => {}
115 }
116 }
117
118 fn mark_storage_args(&mut self, call: &'gcx Expr<'gcx>) {
120 if let ExprKind::Call(callee, ..) = &call.kind
121 && let Some(parameters) =
122 self.gcx.type_of_expr(callee.id).and_then(|ty| ty.parameters())
123 {
124 for (index, ty) in parameters.iter().enumerate() {
125 if ty.is_ref_at(DataLocation::Storage)
126 && let Some(arg) = self.gcx.call_arg(call, index)
127 {
128 self.mark_written(arg);
129 }
130 }
131 }
132 }
133}
134
135impl<'gcx> Visit<'gcx> for Collector<'gcx> {
136 type BreakValue = ();
137
138 fn hir(&self) -> &'gcx Hir<'gcx> {
139 self.hir
140 }
141
142 fn visit_function(&mut self, func: &'gcx Function<'gcx>) -> ControlFlow<()> {
143 self.aliases = storage_aliases(self.gcx, func);
144 func.modifiers.iter().try_for_each(|m| self.visit_modifier(m))?;
145 func.body.iter().flat_map(|body| body.stmts).try_for_each(|stmt| self.visit_stmt(stmt))
146 }
147
148 fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<()> {
149 match stmt.kind {
150 StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) | StmtKind::Err(_) => {
151 ControlFlow::Break(())
152 }
153 _ => self.walk_stmt(stmt),
154 }
155 }
156
157 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<()> {
158 match &expr.kind {
159 ExprKind::Ident(_) => self.read.extend(self.gcx.resolved_variable(expr)),
160 ExprKind::Assign(lhs, ..) if !is_storage_pointer(self.gcx, lhs) => {
162 self.mark_written(lhs)
163 }
164 ExprKind::Delete(lhs) => self.mark_written(lhs),
165 ExprKind::Unary(op, lhs) if op.kind.has_side_effects() => self.mark_written(lhs),
166 ExprKind::Call(callee, ..) => {
167 if let ExprKind::Member(base, _) = &callee.kind {
170 self.mark_written(base);
171 }
172 self.mark_storage_args(expr);
173 }
174 _ => {}
175 }
176 self.walk_expr(expr)
177 }
178}
179
180fn storage_aliases<'gcx>(gcx: Gcx<'gcx>, func: &'gcx Function<'gcx>) -> Aliases {
184 let hir = &gcx.hir;
185 struct Edges<'gcx> {
186 gcx: Gcx<'gcx>,
187 hir: &'gcx Hir<'gcx>,
188 edges: HashMap<VariableId, HashSet<VariableId>>,
189 }
190
191 impl Edges<'_> {
192 fn record(&mut self, lhs: &Expr<'_>, rhs: &Expr<'_>) {
194 match (tuple_elems(lhs), tuple_elems(rhs)) {
195 (Some(targets), Some(values)) => {
196 for (target, value) in targets.iter().zip(values) {
197 if let (Some(target), Some(value)) = (target, value) {
198 self.record(target, value);
199 }
200 }
201 }
202 _ => {
203 if let Some(var) =
204 referenced_item(self.gcx, lhs).and_then(|id| id.as_variable())
205 {
206 self.record_var(var, rhs);
207 }
208 }
209 }
210 }
211
212 fn record_var(&mut self, var: VariableId, rhs: &Expr<'_>) {
213 if is_local_storage_var(self.hir, var) {
214 root_vars(self.gcx, rhs, self.edges.entry(var).or_default());
215 }
216 }
217 }
218
219 impl<'gcx> Visit<'gcx> for Edges<'gcx> {
220 type BreakValue = Never;
221
222 fn hir(&self) -> &'gcx Hir<'gcx> {
223 self.hir
224 }
225
226 fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Never> {
227 match &stmt.kind {
228 StmtKind::DeclSingle(var) => {
229 if let Some(init) = self.hir.variable(*var).initializer {
230 self.record_var(*var, init);
231 }
232 }
233 StmtKind::DeclMulti(vars, init) => {
234 for (var, value) in vars.iter().zip(tuple_elems(init).unwrap_or_default()) {
235 if let (Some(var), Some(value)) = (var, value) {
236 self.record_var(*var, value);
237 }
238 }
239 }
240 _ => {}
241 }
242 self.walk_stmt(stmt)
243 }
244
245 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
246 if let ExprKind::Assign(lhs, None, rhs) = &expr.kind {
247 self.record(lhs, rhs);
248 }
249 self.walk_expr(expr)
250 }
251 }
252
253 let mut edges = Edges { gcx, hir, edges: HashMap::new() };
254 let _ = edges.visit_function(func);
255 let edges = edges.edges;
256 let mut aliases = Aliases::new();
257 for &pointer in edges.keys() {
258 let (mut targets, mut seen, mut stack) = (HashSet::new(), HashSet::new(), vec![pointer]);
259 while let Some(var) = stack.pop() {
260 if !seen.insert(var) {
261 continue;
262 }
263 if hir.variable(var).kind.is_state() {
264 targets.insert(var);
265 } else if let Some(roots) = edges.get(&var) {
266 stack.extend(roots);
267 }
268 }
269 if !targets.is_empty() {
270 aliases.insert(pointer, targets);
271 }
272 }
273 aliases
274}
275
276fn root_vars(gcx: Gcx<'_>, expr: &Expr<'_>, roots: &mut HashSet<VariableId>) {
278 match &expr.peel_parens().kind {
279 ExprKind::Ident(_) => roots.extend(gcx.resolved_variable(expr)),
280 ExprKind::Index(base, _) | ExprKind::Slice(base, ..) | ExprKind::Member(base, _) => {
281 root_vars(gcx, base, roots)
282 }
283 ExprKind::Ternary(_, then, otherwise) => {
284 root_vars(gcx, then, roots);
285 root_vars(gcx, otherwise, roots);
286 }
287 _ => {}
288 }
289}
290
291fn is_storage_pointer(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
293 referenced_item(gcx, expr)
294 .and_then(|id| id.as_variable())
295 .is_some_and(|var| is_local_storage_var(&gcx.hir, var))
296}
297
298fn is_local_storage_var(hir: &Hir<'_>, var: VariableId) -> bool {
299 let var = hir.variable(var);
300 !var.kind.is_state() && var.data_location == Some(DataLocation::Storage)
301}