Skip to main content

forge_lint/sol/gas/
var_read_using_this.rs

1use 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        // `this` only exists in (abstract) contracts: libraries have none, interfaces no bodies.
32        if !matches!(contract.kind, ContractKind::Contract | ContractKind::AbstractContract) {
33            return;
34        }
35
36        let mut finder = ThisReadFinder { ctx, gcx, try_target: None };
37        // State variable initializers run in the synthesized constructor.
38        for var_id in contract.variables() {
39            let _ = finder.visit_nested_var(var_id);
40        }
41        // Only bodies defined in this contract; inherited ones are walked with their own contract.
42        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    /// The expression tried by the enclosing `try` statement, which must stay an external call.
52    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    /// Flags `this.<name>(args)` when `<name>` resolves to a `view`/`pure` function of the
79    /// current contract.
80    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        // With call options like `{gas: ...}` the external call is deliberate: flag the gas waste
92        // without an auto-fix.
93        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        // Ordinary `view`/`pure` functions may be `external`, requiring a refactor to call them.
112        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    // Struct getters destructure their fields, so a direct read is not equivalent.
118    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    // Mapping/array getter: `name[arg1][arg2]...`.
131    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}