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, write_target},
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 if let Some(lvalue) = write_target(expr) {
152 for_each_lhs_var(self.gcx, lvalue, &mut |v| {
153 self.writes.insert(v);
154 });
155 }
156 self.walk_expr(expr)
157 }
158}
159
160fn is_compile_time_constant(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
161 let is_const = |e: &Expr<'_>| is_compile_time_constant(gcx, e);
162 match &expr.kind {
163 ExprKind::Lit(_) | ExprKind::Type(_) | ExprKind::TypeCall(_) => true,
164 ExprKind::Ident(_) => {
165 gcx.resolved_variable(expr).is_some_and(|v| gcx.hir.variable(v).is_constant())
166 }
167 ExprKind::Unary(op, inner) => !op.kind.has_side_effects() && is_const(inner),
168 ExprKind::Binary(lhs, _, rhs) => is_const(lhs) && is_const(rhs),
169 ExprKind::Ternary(c, t, f) => is_const(c) && is_const(t) && is_const(f),
170 ExprKind::Tuple(exprs) => exprs.iter().flatten().all(|e| is_const(e)),
171 ExprKind::Call(callee, args) => {
172 let (callee, opts) = callee.split_call_options();
173 is_constant_call(gcx, callee)
174 && args.exprs().all(is_const)
175 && opts.is_none_or(|opts| opts.args.iter().all(|arg| is_const(&arg.value)))
176 }
177 ExprKind::Member(base, member) => match (&base.kind, member.name) {
180 (ExprKind::TypeCall(ty), sym::min | sym::max) => matches!(
181 ty.kind,
182 TypeKind::Elementary(ElementaryType::Int(_) | ElementaryType::UInt(_))
183 | TypeKind::Custom(ItemId::Enum(_))
184 ),
185 (ExprKind::TypeCall(ty), sym::interfaceId) => matches!(
186 ty.kind,
187 TypeKind::Custom(ItemId::Contract(cid))
188 if gcx.hir.contract(cid).kind == ContractKind::Interface
189 ),
190 _ => false,
191 },
192 _ => false,
193 }
194}
195
196fn is_constant_call(gcx: Gcx<'_>, callee: &Expr<'_>) -> bool {
198 matches!(callee.kind, ExprKind::Type(_))
199 || is_contract_cast(gcx, callee)
200 || gcx.resolved_builtin(callee).is_some_and(|b| {
201 matches!(
202 b.name(),
203 kw::Keccak256
204 | kw::Addmod
205 | kw::Mulmod
206 | sym::sha256
207 | sym::ripemd160
208 | sym::ecrecover
209 )
210 })
211}