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