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 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}