forge_lint/sol/med/
unused_return.rs1use 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
48fn 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 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 func.returns.is_empty() {
84 return false;
85 }
86
87 if is_erc20_transfer_sig(func, func_ident.as_str(), &is_type) {
89 return false;
90 }
91 }
92
93 has_candidate
94}
95
96fn 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}