forge_lint/sol/med/
unused_return.rs1use 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
28fn 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 let arity = call_args.kind.len();
43
44 let Some(cid) = (match &contract_expr.kind {
45 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 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 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 if candidates.is_empty() {
78 return false;
79 }
80
81 if candidates.iter().any(|f| f.returns.is_empty()) {
84 return false;
85 }
86
87 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
95fn 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}