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 call whose selected function has return values, excluding ERC20
46/// `transfer`/`transferFrom` member calls (covered by `erc20-unchecked-transfer`).
47fn is_unused_return_call<'gcx>(gcx: Gcx<'gcx>, expr: &Expr<'gcx>) -> bool {
48    let ExprKind::Call(callee, ..) = &expr.peel_parens().kind else { return false };
49    let Some(TyKind::Fn(function_ty)) = gcx.type_of_expr(callee.peel_parens().id).map(|ty| ty.kind)
50    else {
51        return false;
52    };
53    if !matches!(function_ty.kind(), TyFnKind::External | TyFnKind::DelegateCall)
54        || function_ty.returns.is_empty()
55    {
56        return false;
57    }
58
59    let ExprKind::Member(_, name) = &callee.peel_parens().kind else { return true };
60    let Some(fid) = gcx.resolved_function(callee) else { return false };
61    let f = gcx.hir.function(fid);
62
63    let sig = |vars: &[_], expected: &[&str]| {
64        vars.len() == expected.len()
65            && vars.iter().zip(expected).all(|(&id, &ty)| is_elementary(&gcx.hir, id, ty))
66    };
67    let is_erc20_transfer = sig(f.returns, &["bool"])
68        && match name.as_str() {
69            "transfer" => sig(f.parameters, &["address", "uint256"]),
70            "transferFrom" => sig(f.parameters, &["address", "address", "uint256"]),
71            _ => false,
72        };
73    !is_erc20_transfer
74}