Skip to main content

forge_lint/sol/analysis/
primitives.rs

1//! Side-effect-free syntactic/semantic probes over solar HIR.
2
3use solar::{
4    ast::LitKind,
5    interface::{Symbol, kw, sym},
6    sema::hir::{self, ElementaryType, Expr, ExprKind, Res, Stmt, StmtKind, TypeKind, VariableId},
7};
8
9/// True if `expr` references the named global builtin (`msg`, `tx`, `this`, ...).
10fn is_builtin(expr: &Expr<'_>, name: Symbol) -> bool {
11    matches!(&expr.peel_parens().kind, ExprKind::Ident(reses)
12        if reses.iter().any(|r| matches!(r, Res::Builtin(b) if b.name() == name)))
13}
14
15/// True if `vid` is typed as `address`/`address payable`.
16pub fn is_address_type(hir: &hir::Hir<'_>, vid: VariableId) -> bool {
17    matches!(hir.variable(vid).ty.kind, TypeKind::Elementary(ElementaryType::Address(_)))
18}
19
20/// True if `callee` resolves to the builtin `require` or `assert`.
21pub fn is_require_or_assert(callee: &Expr<'_>) -> bool {
22    matches!(&callee.kind, ExprKind::Ident(reses)
23        if reses.iter().any(|r| matches!(r,
24            Res::Builtin(b) if b.name() == sym::require || b.name() == sym::assert)))
25}
26
27/// Receiver of `<expr>.{call,delegatecall,transfer,send}`, including the
28/// `.call{value: x}(...)` option form.
29pub fn address_call_receiver<'a>(callee: &'a Expr<'a>) -> Option<&'a Expr<'a>> {
30    // `addr.call{...}(..)` lowers as `Call(Member(receiver, "call"), ..)`.
31    let inner = match &callee.kind {
32        ExprKind::Call(inner, ..) => inner,
33        _ => callee,
34    };
35    let target = if matches!(inner.kind, ExprKind::Member(..)) { inner } else { callee };
36    if let ExprKind::Member(receiver, name) = &target.kind {
37        let n = name.name;
38        if n == kw::Call || n == kw::Delegatecall || n == sym::transfer || n == sym::send {
39            return Some(receiver);
40        }
41    }
42    None
43}
44
45/// True when executing `stmt` provably prevents control from continuing past
46/// it: a `return`, `revert`/`revert(...)`, `require(false, ...)`,
47/// `assert(false)`, a block containing any such statement (any subsequent
48/// statements are unreachable), or an `if` whose both arms exit.
49pub fn branch_always_exits(stmt: &Stmt<'_>) -> bool {
50    match &stmt.kind {
51        StmtKind::Return(_) | StmtKind::Revert(_) => true,
52        StmtKind::Expr(expr) => is_exit_call(expr),
53        StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => b.stmts.iter().any(branch_always_exits),
54        StmtKind::If(_, t, Some(e)) => branch_always_exits(t) && branch_always_exits(e),
55        _ => false,
56    }
57}
58
59fn is_exit_call(expr: &Expr<'_>) -> bool {
60    let ExprKind::Call(callee, args, _) = &expr.kind else { return false };
61    if is_builtin(callee, kw::Revert) {
62        return true;
63    }
64    if is_require_or_assert(callee)
65        && let Some(first) = args.exprs().next()
66        && matches!(
67            &first.peel_parens().kind,
68            ExprKind::Lit(lit) if matches!(lit.kind, LitKind::Bool(false))
69        )
70    {
71        return true;
72    }
73    false
74}