Skip to main content

forge_lint/sol/med/
unused_return.rs

1use super::UnusedReturn;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::sema::{
7    Hir,
8    hir::{Expr, ExprKind, Function, ItemId, Res, 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(&mut self, ctx: &LintContext, hir: &'hir Hir<'hir>, stmt: &'hir Stmt<'hir>) {
20        if let StmtKind::Expr(expr) = &stmt.kind
21            && is_unused_return_call(hir, expr)
22        {
23            ctx.emit(&UNUSED_RETURN, expr.span);
24        }
25    }
26}
27
28/// Returns true if `expr` is a member call on a contract whose resolved function has return
29/// values, excluding ERC20 `transfer`/`transferFrom` (covered by `erc20-unchecked-transfer`).
30fn is_unused_return_call(hir: &Hir<'_>, expr: &Expr<'_>) -> bool {
31    let is_type = |var_id: VariableId, type_str: &str| {
32        matches!(
33            &hir.variable(var_id).ty.kind,
34            TypeKind::Elementary(ty) if ty.to_abi_str() == type_str
35        )
36    };
37
38    let ExprKind::Call(callee, call_args, ..) = &expr.kind else { return false };
39    let ExprKind::Member(contract_expr, func_ident) = &callee.kind else { return false };
40
41    // Arity from either positional or named args.
42    let arity = call_args.kind.len();
43
44    let Some(cid) = (match &contract_expr.kind {
45        // Pre-instantiated contract variable: `oracle.f()`
46        ExprKind::Ident([Res::Item(ItemId::Variable(id)), ..]) => {
47            if let TypeKind::Custom(ItemId::Contract(cid)) = hir.variable(*id).ty.kind {
48                Some(cid)
49            } else {
50                None
51            }
52        }
53        // Explicit interface cast: `IOracle(addr).f()`
54        ExprKind::Call(
55            Expr { kind: ExprKind::Ident([Res::Item(ItemId::Contract(cid))]), .. },
56            ..,
57        ) => Some(*cid),
58        _ => None,
59    }) else {
60        return false;
61    };
62
63    // Collect all functions in the contract matching this name and arity.
64    let candidates: Vec<&Function<'_>> = hir
65        .contract_item_ids(cid)
66        .filter_map(|item| {
67            let fid = item.as_function()?;
68            let func = hir.function(fid);
69            (func.name.is_some_and(|n| n.as_str() == func_ident.as_str())
70                && func.kind.is_function()
71                && func.parameters.len() == arity)
72                .then_some(func)
73        })
74        .collect();
75
76    // No matching candidate found, nothing to lint.
77    if candidates.is_empty() {
78        return false;
79    }
80
81    // If any candidate returns nothing, we can't tell which overload is being called,
82    // skip to avoid a false positive.
83    if candidates.iter().any(|f| f.returns.is_empty()) {
84        return false;
85    }
86
87    // If any candidate is an ERC20 transfer/transferFrom, defer to erc20-unchecked-transfer.
88    if candidates.iter().any(|f| is_erc20_transfer_sig(f, func_ident.as_str(), &is_type)) {
89        return false;
90    }
91
92    true
93}
94
95/// Returns true if `func` matches the ERC20 `transfer` or `transferFrom` signature exactly.
96/// These are handled by `erc20-unchecked-transfer` and must not be double-reported.
97fn is_erc20_transfer_sig(
98    func: &Function<'_>,
99    name: &str,
100    is_type: &impl Fn(VariableId, &str) -> bool,
101) -> bool {
102    match name {
103        "transfer" if func.parameters.len() == 2 && func.returns.len() == 1 => {
104            is_type(func.parameters[0], "address")
105                && is_type(func.parameters[1], "uint256")
106                && is_type(func.returns[0], "bool")
107        }
108        "transferFrom" if func.parameters.len() == 3 && func.returns.len() == 1 => {
109            is_type(func.parameters[0], "address")
110                && is_type(func.parameters[1], "address")
111                && is_type(func.parameters[2], "uint256")
112                && is_type(func.returns[0], "bool")
113        }
114        _ => false,
115    }
116}