forge_lint/sol/gas/
var_read_using_this.rs1use super::VarReadUsingThis;
2use crate::{
3 linter::{LateLintPass, LintContext, Suggestion},
4 sol::{Severity, SolLint, analysis::is_builtin},
5};
6use solar::{
7 ast::{ContractKind, StateMutability},
8 interface::{Symbol, data_structures::Never, diagnostics::Applicability, sym},
9 sema::{
10 Gcx,
11 hir::{self, CallArgs, Expr, ExprId, ExprKind, Function, Stmt, StmtKind, Visit as _},
12 },
13};
14use std::ops::ControlFlow;
15
16declare_forge_lint!(
17 VAR_READ_USING_THIS,
18 Severity::Gas,
19 "var-read-using-this",
20 "call through `this` to a `view` or `pure` function incurs a `STATICCALL`"
21);
22
23impl<'gcx> LateLintPass<'gcx> for VarReadUsingThis {
24 fn check_nested_contract(
25 &mut self,
26 ctx: &LintContext,
27 gcx: Gcx<'gcx>,
28 contract_id: hir::ContractId,
29 ) {
30 let contract = gcx.hir.contract(contract_id);
31 if !matches!(contract.kind, ContractKind::Contract | ContractKind::AbstractContract) {
33 return;
34 }
35
36 let mut finder = ThisReadFinder { ctx, gcx, try_target: None };
37 for var_id in contract.variables() {
39 let _ = finder.visit_nested_var(var_id);
40 }
41 for fid in contract.all_functions() {
43 let _ = finder.visit_nested_function(fid);
44 }
45 }
46}
47
48struct ThisReadFinder<'a, 'gcx> {
49 ctx: &'a LintContext<'a, 'a>,
50 gcx: Gcx<'gcx>,
51 try_target: Option<ExprId>,
53}
54
55impl<'gcx> hir::Visit<'gcx> for ThisReadFinder<'_, 'gcx> {
56 type BreakValue = Never;
57
58 fn hir(&self) -> &'gcx hir::Hir<'gcx> {
59 &self.gcx.hir
60 }
61
62 fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Self::BreakValue> {
63 if let StmtKind::Try(try_stmt) = &stmt.kind {
64 self.try_target = Some(try_stmt.expr.id);
65 }
66 self.walk_stmt(stmt)
67 }
68
69 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
70 if self.try_target != Some(expr.id) {
71 self.check_call(expr);
72 }
73 self.walk_expr(expr)
74 }
75}
76
77impl ThisReadFinder<'_, '_> {
78 fn check_call(&self, expr: &Expr<'_>) {
81 let ExprKind::Call(callee, args, opts) = &expr.kind else { return };
82 let ExprKind::Member(base, member) = &callee.peel_parens().kind else { return };
83 if !is_builtin(self.gcx, base, sym::this) {
84 return;
85 }
86 let Some(function_id) = self.gcx.resolved_function(callee) else { return };
87 let func = self.gcx.hir.function(function_id);
88 if !matches!(func.state_mutability, StateMutability::View | StateMutability::Pure) {
89 return;
90 }
91 let suggestion =
94 if opts.is_some() { None } else { suggestion(self.ctx, func, member.name, args) };
95 match suggestion {
96 Some(suggestion) => {
97 self.ctx.emit_with_suggestion(&VAR_READ_USING_THIS, expr.span, suggestion);
98 }
99 None => self.ctx.emit(&VAR_READ_USING_THIS, expr.span),
100 }
101 }
102}
103
104fn suggestion(
105 ctx: &LintContext,
106 func: &Function<'_>,
107 name: Symbol,
108 args: &CallArgs<'_>,
109) -> Option<Suggestion> {
110 if !func.is_getter() {
111 return Some(
113 Suggestion::example(format!("call directly without `this.`: `{name}(...)`"))
114 .with_desc("avoid the `STATICCALL` by invoking the function directly"),
115 );
116 }
117 if func.returns.len() != 1 {
119 return Some(
120 Suggestion::example(format!("read the state variable directly: `{name}`"))
121 .with_desc("read the state variable directly instead of via `this.`"),
122 );
123 }
124 if args.is_empty() {
125 return Some(
126 Suggestion::fix(name.to_string(), Applicability::MachineApplicable)
127 .with_desc("consider reading the state variable directly"),
128 );
129 }
130 let mut indexed = name.to_string();
132 for arg in args.exprs() {
133 indexed += &format!("[{}]", ctx.span_to_snippet(arg.span)?.trim());
134 }
135 Some(
136 Suggestion::fix(indexed, Applicability::MaybeIncorrect)
137 .with_desc("consider accessing storage directly"),
138 )
139}