1use super::VarReadUsingThis;
2use crate::{
3 linter::{LateLintPass, LintContext, Suggestion},
4 sol::{Severity, SolLint},
5};
6use solar::{
7 ast::{ContractKind, StateMutability},
8 interface::{Symbol, diagnostics::Applicability, sym},
9 sema::hir::{self, ExprKind, Res, StmtKind},
10};
11use std::collections::HashMap;
12
13declare_forge_lint!(
14 VAR_READ_USING_THIS,
15 Severity::Gas,
16 "var-read-using-this",
17 "reading a state variable via `this` causes an unnecessary STATICCALL; access it directly"
18);
19
20impl<'hir> LateLintPass<'hir> for VarReadUsingThis {
21 fn check_nested_contract(
22 &mut self,
23 ctx: &LintContext,
24 hir: &'hir hir::Hir<'hir>,
25 contract_id: hir::ContractId,
26 ) {
27 let contract = hir.contract(contract_id);
28
29 if !matches!(contract.kind, ContractKind::Contract | ContractKind::AbstractContract) {
32 return;
33 }
34
35 let mut callable: HashMap<Symbol, Vec<&'hir hir::Function<'hir>>> = HashMap::new();
40 for &cid in contract.linearized_bases {
41 for fid in hir.contract(cid).functions() {
42 let func = hir.function(fid);
43 let Some(name) = func.name else { continue };
44 if !func.is_part_of_external_interface() {
45 continue;
46 }
47 callable.entry(name.name).or_default().push(func);
48 }
49 }
50
51 if callable.is_empty() {
52 return;
53 }
54
55 for var_id in contract.variables() {
57 let var = hir.variable(var_id);
58 if let Some(init) = var.initializer {
59 walk_expr(ctx, init, &callable);
60 }
61 }
62
63 for fid in contract.all_functions() {
66 let func = hir.function(fid);
67
68 for modifier in func.modifiers {
71 for arg in modifier.args.exprs() {
72 walk_expr(ctx, arg, &callable);
73 }
74 }
75
76 if let Some(body) = func.body {
77 walk_block(ctx, hir, body, &callable);
78 }
79 }
80 }
81}
82
83fn walk_block<'hir>(
84 ctx: &LintContext,
85 hir: &'hir hir::Hir<'hir>,
86 block: hir::Block<'hir>,
87 callable: &HashMap<Symbol, Vec<&'hir hir::Function<'hir>>>,
88) {
89 for stmt in block.stmts {
90 walk_stmt(ctx, hir, stmt, callable);
91 }
92}
93
94fn walk_stmt<'hir>(
95 ctx: &LintContext,
96 hir: &'hir hir::Hir<'hir>,
97 stmt: &'hir hir::Stmt<'hir>,
98 callable: &HashMap<Symbol, Vec<&'hir hir::Function<'hir>>>,
99) {
100 match &stmt.kind {
101 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
102 walk_block(ctx, hir, *block, callable);
103 }
104 StmtKind::If(cond, then_stmt, else_stmt) => {
105 walk_expr(ctx, cond, callable);
106 walk_stmt(ctx, hir, then_stmt, callable);
107 if let Some(else_stmt) = else_stmt {
108 walk_stmt(ctx, hir, else_stmt, callable);
109 }
110 }
111 StmtKind::Try(stmt_try) => {
112 let try_expr = &stmt_try.expr;
117 if let ExprKind::Call(callee, args, named_args) = &try_expr.kind {
118 walk_expr(ctx, callee, callable);
119 for e in args.exprs() {
120 walk_expr(ctx, e, callable);
121 }
122 if let Some(nargs) = named_args {
123 for arg in *nargs {
124 walk_expr(ctx, &arg.value, callable);
125 }
126 }
127 } else {
128 walk_expr(ctx, try_expr, callable);
129 }
130 for clause in stmt_try.clauses {
131 walk_block(ctx, hir, clause.block, callable);
132 }
133 }
134 StmtKind::DeclSingle(var_id) => {
135 if let Some(init) = hir.variable(*var_id).initializer {
136 walk_expr(ctx, init, callable);
137 }
138 }
139 StmtKind::DeclMulti(_, expr)
140 | StmtKind::Emit(expr)
141 | StmtKind::Revert(expr)
142 | StmtKind::Return(Some(expr))
143 | StmtKind::Expr(expr) => walk_expr(ctx, expr, callable),
144 StmtKind::Return(None)
145 | StmtKind::Break
146 | StmtKind::Continue
147 | StmtKind::Placeholder
148 | StmtKind::Err(_) => {}
149 }
150}
151
152fn walk_expr<'hir>(
153 ctx: &LintContext,
154 expr: &'hir hir::Expr<'hir>,
155 callable: &HashMap<Symbol, Vec<&'hir hir::Function<'hir>>>,
156) {
157 if let Some(matched) = match_this_call(expr, callable) {
160 emit(ctx, expr, &matched);
161 }
162
163 match &expr.kind {
164 ExprKind::Array(exprs) => {
165 for e in *exprs {
166 walk_expr(ctx, e, callable);
167 }
168 }
169 ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
170 walk_expr(ctx, lhs, callable);
171 walk_expr(ctx, rhs, callable);
172 }
173 ExprKind::Call(callee, args, named_args) => {
174 walk_expr(ctx, callee, callable);
175 for e in args.exprs() {
176 walk_expr(ctx, e, callable);
177 }
178 if let Some(nargs) = named_args {
179 for arg in *nargs {
180 walk_expr(ctx, &arg.value, callable);
181 }
182 }
183 }
184 ExprKind::Delete(inner) | ExprKind::Payable(inner) | ExprKind::Unary(_, inner) => {
185 walk_expr(ctx, inner, callable);
186 }
187 ExprKind::Index(base, index) => {
188 walk_expr(ctx, base, callable);
189 if let Some(i) = index {
190 walk_expr(ctx, i, callable);
191 }
192 }
193 ExprKind::Slice(base, start, end) => {
194 walk_expr(ctx, base, callable);
195 if let Some(s) = start {
196 walk_expr(ctx, s, callable);
197 }
198 if let Some(e) = end {
199 walk_expr(ctx, e, callable);
200 }
201 }
202 ExprKind::Member(base, _) => walk_expr(ctx, base, callable),
203 ExprKind::Ternary(cond, then_e, else_e) => {
204 walk_expr(ctx, cond, callable);
205 walk_expr(ctx, then_e, callable);
206 walk_expr(ctx, else_e, callable);
207 }
208 ExprKind::Tuple(exprs) => {
209 for e in exprs.iter().flatten() {
210 walk_expr(ctx, e, callable);
211 }
212 }
213 ExprKind::Ident(_)
214 | ExprKind::Lit(_)
215 | ExprKind::New(_)
216 | ExprKind::TypeCall(_)
217 | ExprKind::Type(_)
218 | ExprKind::Err(_) => {}
219 }
220}
221
222#[derive(Clone, Copy)]
223struct MatchedCall<'hir> {
224 func: &'hir hir::Function<'hir>,
225 args: hir::CallArgs<'hir>,
226 has_call_options: bool,
228 member_name: Symbol,
230}
231
232fn match_this_call<'hir>(
236 expr: &'hir hir::Expr<'hir>,
237 callable: &HashMap<Symbol, Vec<&'hir hir::Function<'hir>>>,
238) -> Option<MatchedCall<'hir>> {
239 let ExprKind::Call(callee, args, named_args) = &expr.kind else { return None };
240
241 let ExprKind::Member(base, member) = &callee.peel_parens().kind else { return None };
243
244 let ExprKind::Ident(resolutions) = &base.peel_parens().kind else { return None };
246 let is_this = resolutions.iter().any(|r| matches!(r, Res::Builtin(b) if b.name() == sym::this));
247 if !is_this {
248 return None;
249 }
250
251 let candidates = callable.get(&member.name)?;
252 let arity = args.len();
253
254 let mut found: Option<&'hir hir::Function<'hir>> = None;
259 for f in candidates.iter().copied() {
260 if f.parameters.len() != arity {
261 continue;
262 }
263 if !matches!(f.state_mutability, StateMutability::View | StateMutability::Pure) {
264 return None;
265 }
266 if found.is_none() {
267 found = Some(f);
268 }
269 }
270 let func = found?;
271
272 Some(MatchedCall {
273 func,
274 args: *args,
275 has_call_options: named_args.is_some(),
276 member_name: member.name,
277 })
278}
279
280fn emit(ctx: &LintContext, expr: &hir::Expr<'_>, matched: &MatchedCall<'_>) {
281 if matched.has_call_options {
284 ctx.emit(&VAR_READ_USING_THIS, expr.span);
285 return;
286 }
287
288 if let Some(suggestion) = build_suggestion(ctx, matched) {
289 ctx.emit_with_suggestion(&VAR_READ_USING_THIS, expr.span, suggestion);
290 } else {
291 ctx.emit(&VAR_READ_USING_THIS, expr.span);
292 }
293}
294
295fn build_suggestion(ctx: &LintContext, matched: &MatchedCall<'_>) -> Option<Suggestion> {
296 let name = matched.member_name.as_str();
297
298 if matched.func.is_getter() {
299 if matched.func.returns.len() != 1 {
302 return Some(
303 Suggestion::example(format!("read the state variable directly: `{name}`"))
304 .with_desc("read the state variable directly instead of via `this.`"),
305 );
306 }
307
308 if matched.args.is_empty() {
309 return Some(
311 Suggestion::fix(name.to_string(), Applicability::MachineApplicable)
312 .with_desc("consider reading the state variable directly"),
313 );
314 }
315
316 let mut indexed = String::from(name);
318 for arg in matched.args.exprs() {
319 let snippet = ctx.span_to_snippet(arg.span)?;
320 indexed.push('[');
321 indexed.push_str(snippet.trim());
322 indexed.push(']');
323 }
324 return Some(
325 Suggestion::fix(indexed, Applicability::MaybeIncorrect)
326 .with_desc("consider accessing storage directly"),
327 );
328 }
329
330 Some(
333 Suggestion::example(format!("call directly without `this.`: `{name}(...)`"))
334 .with_desc("avoid the STATICCALL by invoking the function directly"),
335 )
336}