Skip to main content

forge_lint/sol/med/
unused_return.rs

1use super::UnusedReturn;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint, analysis::interface::receiver_contract_id},
5};
6use solar::sema::{
7    Gcx, Hir,
8    hir::{Expr, ExprKind, Function, Stmt, StmtKind, TypeKind, VariableId},
9};
10
11declare_forge_lint!(
12    UNUSED_RETURN,
13    Severity::Med,
14    "unused-return",
15    "Return value of an external call is not used"
16);
17
18impl<'hir> LateLintPass<'hir> for UnusedReturn {
19    fn check_stmt(
20        &mut self,
21        ctx: &LintContext,
22        _gcx: Gcx<'hir>,
23        hir: &'hir Hir<'hir>,
24        stmt: &'hir Stmt<'hir>,
25    ) {
26        match &stmt.kind {
27            StmtKind::Expr(expr)
28                if is_unused_return_call(hir, expr) || is_ignored_tuple_assignment(hir, expr) =>
29            {
30                ctx.emit(&UNUSED_RETURN, expr.span);
31            }
32            StmtKind::DeclMulti(vars, expr)
33                if vars.iter().any(Option::is_none) && is_unused_return_call(hir, expr) =>
34            {
35                ctx.emit(&UNUSED_RETURN, expr.span);
36            }
37            _ => {}
38        }
39    }
40}
41
42fn is_ignored_tuple_assignment(hir: &Hir<'_>, expr: &Expr<'_>) -> bool {
43    let ExprKind::Assign(lhs, None, rhs) = &expr.peel_parens().kind else { return false };
44    matches!(&lhs.peel_parens().kind, ExprKind::Tuple(elems) if elems.iter().any(Option::is_none))
45        && is_unused_return_call(hir, rhs)
46}
47
48/// Returns true if `expr` is a member call on a contract whose resolved function has return
49/// values, excluding ERC20 `transfer`/`transferFrom` (covered by `erc20-unchecked-transfer`).
50fn is_unused_return_call(hir: &Hir<'_>, expr: &Expr<'_>) -> bool {
51    let is_type = |var_id: VariableId, type_str: &str| {
52        matches!(
53            &hir.variable(var_id).ty.kind,
54            TypeKind::Elementary(ty) if ty.to_abi_str() == type_str
55        )
56    };
57
58    let ExprKind::Call(callee, call_args, ..) = &expr.peel_parens().kind else { return false };
59    let ExprKind::Member(contract_expr, func_ident) = &callee.peel_parens().kind else {
60        return false;
61    };
62
63    // Arity from either positional or named args.
64    let arity = call_args.kind.len();
65
66    let Some(cid) = receiver_contract_id(hir, contract_expr) else { return false };
67
68    let mut has_candidate = false;
69    for item in hir.contract_item_ids(cid) {
70        let Some(fid) = item.as_function() else { continue };
71        let func = hir.function(fid);
72        if func.name.is_none_or(|n| n.as_str() != func_ident.as_str())
73            || !func.kind.is_function()
74            || func.parameters.len() != arity
75        {
76            continue;
77        }
78
79        has_candidate = true;
80
81        // If any matching overload returns nothing, we can't tell which overload is being called,
82        // skip to avoid a false positive.
83        if func.returns.is_empty() {
84            return false;
85        }
86
87        // If any candidate is an ERC20 transfer/transferFrom, defer to erc20-unchecked-transfer.
88        if is_erc20_transfer_sig(func, func_ident.as_str(), &is_type) {
89            return false;
90        }
91    }
92
93    has_candidate
94}
95
96/// Returns true if `func` matches the ERC20 `transfer` or `transferFrom` signature exactly.
97/// These are handled by `erc20-unchecked-transfer` and must not be double-reported.
98fn is_erc20_transfer_sig(
99    func: &Function<'_>,
100    name: &str,
101    is_type: &impl Fn(VariableId, &str) -> bool,
102) -> bool {
103    match name {
104        "transfer" if func.parameters.len() == 2 && func.returns.len() == 1 => {
105            is_type(func.parameters[0], "address")
106                && is_type(func.parameters[1], "uint256")
107                && is_type(func.returns[0], "bool")
108        }
109        "transferFrom" if func.parameters.len() == 3 && func.returns.len() == 1 => {
110            is_type(func.parameters[0], "address")
111                && is_type(func.parameters[1], "address")
112                && is_type(func.parameters[2], "uint256")
113                && is_type(func.returns[0], "bool")
114        }
115        _ => false,
116    }
117}