Skip to main content

forge_lint/sol/med/
unused_return.rs

1use super::UnusedReturn;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{
5        Severity, SolLint,
6        analysis::{is_elementary, tuple_elems},
7    },
8};
9use solar::sema::{
10    Gcx,
11    hir::{Expr, ExprKind, Stmt, StmtKind},
12    ty::{TyFnKind, TyKind},
13};
14
15declare_forge_lint!(
16    UNUSED_RETURN,
17    Severity::Med,
18    "unused-return",
19    "return value of an external call is not used"
20);
21
22impl<'gcx> LateLintPass<'gcx> for UnusedReturn {
23    fn check_stmt(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, stmt: &'gcx Stmt<'gcx>) {
24        let (call, span) = match &stmt.kind {
25            StmtKind::Expr(expr) => match &expr.peel_parens().kind {
26                // `(x, ) = call()` with an ignored slot.
27                ExprKind::Assign(lhs, None, rhs)
28                    if tuple_elems(lhs).is_some_and(|e| e.iter().any(Option::is_none)) =>
29                {
30                    (rhs, expr.span)
31                }
32                _ => (expr, expr.span),
33            },
34            StmtKind::DeclMulti(vars, expr) if vars.iter().any(Option::is_none) => {
35                (expr, expr.span)
36            }
37            _ => return,
38        };
39        if is_unused_return_call(gcx, call) {
40            ctx.emit(&UNUSED_RETURN, span);
41        }
42    }
43}
44
45/// True if `expr` is an external member call whose selected function has return values,
46/// excluding ERC20 `transfer`/`transferFrom` (covered by
47/// `erc20-unchecked-transfer`).
48fn is_unused_return_call<'gcx>(gcx: Gcx<'gcx>, expr: &Expr<'gcx>) -> bool {
49    let ExprKind::Call(callee, ..) = &expr.peel_parens().kind else { return false };
50    let ExprKind::Member(_, name) = &callee.peel_parens().kind else { return false };
51    let Some(ty) = gcx.type_of_expr(callee.peel_parens().id) else { return false };
52    if !matches!(ty.kind, TyKind::Fn(f) if matches!(f.kind(), TyFnKind::External | TyFnKind::DelegateCall))
53    {
54        return false;
55    }
56    let Some(fid) = gcx.resolved_function(callee) else { return false };
57    let f = gcx.hir.function(fid);
58
59    let sig = |vars: &[_], expected: &[&str]| {
60        vars.len() == expected.len()
61            && vars.iter().zip(expected).all(|(&id, &ty)| is_elementary(&gcx.hir, id, ty))
62    };
63    let is_erc20_transfer = sig(f.returns, &["bool"])
64        && match name.as_str() {
65            "transfer" => sig(f.parameters, &["address", "uint256"]),
66            "transferFrom" => sig(f.parameters, &["address", "address", "uint256"]),
67            _ => false,
68        };
69    !f.returns.is_empty() && !is_erc20_transfer
70}