1use super::UnchangedStateVariables;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{
5 Severity, SolLint,
6 analysis::{for_each_lhs_var, is_contract_cast, loop_stmts},
7 },
8};
9use solar::{
10 ast::{ContractKind, ElementaryType},
11 interface::{data_structures::Never, kw, sym},
12 sema::{
13 Gcx,
14 hir::{self, Expr, ExprKind, ItemId, Stmt, StmtKind, TypeKind, VariableId, Visit as _},
15 },
16};
17use std::{collections::HashSet, ops::ControlFlow};
18
19declare_forge_lint!(
20 COULD_BE_IMMUTABLE,
21 Severity::Gas,
22 "could-be-immutable",
23 "state variable could be declared `immutable`"
24);
25
26declare_forge_lint!(
27 COULD_BE_CONSTANT,
28 Severity::Gas,
29 "could-be-constant",
30 "state variable could be declared `constant`"
31);
32
33impl<'gcx> LateLintPass<'gcx> for UnchangedStateVariables {
34 fn check_nested_contract(
35 &mut self,
36 ctx: &LintContext,
37 gcx: Gcx<'gcx>,
38 contract_id: hir::ContractId,
39 ) {
40 let contract = gcx.hir.contract(contract_id);
41 if contract.kind == ContractKind::Interface
43 || gcx.hir.contracts().any(|c| c.linearized_bases[1..].contains(&contract_id))
44 {
45 return;
46 }
47
48 let candidates = contract
51 .linearized_bases
52 .iter()
53 .flat_map(|&id| gcx.hir.contract(id).variables())
54 .filter(|&id| {
55 let var = gcx.hir.variable(id);
56 var.mutability.is_none()
57 && matches!(
58 var.ty.kind,
59 TypeKind::Elementary(_) | TypeKind::Custom(ItemId::Contract(_))
60 )
61 });
62 let functions = contract
63 .linearized_bases
64 .iter()
65 .flat_map(|&id| gcx.hir.contract(id).all_functions())
66 .map(|id| gcx.hir.function(id));
67
68 if functions
70 .clone()
71 .any(|f| f.body.is_some_and(|body| body.stmts.iter().any(has_assembly_or_unknown)))
72 {
73 return;
74 }
75
76 let mut initializer_writes = WriteCollector { gcx, writes: HashSet::new() };
79 for id in candidates.clone() {
80 if let Some(init) = gcx.hir.variable(id).initializer {
81 let _ = initializer_writes.visit_expr(init);
82 }
83 }
84 let mut constructor_writes = WriteCollector { gcx, writes: HashSet::new() };
86 let mut runtime_writes = WriteCollector { gcx, writes: HashSet::new() };
87 for function in functions {
88 let collector = if function.is_constructor() {
89 &mut constructor_writes
90 } else {
91 &mut runtime_writes
92 };
93 let _ = collector.visit_function(function);
94 }
95
96 for var_id in candidates {
97 if runtime_writes.writes.contains(&var_id) {
98 continue;
99 }
100 let var = gcx.hir.variable(var_id);
101 let span = var.name.map_or(var.span, |name| name.span);
102 let constant_initializer =
103 var.initializer.is_some_and(|expr| is_compile_time_constant(gcx, expr));
104 let written_in_constructor = constructor_writes.writes.contains(&var_id);
105 let immutable_type = gcx.type_of_item(var_id.into()).is_value_type();
106 if constant_initializer
107 && !written_in_constructor
108 && !initializer_writes.writes.contains(&var_id)
109 {
110 ctx.emit(&COULD_BE_CONSTANT, span);
111 } else if immutable_type
112 && (written_in_constructor || (var.initializer.is_some() && !constant_initializer))
113 {
114 ctx.emit(&COULD_BE_IMMUTABLE, span);
115 }
116 }
117 }
118}
119
120fn has_assembly_or_unknown(stmt: &Stmt<'_>) -> bool {
121 match &stmt.kind {
122 StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) | StmtKind::Err(_) => true,
123 StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => {
124 b.stmts.iter().any(has_assembly_or_unknown)
125 }
126 StmtKind::Loop(b, source) => loop_stmts(*b, *source).any(has_assembly_or_unknown),
127 StmtKind::If(_, t, e) => {
128 has_assembly_or_unknown(t) || e.is_some_and(has_assembly_or_unknown)
129 }
130 StmtKind::Try(t) => {
131 t.clauses.iter().any(|c| c.block.stmts.iter().any(has_assembly_or_unknown))
132 }
133 _ => false,
134 }
135}
136
137struct WriteCollector<'gcx> {
139 gcx: Gcx<'gcx>,
140 writes: HashSet<VariableId>,
141}
142
143impl<'gcx> hir::Visit<'gcx> for WriteCollector<'gcx> {
144 type BreakValue = Never;
145
146 fn hir(&self) -> &'gcx hir::Hir<'gcx> {
147 &self.gcx.hir
148 }
149
150 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
151 let lvalue = match &expr.kind {
152 ExprKind::Assign(lhs, ..) | ExprKind::Delete(lhs) => Some(lhs),
153 ExprKind::Unary(op, inner) if op.kind.has_side_effects() => Some(inner),
154 _ => None,
155 };
156 if let Some(lvalue) = lvalue {
157 for_each_lhs_var(self.gcx, lvalue, &mut |v| {
158 self.writes.insert(v);
159 });
160 }
161 self.walk_expr(expr)
162 }
163}
164
165fn is_compile_time_constant(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
166 let is_const = |e: &Expr<'_>| is_compile_time_constant(gcx, e);
167 match &expr.kind {
168 ExprKind::Lit(_) | ExprKind::Type(_) | ExprKind::TypeCall(_) => true,
169 ExprKind::Ident(_) => {
170 gcx.resolved_variable(expr).is_some_and(|v| gcx.hir.variable(v).is_constant())
171 }
172 ExprKind::Unary(op, inner) => !op.kind.has_side_effects() && is_const(inner),
173 ExprKind::Binary(lhs, _, rhs) => is_const(lhs) && is_const(rhs),
174 ExprKind::Ternary(c, t, f) => is_const(c) && is_const(t) && is_const(f),
175 ExprKind::Tuple(exprs) => exprs.iter().flatten().all(|e| is_const(e)),
176 ExprKind::Call(callee, args, opts) => {
177 is_constant_call(gcx, callee)
178 && args.exprs().all(is_const)
179 && opts.is_none_or(|opts| opts.args.iter().all(|arg| is_const(&arg.value)))
180 }
181 ExprKind::Member(base, member) => match (&base.kind, member.name) {
184 (ExprKind::TypeCall(ty), sym::min | sym::max) => matches!(
185 ty.kind,
186 TypeKind::Elementary(ElementaryType::Int(_) | ElementaryType::UInt(_))
187 | TypeKind::Custom(ItemId::Enum(_))
188 ),
189 (ExprKind::TypeCall(ty), sym::interfaceId) => matches!(
190 ty.kind,
191 TypeKind::Custom(ItemId::Contract(cid))
192 if gcx.hir.contract(cid).kind == ContractKind::Interface
193 ),
194 _ => false,
195 },
196 _ => false,
197 }
198}
199
200fn is_constant_call(gcx: Gcx<'_>, callee: &Expr<'_>) -> bool {
202 matches!(callee.kind, ExprKind::Type(_))
203 || is_contract_cast(gcx, callee)
204 || gcx.resolved_builtin(callee).is_some_and(|b| {
205 matches!(
206 b.name(),
207 kw::Keccak256
208 | kw::Addmod
209 | kw::Mulmod
210 | sym::sha256
211 | sym::ripemd160
212 | sym::ecrecover
213 )
214 })
215}