Skip to main content

forge_lint/sol/low/
calls_loop.rs

1use super::CallsLoop;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast::{DataLocation, ElementaryType, Visibility},
8    interface::{kw, sym},
9    sema::{
10        Gcx, Ty,
11        hir::{
12            self, Block, ContractId, Expr, ExprKind, Function, FunctionId, Hir, ItemId, Res, Stmt,
13            StmtKind, TypeKind,
14        },
15        ty::TyKind,
16    },
17};
18use std::collections::HashSet;
19
20declare_forge_lint!(CALLS_LOOP, Severity::Low, "calls-loop", "external call inside a loop");
21
22impl<'hir> LateLintPass<'hir> for CallsLoop {
23    fn check_function_with_gcx(
24        &mut self,
25        ctx: &LintContext,
26        gcx: Gcx<'hir>,
27        hir: &'hir Hir<'hir>,
28        func: &'hir Function<'hir>,
29    ) {
30        let Some(body) = func.body else { return };
31
32        let mut analyzer = Analyzer::new(ctx, gcx, hir);
33        analyzer.analyze_callable(func, body, 0);
34    }
35}
36
37type Placeholder<'hir> = Option<(&'hir [hir::Modifier<'hir>], usize, Block<'hir>)>;
38
39struct Analyzer<'ctx, 's, 'c, 'hir> {
40    ctx: &'ctx LintContext<'s, 'c>,
41    gcx: Gcx<'hir>,
42    hir: &'hir Hir<'hir>,
43    call_stack: Vec<FunctionId>,
44    analyzed_loop_calls: HashSet<FunctionId>,
45    emitted: HashSet<solar::interface::Span>,
46}
47
48impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> {
49    fn new(ctx: &'ctx LintContext<'s, 'c>, gcx: Gcx<'hir>, hir: &'hir Hir<'hir>) -> Self {
50        Self {
51            ctx,
52            gcx,
53            hir,
54            call_stack: Vec::new(),
55            analyzed_loop_calls: HashSet::new(),
56            emitted: HashSet::new(),
57        }
58    }
59
60    fn analyze_callable(&mut self, func: &'hir Function<'hir>, body: Block<'hir>, loop_depth: u32) {
61        self.analyze_modifier_chain(func.modifiers, 0, body, loop_depth);
62    }
63
64    fn analyze_modifier_chain(
65        &mut self,
66        modifiers: &'hir [hir::Modifier<'hir>],
67        index: usize,
68        body: Block<'hir>,
69        loop_depth: u32,
70    ) {
71        let Some(modifier) = modifiers.get(index) else {
72            return self.analyze_block(body, None, loop_depth);
73        };
74
75        for arg in modifier.args.exprs() {
76            self.analyze_expr(arg, loop_depth);
77        }
78
79        let Some(modifier_id) = modifier.id.as_function() else {
80            return self.analyze_modifier_chain(modifiers, index + 1, body, loop_depth);
81        };
82
83        if self.call_stack.contains(&modifier_id) {
84            return self.analyze_modifier_chain(modifiers, index + 1, body, loop_depth);
85        }
86
87        let modifier_func = self.hir.function(modifier_id);
88        let Some(modifier_body) = modifier_func.body else {
89            return self.analyze_modifier_chain(modifiers, index + 1, body, loop_depth);
90        };
91
92        self.call_stack.push(modifier_id);
93        self.analyze_block(modifier_body, Some((modifiers, index + 1, body)), loop_depth);
94        self.call_stack.pop();
95    }
96
97    fn analyze_block(
98        &mut self,
99        block: Block<'hir>,
100        placeholder: Placeholder<'hir>,
101        loop_depth: u32,
102    ) {
103        for stmt in block.stmts {
104            self.analyze_stmt(stmt, placeholder, loop_depth);
105        }
106    }
107
108    fn analyze_stmt(
109        &mut self,
110        stmt: &'hir Stmt<'hir>,
111        placeholder: Placeholder<'hir>,
112        loop_depth: u32,
113    ) {
114        match stmt.kind {
115            StmtKind::DeclSingle(var_id) => {
116                if let Some(init) = self.hir.variable(var_id).initializer {
117                    self.analyze_expr(init, loop_depth);
118                }
119            }
120            StmtKind::DeclMulti(_, expr) | StmtKind::Expr(expr) => {
121                self.analyze_expr(expr, loop_depth);
122            }
123            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
124                self.analyze_block(block, placeholder, loop_depth);
125            }
126            StmtKind::Emit(expr) | StmtKind::Revert(expr) => {
127                self.analyze_expr(expr, loop_depth);
128            }
129            StmtKind::Return(Some(expr)) => {
130                self.analyze_expr(expr, loop_depth);
131            }
132            StmtKind::Loop(block, _) => {
133                self.analyze_block(block, placeholder, loop_depth + 1);
134            }
135            StmtKind::If(cond, then_stmt, else_stmt) => {
136                self.analyze_expr(cond, loop_depth);
137                self.analyze_stmt(then_stmt, placeholder, loop_depth);
138                if let Some(else_stmt) = else_stmt {
139                    self.analyze_stmt(else_stmt, placeholder, loop_depth);
140                }
141            }
142            StmtKind::Try(try_stmt) => {
143                self.analyze_expr(&try_stmt.expr, loop_depth);
144                for clause in try_stmt.clauses {
145                    self.analyze_block(clause.block, placeholder, loop_depth);
146                }
147            }
148            StmtKind::Placeholder => {
149                if let Some((modifiers, index, body)) = placeholder {
150                    self.analyze_modifier_chain(modifiers, index, body, loop_depth);
151                }
152            }
153            StmtKind::Return(None) | StmtKind::Break | StmtKind::Continue | StmtKind::Err(_) => {}
154        }
155    }
156
157    fn analyze_expr(&mut self, expr: &'hir Expr<'hir>, loop_depth: u32) {
158        match &expr.kind {
159            ExprKind::Call(callee, args, opts) => {
160                self.analyze_expr(callee, loop_depth);
161                if let Some(opts) = opts {
162                    for opt in *opts {
163                        self.analyze_expr(&opt.value, loop_depth);
164                    }
165                }
166                for arg in args.exprs() {
167                    self.analyze_expr(arg, loop_depth);
168                }
169
170                if loop_depth > 0 {
171                    if is_external_call(self.gcx, self.hir, callee, args.len()) {
172                        self.emit(expr);
173                    }
174                    for func_id in resolved_internal_function_ids(self.hir, callee) {
175                        self.analyze_internal_call(func_id, loop_depth);
176                    }
177                }
178            }
179            ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
180                self.analyze_expr(lhs, loop_depth);
181                self.analyze_expr(rhs, loop_depth);
182            }
183            ExprKind::Unary(_, inner) | ExprKind::Delete(inner) | ExprKind::Payable(inner) => {
184                self.analyze_expr(inner, loop_depth);
185            }
186            ExprKind::Index(base, index) => {
187                self.analyze_expr(base, loop_depth);
188                if let Some(index) = index {
189                    self.analyze_expr(index, loop_depth);
190                }
191            }
192            ExprKind::Slice(base, start, end) => {
193                self.analyze_expr(base, loop_depth);
194                if let Some(start) = start {
195                    self.analyze_expr(start, loop_depth);
196                }
197                if let Some(end) = end {
198                    self.analyze_expr(end, loop_depth);
199                }
200            }
201            ExprKind::Ternary(cond, then_expr, else_expr) => {
202                self.analyze_expr(cond, loop_depth);
203                self.analyze_expr(then_expr, loop_depth);
204                self.analyze_expr(else_expr, loop_depth);
205            }
206            ExprKind::Array(exprs) => {
207                for expr in *exprs {
208                    self.analyze_expr(expr, loop_depth);
209                }
210            }
211            ExprKind::Tuple(exprs) => {
212                for expr in exprs.iter().copied().flatten() {
213                    self.analyze_expr(expr, loop_depth);
214                }
215            }
216            ExprKind::Member(base, _) => self.analyze_expr(base, loop_depth),
217            ExprKind::Ident(_)
218            | ExprKind::Lit(_)
219            | ExprKind::New(_)
220            | ExprKind::TypeCall(_)
221            | ExprKind::Type(_)
222            | ExprKind::Err(_) => {}
223        }
224    }
225
226    fn analyze_internal_call(&mut self, func_id: FunctionId, loop_depth: u32) {
227        if self.call_stack.contains(&func_id) {
228            return;
229        }
230        if !self.analyzed_loop_calls.insert(func_id) {
231            return;
232        }
233
234        let func = self.hir.function(func_id);
235        let Some(body) = func.body else { return };
236
237        self.call_stack.push(func_id);
238        self.analyze_callable(func, body, loop_depth);
239        self.call_stack.pop();
240    }
241
242    fn emit(&mut self, expr: &Expr<'_>) {
243        if self.emitted.insert(expr.span) {
244            self.ctx.emit(&CALLS_LOOP, expr.span);
245        }
246    }
247}
248
249fn is_external_call<'gcx>(
250    gcx: Gcx<'gcx>,
251    hir: &Hir<'gcx>,
252    callee: &Expr<'gcx>,
253    explicit_arg_count: usize,
254) -> bool {
255    let ExprKind::Member(base, member) = &callee.peel_parens().kind else { return false };
256
257    if matches!(member.name, kw::Call | kw::Delegatecall | kw::Staticcall)
258        && is_address_like(hir, base)
259    {
260        return true;
261    }
262
263    if matches!(member.name, sym::send | sym::transfer) && is_address_like(hir, base) {
264        return true;
265    }
266
267    if is_this(base) {
268        return true;
269    }
270
271    if resolves_to_internal_library_extension(gcx, hir, base, *member, explicit_arg_count) {
272        return false;
273    }
274
275    matches!(semantic_member_ty(gcx, hir, base, member.name).map(|ty| ty.kind), Some(TyKind::FnPtr(func)) if func.visibility >= Visibility::Public)
276}
277
278fn resolves_to_internal_library_extension<'gcx>(
279    gcx: Gcx<'gcx>,
280    hir: &Hir<'gcx>,
281    base: &Expr<'gcx>,
282    member: solar::interface::Ident,
283    explicit_arg_count: usize,
284) -> bool {
285    member_function_ids(gcx, hir, base, member.name).into_iter().any(|func_id| {
286        let func = hir.function(func_id);
287        func.parameters.len() == explicit_arg_count + 1
288            && matches!(func.visibility, Visibility::Internal | Visibility::Private)
289            && func.contract.is_some_and(|contract_id| hir.contract(contract_id).kind.is_library())
290    })
291}
292
293fn member_function_ids<'gcx>(
294    gcx: Gcx<'gcx>,
295    hir: &Hir<'gcx>,
296    base: &Expr<'gcx>,
297    member_name: solar::interface::Symbol,
298) -> Vec<FunctionId> {
299    let Some(base_ty) = semantic_expr_ty(gcx, hir, base) else { return Vec::new() };
300
301    gcx.members_of(base_ty, base_item_source(hir, base), base_contract(hir, base))
302        .iter()
303        .filter(|member| member.name == member_name)
304        .filter_map(|member| match (member.res, member.ty.kind) {
305            (Some(Res::Item(ItemId::Function(func_id))), _) => Some(func_id),
306            (_, TyKind::FnPtr(func)) => func.function_id,
307            _ => None,
308        })
309        .collect()
310}
311
312fn resolved_internal_function_ids<'hir>(
313    hir: &'hir Hir<'hir>,
314    callee: &'hir Expr<'hir>,
315) -> impl Iterator<Item = FunctionId> + 'hir {
316    let reses = match &callee.peel_parens().kind {
317        ExprKind::Ident(reses) => *reses,
318        _ => &[],
319    };
320
321    reses.iter().filter_map(|res| match res {
322        Res::Item(ItemId::Function(func_id)) if is_internal_callable(hir.function(*func_id)) => {
323            Some(*func_id)
324        }
325        _ => None,
326    })
327}
328
329const fn is_internal_callable(func: &Function<'_>) -> bool {
330    func.kind.is_function()
331        && matches!(
332            func.visibility,
333            Visibility::Public | Visibility::Internal | Visibility::Private
334        )
335}
336
337fn is_this(expr: &Expr<'_>) -> bool {
338    matches!(
339        &expr.peel_parens().kind,
340        ExprKind::Ident(reses)
341            if reses.iter().any(|res| {
342                matches!(res, Res::Builtin(builtin) if builtin.name() == sym::this)
343            })
344    )
345}
346
347fn is_address_like(hir: &Hir<'_>, expr: &Expr<'_>) -> bool {
348    match &expr.peel_parens().kind {
349        ExprKind::Payable(_) => true,
350        ExprKind::Call(callee, _, _) if is_address_type_expr(callee) => true,
351        _ => expr_type(hir, expr).is_some_and(type_is_address_like),
352    }
353}
354
355fn contract_type_expr_id(expr: &Expr<'_>) -> Option<ContractId> {
356    match &expr.peel_parens().kind {
357        ExprKind::Type(hir::Type { kind: TypeKind::Custom(ItemId::Contract(id)), .. }) => Some(*id),
358        ExprKind::Ident(reses) => reses.iter().find_map(|res| match res {
359            Res::Item(ItemId::Contract(id)) => Some(*id),
360            _ => None,
361        }),
362        _ => None,
363    }
364}
365
366fn is_address_type_expr(expr: &Expr<'_>) -> bool {
367    matches!(
368        &expr.peel_parens().kind,
369        ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ElementaryType::Address(_)), .. })
370    )
371}
372
373const fn type_contract_id(ty: &hir::Type<'_>) -> Option<ContractId> {
374    match ty.kind {
375        TypeKind::Custom(ItemId::Contract(id)) => Some(id),
376        _ => None,
377    }
378}
379
380const fn type_is_address_like(ty: &hir::Type<'_>) -> bool {
381    matches!(ty.kind, TypeKind::Elementary(ElementaryType::Address(_)))
382}
383
384fn expr_type<'hir>(hir: &'hir Hir<'hir>, expr: &Expr<'hir>) -> Option<&'hir hir::Type<'hir>> {
385    match &expr.peel_parens().kind {
386        ExprKind::Ident(reses) => reses.iter().find_map(|res| {
387            let var_id = res.as_variable()?;
388            Some(&hir.variable(var_id).ty)
389        }),
390        ExprKind::Call(callee, args, _) => single_return_type(hir, callee, args.len()),
391        ExprKind::Index(base, _) => indexed_element_type(hir, base),
392        ExprKind::Member(base, member) => member_type(hir, base, *member),
393        _ => None,
394    }
395}
396
397fn single_return_type<'hir>(
398    hir: &'hir Hir<'hir>,
399    callee: &Expr<'hir>,
400    arity: usize,
401) -> Option<&'hir hir::Type<'hir>> {
402    match &callee.peel_parens().kind {
403        ExprKind::Ident(reses) => reses.iter().find_map(|res| {
404            let Res::Item(ItemId::Function(func_id)) = res else { return None };
405            let func = hir.function(*func_id);
406            let [ret] = func.returns else { return None };
407            Some(&hir.variable(*ret).ty)
408        }),
409        ExprKind::Member(base, member) => member_return_type(hir, base, *member, arity),
410        _ => None,
411    }
412}
413
414fn member_return_type<'hir>(
415    hir: &'hir Hir<'hir>,
416    base: &Expr<'hir>,
417    member: solar::interface::Ident,
418    arity: usize,
419) -> Option<&'hir hir::Type<'hir>> {
420    let contract_id = receiver_contract_id(hir, base)?;
421    let mut ret = None;
422    for item in hir.contract_item_ids(contract_id) {
423        let Some(func_id) = item.as_function() else { continue };
424        let func = hir.function(func_id);
425        if func.name.is_none_or(|name| name.name != member.name) || func.parameters.len() != arity {
426            continue;
427        }
428        let [ret_id] = func.returns else { return None };
429        if ret.replace(&hir.variable(*ret_id).ty).is_some() {
430            return None;
431        }
432    }
433    ret
434}
435
436fn receiver_contract_id(hir: &Hir<'_>, expr: &Expr<'_>) -> Option<ContractId> {
437    match &expr.peel_parens().kind {
438        ExprKind::Ident(reses) => reses.iter().find_map(|res| match res {
439            Res::Item(ItemId::Contract(id)) => Some(*id),
440            Res::Item(ItemId::Variable(id)) => type_contract_id(&hir.variable(*id).ty),
441            _ => None,
442        }),
443        ExprKind::Call(callee, _, _) => contract_type_expr_id(callee)
444            .or_else(|| expr_type(hir, expr).and_then(type_contract_id)),
445        ExprKind::New(hir::Type { kind: TypeKind::Custom(ItemId::Contract(id)), .. }) => Some(*id),
446        _ => expr_type(hir, expr).and_then(type_contract_id),
447    }
448}
449
450fn indexed_element_type<'hir>(
451    hir: &'hir Hir<'hir>,
452    expr: &Expr<'hir>,
453) -> Option<&'hir hir::Type<'hir>> {
454    expr_type(hir, expr).and_then(|ty| match &ty.kind {
455        TypeKind::Array(array) => Some(&array.element),
456        TypeKind::Mapping(mapping) => Some(&mapping.value),
457        _ => None,
458    })
459}
460
461fn member_type<'hir>(
462    hir: &'hir Hir<'hir>,
463    expr: &Expr<'hir>,
464    member: solar::interface::Ident,
465) -> Option<&'hir hir::Type<'hir>> {
466    expr_type(hir, expr).and_then(|ty| match ty.kind {
467        TypeKind::Custom(ItemId::Struct(struct_id)) => {
468            hir.strukt(struct_id).fields.iter().find_map(|field_id| {
469                let field = hir.variable(*field_id);
470                (field.name?.name == member.name).then_some(&field.ty)
471            })
472        }
473        _ => None,
474    })
475}
476
477fn semantic_expr_ty<'gcx>(gcx: Gcx<'gcx>, hir: &Hir<'gcx>, expr: &Expr<'gcx>) -> Option<Ty<'gcx>> {
478    match &expr.peel_parens().kind {
479        ExprKind::Ident(reses) => {
480            let res = unique(reses.iter().filter(|res| !matches!(res, Res::Err(_))).copied())
481                .or_else(|| {
482                    unique(reses.iter().filter_map(|res| {
483                        res.as_variable().map(|var_id| Res::Item(ItemId::Variable(var_id)))
484                    }))
485                })?;
486            let ty = gcx.type_of_res(res);
487            Some(match res {
488                Res::Item(ItemId::Variable(var_id)) => {
489                    ty.with_loc_if_ref_opt(gcx, variable_data_location(hir, var_id))
490                }
491                _ => ty,
492            })
493        }
494        ExprKind::Index(base, _) => semantic_index_ty(gcx, hir, base),
495        ExprKind::Member(base, member) => semantic_member_ty(gcx, hir, base, member.name),
496        ExprKind::Call(callee, _, _) => {
497            let callee_ty = semantic_expr_ty(gcx, hir, callee)?;
498            match callee_ty.kind {
499                TyKind::FnPtr(func) => semantic_fn_call_return_ty(gcx, func.returns),
500                TyKind::Type(to) => Some(to),
501                _ => None,
502            }
503        }
504        ExprKind::New(ty) | ExprKind::Type(ty) | ExprKind::TypeCall(ty) => {
505            Some(gcx.mk_ty(TyKind::Type(gcx.type_of_hir_ty(ty))))
506        }
507        ExprKind::Payable(_) => Some(gcx.types.address_payable),
508        _ => None,
509    }
510}
511
512fn semantic_index_ty<'gcx>(gcx: Gcx<'gcx>, hir: &Hir<'gcx>, base: &Expr<'gcx>) -> Option<Ty<'gcx>> {
513    let base_ty = semantic_expr_ty(gcx, hir, base)?;
514    let loc = indexed_base_data_location(base_ty);
515    match base_ty.peel_refs().kind {
516        TyKind::Mapping(_, value) => Some(value.with_loc_if_ref_opt(gcx, loc)),
517        _ => base_ty.base_type(gcx),
518    }
519}
520
521fn indexed_base_data_location(ty: Ty<'_>) -> Option<DataLocation> {
522    ty.loc().or_else(|| {
523        // Mappings can only live in storage, but Solar does not model `TyKind::Mapping`
524        // itself as a reference type.
525        matches!(ty.kind, TyKind::Mapping(..)).then_some(DataLocation::Storage)
526    })
527}
528
529fn semantic_member_ty<'gcx>(
530    gcx: Gcx<'gcx>,
531    hir: &Hir<'gcx>,
532    base: &Expr<'gcx>,
533    member_name: solar::interface::Symbol,
534) -> Option<Ty<'gcx>> {
535    let base_ty = semantic_expr_ty(gcx, hir, base)?;
536    unique(
537        gcx.members_of(base_ty, base_item_source(hir, base), base_contract(hir, base))
538            .iter()
539            .filter(|member| member.name == member_name)
540            .map(|member| member.ty),
541    )
542}
543
544fn semantic_fn_call_return_ty<'gcx>(gcx: Gcx<'gcx>, returns: &'gcx [Ty<'gcx>]) -> Option<Ty<'gcx>> {
545    Some(match returns {
546        [] => gcx.types.unit,
547        [ret] => *ret,
548        _ => gcx.mk_ty_tuple(returns),
549    })
550}
551
552fn base_item_source(hir: &Hir<'_>, expr: &Expr<'_>) -> solar::sema::hir::SourceId {
553    referenced_item(expr)
554        .map(|id| hir.item(id).source())
555        .unwrap_or_else(|| hir.sources_enumerated().next().expect("HIR has a source").0)
556}
557
558fn base_contract(hir: &Hir<'_>, expr: &Expr<'_>) -> Option<ContractId> {
559    referenced_item(expr).and_then(|id| hir.item(id).contract())
560}
561
562fn referenced_item(expr: &Expr<'_>) -> Option<ItemId> {
563    match &expr.peel_parens().kind {
564        ExprKind::Ident([Res::Item(id), ..]) => Some(*id),
565        _ => None,
566    }
567}
568
569fn variable_data_location(hir: &Hir<'_>, var_id: hir::VariableId) -> Option<DataLocation> {
570    let var = hir.variable(var_id);
571    var.data_location.or_else(|| {
572        (var.function.is_none() && var.contract.is_some()).then_some(DataLocation::Storage)
573    })
574}
575
576fn unique<T>(mut iter: impl Iterator<Item = T>) -> Option<T> {
577    let first = iter.next()?;
578    iter.next().is_none().then_some(first)
579}