Skip to main content

forge_lint/sol/analysis/
exprs.rs

1//! Expression-shape probes over Solar HIR (and a few AST-level ones).
2
3use solar::{
4    ast::{self, LitKind, UnOpKind},
5    interface::{Symbol, kw, sym},
6    sema::{
7        Gcx,
8        builtins::Builtin,
9        hir::{
10            self, CallArgs, ElementaryType, Expr, ExprKind, FunctionId, ItemId, Res, TypeKind,
11            VariableId,
12        },
13        ty::{CallableParamSource, TyKind},
14    },
15};
16use std::ops::ControlFlow;
17
18/// True if `expr` resolves to the given builtin name.
19pub fn is_builtin(gcx: Gcx<'_>, expr: &Expr<'_>, name: Symbol) -> bool {
20    gcx.resolved_builtin(expr).is_some_and(|builtin| builtin.name() == name)
21}
22
23/// `msg.sender`.
24pub fn is_msg_sender(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
25    gcx.resolved_builtin(expr) == Some(Builtin::MsgSender)
26}
27
28/// `msg.sender` or `tx.origin`.
29pub fn is_sender_member(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
30    matches!(gcx.resolved_builtin(expr), Some(Builtin::MsgSender | Builtin::TxOrigin))
31}
32
33/// True if `callee` resolves to the builtin `require` or `assert`.
34pub fn is_require_or_assert(gcx: Gcx<'_>, callee: &Expr<'_>) -> bool {
35    matches!(gcx.resolved_builtin(callee), Some(Builtin::Require | Builtin::Assert))
36}
37
38/// `revert(...)`, `revert Err(...)`-style builtin revert call (any form).
39pub fn is_revert_call(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
40    matches!(&expr.peel_parens().kind, ExprKind::Call(callee, ..) if is_builtin(gcx, callee, kw::Revert))
41}
42
43/// A literal zero/false or an elementary cast or arithmetic negation of one.
44pub fn is_zero_value(expr: &Expr<'_>) -> bool {
45    match &expr.peel_parens().kind {
46        ExprKind::Lit(lit) => match &lit.kind {
47            LitKind::Number(value) => value.is_zero(),
48            LitKind::Address(value) => value.is_zero(),
49            LitKind::Bool(value) => !value,
50            _ => false,
51        },
52        ExprKind::Call(callee, args, _) if cast_type(callee).is_some() => {
53            let mut exprs = args.exprs();
54            exprs.len() == 1 && exprs.next().is_some_and(is_zero_value)
55        }
56        ExprKind::Payable(inner) => is_zero_value(inner),
57        ExprKind::Unary(op, inner) if op.kind == UnOpKind::Neg => is_zero_value(inner),
58        _ => false,
59    }
60}
61
62/// `revert(...)`, `selfdestruct(...)`, `require(false, ...)` or `assert(false)`.
63pub fn is_exit_call(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
64    let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return false };
65    match gcx.resolved_builtin(callee) {
66        Some(Builtin::Revert | Builtin::RevertMsg | Builtin::Selfdestruct) => true,
67        Some(Builtin::Require | Builtin::Assert) => {
68            args.exprs().next().is_some_and(is_literal_false)
69        }
70        _ => false,
71    }
72}
73
74/// The boolean literal `false`.
75pub fn is_literal_false(expr: &Expr<'_>) -> bool {
76    matches!(&expr.peel_parens().kind, ExprKind::Lit(lit) if matches!(lit.kind, LitKind::Bool(false)))
77}
78
79/// The integer literal `0`.
80pub fn is_literal_zero(expr: &Expr<'_>) -> bool {
81    matches!(&expr.peel_parens().kind, ExprKind::Lit(lit)
82        if matches!(&lit.kind, LitKind::Number(n) if n.is_zero()))
83}
84
85/// `address(...)` / `address payable(...)` cast head.
86pub fn is_address_cast(callee: &Expr<'_>) -> bool {
87    matches!(
88        &callee.peel_parens().kind,
89        ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ElementaryType::Address(_)), .. })
90    )
91}
92
93/// `IFoo(...)` contract / interface cast head.
94pub fn is_contract_cast(gcx: Gcx<'_>, callee: &Expr<'_>) -> bool {
95    gcx.type_of_expr(callee.peel_parens().id).is_some_and(
96        |ty| matches!(ty.kind, TyKind::Type(inner) if matches!(inner.kind, TyKind::Contract(_))),
97    )
98}
99
100/// `address(...)` or `IFoo(...)` cast head.
101pub fn is_address_like_cast(gcx: Gcx<'_>, callee: &Expr<'_>) -> bool {
102    is_address_cast(callee) || is_contract_cast(gcx, callee)
103}
104
105/// `address(this)`, `payable(this)`, `IFoo(this)`, `IFoo(address(this))`, or bare `this`.
106pub fn is_address_self(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
107    let expr = expr.peel_parens();
108    match &expr.kind {
109        ExprKind::Payable(inner) => is_address_self(gcx, inner),
110        ExprKind::Call(callee, args, _) if is_address_like_cast(gcx, callee) => {
111            args.exprs().next().is_some_and(|expr| is_address_self(gcx, expr))
112        }
113        _ => is_builtin(gcx, expr, sym::this),
114    }
115}
116
117/// The variable a bare identifier refers to, looking through parens, `payable(...)` and
118/// address-like casts (`address(x)`, `IFoo(x)`).
119pub fn underlying_var(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<VariableId> {
120    match &expr.peel_parens().kind {
121        ExprKind::Ident(_) => gcx.resolved_variable(expr),
122        ExprKind::Call(callee, args, _) if is_address_like_cast(gcx, callee) => {
123            args.exprs().next().and_then(|arg| underlying_var(gcx, arg))
124        }
125        ExprKind::Payable(inner) => underlying_var(gcx, inner),
126        _ => None,
127    }
128}
129
130/// The local (non-state) variable a bare identifier refers to.
131pub fn lhs_local_var(gcx: Gcx<'_>, lhs: &Expr<'_>) -> Option<VariableId> {
132    let ExprKind::Ident(_) = lhs.peel_parens().kind else { return None };
133    gcx.resolved_variable(lhs).filter(|&v| !gcx.hir.variable(v).kind.is_state())
134}
135
136/// State variables written by an lvalue: peels index/slice/member/payable/unary/delete wrappers
137/// and tuple destructuring. Duplicates are removed.
138pub fn state_lhs_vars(gcx: Gcx<'_>, lhs: &Expr<'_>) -> Vec<VariableId> {
139    let mut vars = Vec::new();
140    for_each_lhs_var(gcx, lhs, &mut |v| {
141        if gcx.hir.variable(v).kind.is_state() && !vars.contains(&v) {
142            vars.push(v);
143        }
144    });
145    vars
146}
147
148/// Calls `f` for each resolved variable at the root of an lvalue, peeling
149/// index/slice/member/payable/unary/delete wrappers and tuple destructuring.
150pub fn for_each_lhs_var(gcx: Gcx<'_>, expr: &Expr<'_>, f: &mut impl FnMut(VariableId)) {
151    match &expr.peel_parens().kind {
152        ExprKind::Ident(_) => {
153            if let Some(var) = gcx.resolved_variable(expr) {
154                f(var);
155            }
156        }
157        ExprKind::Index(base, _)
158        | ExprKind::Slice(base, ..)
159        | ExprKind::Member(base, _)
160        | ExprKind::Payable(base)
161        | ExprKind::Unary(_, base)
162        | ExprKind::Delete(base) => for_each_lhs_var(gcx, base, f),
163        ExprKind::Tuple(exprs) => exprs.iter().flatten().for_each(|e| for_each_lhs_var(gcx, e, f)),
164        _ => {}
165    }
166}
167
168/// The elements of a tuple expression (through parens).
169pub fn tuple_elems<'gcx>(expr: &'gcx Expr<'gcx>) -> Option<&'gcx [Option<&'gcx Expr<'gcx>>]> {
170    match &expr.peel_parens().kind {
171        ExprKind::Tuple(elems) => Some(elems),
172        _ => None,
173    }
174}
175
176/// The argument bound to `param` of `function_id` in `args`, positional or named.
177pub fn arg_for_param<'gcx>(
178    gcx: Gcx<'gcx>,
179    function_id: FunctionId,
180    param: VariableId,
181    args: &CallArgs<'gcx>,
182) -> Option<&'gcx Expr<'gcx>> {
183    let function = gcx.hir.function(function_id);
184    let idx = function.parameters.iter().position(|p| *p == param)?;
185    let names = gcx.callable_param_names(CallableParamSource::Function {
186        id: function_id,
187        skips_receiver: false,
188    });
189    args.argument_for_parameter(idx, Some(&names))
190}
191
192/// The function an internal call made from within `contract_id` dispatches to: a virtual call
193/// resolves to the most derived override, `super.f` to the next base implementation, and a
194/// qualified `Base.f` to that declaration exactly. `None` for external and unresolved callees.
195pub fn dispatched_function(
196    gcx: Gcx<'_>,
197    contract_id: hir::ContractId,
198    callee: &Expr<'_>,
199) -> Option<FunctionId> {
200    let callee = callee.peel_parens();
201    let function_id = gcx.resolved_callee(callee.id)?.res.as_function()?;
202    match &callee.kind {
203        ExprKind::Member(base, _) => {
204            let solar::sema::ty::TyKind::Type(ty) = gcx.type_of_expr(base.id)?.kind else {
205                return None;
206            };
207            match ty.kind {
208                solar::sema::ty::TyKind::Contract(_) => Some(function_id),
209                solar::sema::ty::TyKind::Super(defining) => {
210                    Some(gcx.resolve_super_function(contract_id, defining, function_id))
211                }
212                _ => None,
213            }
214        }
215        ExprKind::Ident(_) => Some(gcx.resolve_virtual_function(contract_id, function_id)),
216        _ => None,
217    }
218}
219
220/// The item a bare identifier refers to, if any.
221pub fn referenced_item(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<ItemId> {
222    let ExprKind::Ident(_) = expr.peel_parens().kind else { return None };
223    match gcx.resolved_expr(expr)? {
224        Res::Item(id) => Some(id),
225        _ => None,
226    }
227}
228
229/// Receiver of `<expr>.{call,delegatecall,transfer,send}` (value-bearing sinks), including the
230/// `.call{value: x}(...)` option form.
231pub fn address_call_receiver<'a>(callee: &'a Expr<'a>) -> Option<&'a Expr<'a>> {
232    let inner = match &callee.kind {
233        ExprKind::Call(inner, ..) if matches!(inner.kind, ExprKind::Member(..)) => inner,
234        _ => callee,
235    };
236    let ExprKind::Member(receiver, name) = &inner.kind else { return None };
237    matches!(name.name, kw::Call | kw::Delegatecall | sym::transfer | sym::send).then_some(receiver)
238}
239
240/// True if a HIR call carries an explicit `gas:` option.
241pub fn is_call_with_gas_limit(expr: &Expr<'_>) -> bool {
242    matches!(&expr.peel_parens().kind, ExprKind::Call(_, _, Some(opts))
243        if opts.args.iter().any(|opt| opt.name.name == kw::Gas))
244}
245
246/// AST-level: `target.call(...)`, `.delegatecall(...)`, `.staticcall(...)`, with or without
247/// `{value: x}` options.
248pub const fn is_low_level_call(expr: &ast::Expr<'_>) -> bool {
249    if let ast::ExprKind::Call(call_expr, _) = &expr.kind {
250        let callee = match &call_expr.kind {
251            ast::ExprKind::CallOptions(inner, _) => inner,
252            _ => call_expr,
253        };
254        if let ast::ExprKind::Member(_, member) = &callee.kind {
255            return matches!(member.name, kw::Call | kw::Delegatecall | kw::Staticcall);
256        }
257    }
258    false
259}
260
261/// The lvalue written by an assignment, `delete` or increment/decrement expression.
262pub const fn write_target<'gcx>(expr: &'gcx Expr<'gcx>) -> Option<&'gcx Expr<'gcx>> {
263    match &expr.kind {
264        ExprKind::Assign(target, ..) | ExprKind::Delete(target) => Some(target),
265        ExprKind::Unary(op, target) if op.kind.has_side_effects() => Some(target),
266        _ => None,
267    }
268}
269
270/// The functions reachable through the runtime dispatch of a most-derived contract: its
271/// interface functions plus the inherited `fallback`/`receive`, if any.
272pub fn runtime_entry_points(gcx: Gcx<'_>, contract_id: hir::ContractId) -> Vec<FunctionId> {
273    let bases = gcx.hir.contract(contract_id).linearized_bases;
274    let mut entries: Vec<_> =
275        gcx.interface_functions(contract_id).all().iter().map(|f| f.id).collect();
276    entries.extend(bases.iter().find_map(|&cid| gcx.hir.contract(cid).fallback));
277    entries.extend(bases.iter().find_map(|&cid| gcx.hir.contract(cid).receive));
278    entries
279}
280
281/// The elementary type an explicit cast head `T(...)` converts to.
282pub fn cast_type(callee: &Expr<'_>) -> Option<ElementaryType> {
283    match &callee.peel_parens().kind {
284        ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ty), .. }) => Some(*ty),
285        _ => None,
286    }
287}
288
289/// `address` / `address payable` or a contract/interface type.
290pub const fn var_is_address_like(var: &hir::Variable<'_>) -> bool {
291    matches!(
292        var.ty.kind,
293        TypeKind::Elementary(ElementaryType::Address(_)) | TypeKind::Custom(ItemId::Contract(_))
294    )
295}
296
297/// AST-level boolean literal, through parens.
298pub fn ast_bool_literal(expr: &ast::Expr<'_>) -> Option<bool> {
299    match &expr.peel_parens().kind {
300        ast::ExprKind::Lit(ast::Lit { kind: LitKind::Bool(value), .. }, _) => Some(*value),
301        _ => None,
302    }
303}
304
305/// Calls `f` on every direct sub-expression of `expr`, in evaluation order.
306pub fn for_each_child<'gcx>(expr: &'gcx Expr<'gcx>, f: &mut impl FnMut(&'gcx Expr<'gcx>)) {
307    match &expr.kind {
308        ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
309            f(lhs);
310            f(rhs);
311        }
312        ExprKind::Unary(_, inner)
313        | ExprKind::Delete(inner)
314        | ExprKind::Member(inner, _)
315        | ExprKind::Payable(inner) => f(inner),
316        ExprKind::Call(callee, args, opts) => {
317            f(callee);
318            opts.iter().flat_map(|opts| opts.args).for_each(|opt| f(&opt.value));
319            args.exprs().for_each(f);
320        }
321        ExprKind::Index(base, index) => {
322            f(base);
323            index.iter().copied().for_each(f);
324        }
325        ExprKind::Slice(base, start, end) => {
326            f(base);
327            [*start, *end].into_iter().flatten().for_each(f);
328        }
329        ExprKind::Ternary(cond, true_expr, false_expr) => {
330            f(cond);
331            f(true_expr);
332            f(false_expr);
333        }
334        ExprKind::Array(exprs) => exprs.iter().for_each(f),
335        ExprKind::Tuple(exprs) => exprs.iter().flatten().copied().for_each(f),
336        ExprKind::Ident(_)
337        | ExprKind::Lit(_)
338        | ExprKind::New(_)
339        | ExprKind::TypeCall(_)
340        | ExprKind::Type(_)
341        | ExprKind::YulMember(..)
342        | ExprKind::Err(_) => {}
343    }
344}
345
346/// True if `pred` holds for `expr` or any of its sub-expressions.
347pub fn any_subexpr(expr: &Expr<'_>, mut pred: impl FnMut(&Expr<'_>) -> bool) -> bool {
348    expr.visit(&mut |e| if pred(e) { ControlFlow::Break(()) } else { ControlFlow::Continue(()) })
349        .is_break()
350}
351
352/// True if evaluating `expr` performs an assignment, `delete` or increment/decrement.
353pub fn has_side_effect(expr: &Expr<'_>) -> bool {
354    any_subexpr(expr, |e| match &e.kind {
355        ExprKind::Assign(..) | ExprKind::Delete(_) => true,
356        ExprKind::Unary(op, _) => op.kind.has_side_effects(),
357        _ => false,
358    })
359}
360
361/// True when `callee` names a zero-parameter function whose body returns an expression matching
362/// `pred`.
363pub fn callee_no_arg_returns<'gcx>(
364    gcx: Gcx<'gcx>,
365    callee: &'gcx Expr<'gcx>,
366    mut pred: impl FnMut(&'gcx Expr<'gcx>) -> bool,
367) -> bool {
368    gcx.type_of_expr(callee.peel_parens().id).is_some_and(
369        |ty| matches!(ty.kind, TyKind::Fn(f) if f.is_internal() || f.is_delegate_call()),
370    ) && gcx
371        .resolved_function(callee)
372        .is_some_and(|fid| function_no_arg_returns(gcx, fid, &mut pred))
373}
374
375/// True when `fid` takes no parameters and its body is `return e;` or `namedRet = e;` (optionally
376/// followed by a bare `return;`) with `pred(e)`.
377pub fn function_no_arg_returns<'gcx>(
378    gcx: Gcx<'gcx>,
379    fid: FunctionId,
380    pred: &mut impl FnMut(&'gcx Expr<'gcx>) -> bool,
381) -> bool {
382    let f = gcx.hir.function(fid);
383    let Some(body) = f.body else { return false };
384    let stmts = match body.stmts {
385        [rest @ .., last] if matches!(last.kind, hir::StmtKind::Return(None)) => rest,
386        stmts => stmts,
387    };
388    let [stmt] = stmts else { return false };
389    f.parameters.is_empty()
390        && match &stmt.kind {
391            hir::StmtKind::Return(Some(e)) => pred(e),
392            hir::StmtKind::Expr(e) => {
393                matches!(&e.peel_parens().kind, ExprKind::Assign(lhs, None, rhs)
394                if f.returns.len() == 1 && underlying_var(gcx, lhs) == Some(f.returns[0]) && pred(rhs))
395            }
396            _ => false,
397        }
398}
399
400/// Package-root directory names of the OpenZeppelin distributions (npm scope and git submodules).
401pub const OPENZEPPELIN_ROOTS: &[&str] =
402    &["@openzeppelin", "openzeppelin-contracts", "openzeppelin-contracts-upgradeable"];
403
404/// True if the source file of `source_id` lives under one of the given package-root directory
405/// names (matched as whole, case-insensitive path components).
406pub fn source_in_package(hir: &hir::Hir<'_>, source_id: hir::SourceId, roots: &[&str]) -> bool {
407    let solar::interface::source_map::FileName::Real(path) = &hir.source(source_id).file.name
408    else {
409        return false;
410    };
411    path.components().any(|component| {
412        matches!(component, std::path::Component::Normal(name)
413            if roots.iter().any(|root| name.eq_ignore_ascii_case(root)))
414    })
415}