1use super::{CoverageItem, CoverageItemKind, SourceLocation};
2use alloy_primitives::map::HashMap;
3use foundry_common::TestFunctionExt;
4use foundry_compilers::ProjectCompileOutput;
5use rayon::prelude::*;
6use solar::{
7 ast::{self, ExprKind, ItemKind, StmtKind, yul},
8 data_structures::{Never, map::FxHashMap},
9 interface::{BytePos, Span},
10 sema::{Gcx, hir},
11};
12use std::{
13 ops::{ControlFlow, Range},
14 path::PathBuf,
15 sync::Arc,
16};
17
18#[derive(Clone)]
20struct SourceVisitor<'gcx> {
21 source_id: u32,
23 gcx: Gcx<'gcx>,
25
26 contract_name: Arc<str>,
28
29 branch_id: u32,
31
32 items: Vec<CoverageItem>,
34
35 all_lines: Vec<u32>,
36 function_calls: Vec<(Span, Arc<str>)>,
39 function_call_scopes: FxHashMap<Span, Arc<str>>,
40}
41
42struct SourceVisitorCheckpoint {
43 items: usize,
44 all_lines: usize,
45 function_calls: usize,
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub(crate) enum EmptySpecialFunctionKind {
50 Constructor,
51 Receive,
52 Fallback,
53}
54
55struct ResolvedEmptySpecialFunction {
56 contract_source_id: u32,
57 contract_name: Arc<str>,
58 function_source_id: u32,
59 function_contract_name: Arc<str>,
60 function_bytes: Range<u32>,
61 kind: EmptySpecialFunctionKind,
62}
63
64fn resolve_empty_special_functions(
65 gcx: Gcx<'_>,
66 data: &SourceFiles,
67) -> Vec<ResolvedEmptySpecialFunction> {
68 let hir_source_ids = data
69 .sources
70 .iter()
71 .map(|(&source_id, path)| {
72 let (hir_source_id, _) = gcx.get_hir_source(path).unwrap();
73 (hir_source_id, source_id)
74 })
75 .collect::<HashMap<_, _>>();
76 let mut resolved = Vec::new();
77 for contract_id in gcx.hir.contract_ids() {
78 let contract = gcx.hir.contract(contract_id);
79 let Some(&contract_source_id) = hir_source_ids.get(&contract.source) else { continue };
80 let constructors = contract.linearized_bases.iter().filter_map(|&base_id| {
81 gcx.hir
82 .contract(base_id)
83 .ctor
84 .map(|function_id| (function_id, EmptySpecialFunctionKind::Constructor))
85 });
86 let runtime_functions = [
87 (contract.receive, EmptySpecialFunctionKind::Receive),
88 (contract.fallback, EmptySpecialFunctionKind::Fallback),
89 ]
90 .into_iter()
91 .filter_map(|(function_id, kind)| function_id.map(|function_id| (function_id, kind)));
92 for (function_id, kind) in constructors.chain(runtime_functions) {
93 let function = gcx.hir.function(function_id);
94 if !function.body.is_some_and(|body| body.is_empty()) {
95 continue;
96 }
97 let Some(&function_source_id) = hir_source_ids.get(&function.source) else { continue };
98 let Some(function_contract_id) = function.contract else { continue };
99 let function_contract = gcx.hir.contract(function_contract_id);
100 let function_bytes = gcx.sess.source_map().span_to_source(function.span).unwrap().data;
101 resolved.push(ResolvedEmptySpecialFunction {
102 contract_source_id,
103 contract_name: contract.name.as_str().into(),
104 function_source_id,
105 function_contract_name: function_contract.name.as_str().into(),
106 function_bytes: function_bytes.start as u32..function_bytes.end as u32,
107 kind,
108 });
109 }
110 }
111 resolved
112}
113
114impl<'gcx> SourceVisitor<'gcx> {
115 fn new(source_id: u32, gcx: Gcx<'gcx>) -> Self {
116 Self {
117 source_id,
118 gcx,
119 contract_name: Arc::default(),
120 branch_id: 0,
121 all_lines: Default::default(),
122 function_calls: Default::default(),
123 function_call_scopes: Default::default(),
124 items: Default::default(),
125 }
126 }
127
128 const fn checkpoint(&self) -> SourceVisitorCheckpoint {
129 SourceVisitorCheckpoint {
130 items: self.items.len(),
131 all_lines: self.all_lines.len(),
132 function_calls: self.function_calls.len(),
133 }
134 }
135
136 fn restore_checkpoint(&mut self, checkpoint: SourceVisitorCheckpoint) {
137 let SourceVisitorCheckpoint { items, all_lines, function_calls } = checkpoint;
138 self.items.truncate(items);
139 self.all_lines.truncate(all_lines);
140 self.function_calls.truncate(function_calls);
141 }
142
143 fn visit_contract<'ast>(&mut self, contract: &'ast ast::ItemContract<'ast>) {
144 let _ = ast::Visit::visit_item_contract(self, contract);
145 }
146
147 fn has_tests(&self, checkpoint: &SourceVisitorCheckpoint) -> bool {
149 self.items[checkpoint.items..].iter().any(|item| {
150 if let CoverageItemKind::Function { name } = &item.kind {
151 name.is_any_test()
152 } else {
153 false
154 }
155 })
156 }
157
158 fn disambiguate_functions(&mut self) {
162 let mut dups = HashMap::<_, Vec<usize>>::default();
163 for (i, item) in self.items.iter().enumerate() {
164 if let CoverageItemKind::Function { name } = &item.kind {
165 dups.entry((item.loc.contract_name.clone(), name.clone())).or_default().push(i);
166 }
167 }
168 for dups in dups.values() {
169 if dups.len() > 1 {
170 for (i, &dup) in dups.iter().enumerate() {
171 let item = &mut self.items[dup];
172 if let CoverageItemKind::Function { name } = &item.kind {
173 item.kind =
174 CoverageItemKind::Function { name: format!("{name}.{i}").into() };
175 }
176 }
177 }
178 }
179 }
180
181 fn resolve_function_calls(&mut self, hir_source_id: hir::SourceId) {
182 self.function_call_scopes = self.function_calls.iter().cloned().collect();
183 let _ = hir::Visit::visit_nested_source(self, hir_source_id);
184 }
185
186 fn sort(&mut self) {
187 self.items.sort();
188 }
189
190 fn push_lines(&mut self) {
191 self.all_lines.sort_unstable();
192 self.all_lines.dedup();
193 let mut lines = Vec::with_capacity(self.all_lines.len());
194 let mut items = self.items.iter().peekable();
196 for &line in &self.all_lines {
197 while items.peek().is_some_and(|item| item.loc.lines.start < line) {
198 items.next();
199 }
200 if let Some(reference_item) = items.peek().filter(|item| item.loc.lines.start == line) {
201 lines.push(CoverageItem {
202 kind: CoverageItemKind::Line,
203 loc: reference_item.loc.clone(),
204 anchor_loc: None,
205 hits: 0,
206 });
207 }
208 }
209 self.items.extend(lines);
210 }
211
212 fn push_stmt(&mut self, span: Span) {
213 self.push_item_kind(CoverageItemKind::Statement, span);
214 }
215
216 fn push_item_kind(&mut self, kind: CoverageItemKind, span: Span) -> &mut CoverageItem {
219 let item =
220 CoverageItem { kind, loc: self.source_location_for(span), anchor_loc: None, hits: 0 };
221
222 debug_assert!(!matches!(item.kind, CoverageItemKind::Line));
223 self.all_lines.push(item.loc.lines.start);
224
225 self.items.push(item);
226 self.items.last_mut().unwrap()
227 }
228
229 fn source_location_for(&self, mut span: Span) -> SourceLocation {
230 if let Ok(snippet) = self.gcx.sess.source_map().span_to_snippet(span)
232 && let Some(stripped) = snippet.strip_suffix(';')
233 {
234 let stripped = stripped.trim_end();
235 let skipped = snippet.len() - stripped.len();
236 span = span.with_hi(span.hi() - BytePos::from_usize(skipped));
237 }
238
239 SourceLocation {
240 source_id: self.source_id as usize,
241 contract_name: self.contract_name.clone(),
242 bytes: self.byte_range(span),
243 lines: self.line_range(span),
244 }
245 }
246
247 fn byte_range(&self, span: Span) -> Range<u32> {
248 let bytes_usize = self.gcx.sess.source_map().span_to_source(span).unwrap().data;
249 bytes_usize.start as u32..bytes_usize.end as u32
250 }
251
252 fn line_range(&self, span: Span) -> Range<u32> {
253 let lines = self.gcx.sess.source_map().span_to_lines(span).unwrap().data;
254 assert!(!lines.is_empty());
255 let first = lines.first().unwrap();
256 let last = lines.last().unwrap();
257 first.line_index as u32 + 1..last.line_index as u32 + 2
258 }
259
260 const fn next_branch_id(&mut self) -> u32 {
261 let id = self.branch_id;
262 self.branch_id = id + 1;
263 id
264 }
265}
266
267impl<'ast> ast::Visit<'ast> for SourceVisitor<'_> {
268 type BreakValue = Never;
269
270 fn visit_item_contract(
271 &mut self,
272 contract: &'ast ast::ItemContract<'ast>,
273 ) -> ControlFlow<Self::BreakValue> {
274 self.contract_name = contract.name.as_str().into();
275 self.walk_item_contract(contract)
276 }
277
278 #[expect(clippy::single_match)]
279 fn visit_item(&mut self, item: &'ast ast::Item<'ast>) -> ControlFlow<Self::BreakValue> {
280 match &item.kind {
281 ItemKind::Function(func) => {
282 if func.kind == ast::FunctionKind::Modifier && !has_statements(func.body.as_ref()) {
285 return ControlFlow::Continue(());
286 }
287
288 let name = func.header.name.as_ref().map(|n| n.as_str()).unwrap_or_else(|| {
289 match func.kind {
290 ast::FunctionKind::Constructor => "constructor",
291 ast::FunctionKind::Receive => "receive",
292 ast::FunctionKind::Fallback => "fallback",
293 ast::FunctionKind::Function | ast::FunctionKind::Modifier => unreachable!(),
294 }
295 });
296
297 let exclude_func = func.header.virtual_() && !func.is_implemented();
299 if !exclude_func {
300 self.push_item_kind(
301 CoverageItemKind::Function { name: name.into() },
302 item.span,
303 );
304 }
305
306 self.walk_item(item)?;
307 }
308 _ => {}
309 }
310 ControlFlow::Continue(())
312 }
313
314 fn visit_stmt(&mut self, stmt: &'ast ast::Stmt<'ast>) -> ControlFlow<Self::BreakValue> {
315 match &stmt.kind {
316 StmtKind::Break | StmtKind::Continue | StmtKind::Emit(..) | StmtKind::Revert(..) => {
317 self.push_stmt(stmt.span);
318 return ControlFlow::Continue(());
320 }
321 StmtKind::Return(_) | StmtKind::DeclSingle(_) | StmtKind::DeclMulti(..) => {
322 self.push_stmt(stmt.span);
323 }
324
325 StmtKind::If(_cond, then_stmt, else_stmt) => {
326 let branch_id = self.next_branch_id();
327
328 if stmt_has_statements(then_stmt)
330 || else_stmt.as_ref().is_some_and(|s| stmt_has_statements(s))
331 {
332 self.push_item_kind(
335 CoverageItemKind::Branch { branch_id, path_id: 0, is_first_opcode: true },
336 then_stmt.span,
337 );
338 if let Some(else_stmt) = else_stmt {
339 let is_first_opcode = stmt_has_statements(else_stmt);
340 let anchor_loc =
341 is_first_opcode.then(|| self.source_location_for(else_stmt.span));
342 self.push_item_kind(
343 CoverageItemKind::Branch { branch_id, path_id: 1, is_first_opcode },
344 stmt.span,
345 )
346 .anchor_loc = anchor_loc;
347 }
348 }
349 }
350
351 StmtKind::Try(ast::StmtTry { expr: _, clauses }) => {
352 let branch_id = self.next_branch_id();
353
354 let mut path_id = 0;
355 for catch in clauses.iter() {
356 let ast::TryCatchClause { span, name: _, args, block } = catch;
357 let span = if path_id == 0 { stmt.span.to(*span) } else { *span };
358 if path_id == 0 || has_statements(Some(block)) {
359 self.push_item_kind(
360 CoverageItemKind::Branch { branch_id, path_id, is_first_opcode: true },
361 span,
362 );
363 path_id += 1;
364 } else if !args.is_empty() {
365 self.push_stmt(span);
369 }
370 }
371 }
372
373 StmtKind::Assembly(_)
375 | StmtKind::Block(_)
376 | StmtKind::UncheckedBlock(_)
377 | StmtKind::Placeholder
378 | StmtKind::Expr(_)
379 | StmtKind::While(..)
380 | StmtKind::DoWhile(..)
381 | StmtKind::For { .. } => {}
382 }
383 self.walk_stmt(stmt)
384 }
385
386 fn visit_expr(&mut self, expr: &'ast ast::Expr<'ast>) -> ControlFlow<Self::BreakValue> {
387 match &expr.kind {
388 ExprKind::Assign(..)
389 | ExprKind::Unary(..)
390 | ExprKind::Binary(..)
391 | ExprKind::Ternary(..) => {
392 self.push_stmt(expr.span);
393 if matches!(expr.kind, ExprKind::Binary(..)) {
394 return self.walk_expr(expr);
395 }
396 }
397 ExprKind::Call(callee, _args) => {
398 self.function_calls.push((expr.span, self.contract_name.clone()));
400
401 if let ExprKind::Ident(ident) = &callee.kind {
402 if ident.as_str() == "require" {
405 let branch_id = self.next_branch_id();
406 self.push_item_kind(
407 CoverageItemKind::Branch {
408 branch_id,
409 path_id: 0,
410 is_first_opcode: false,
411 },
412 expr.span,
413 );
414 self.push_item_kind(
415 CoverageItemKind::Branch {
416 branch_id,
417 path_id: 1,
418 is_first_opcode: false,
419 },
420 expr.span,
421 );
422 }
423 }
424 }
425 _ => {}
426 }
427 ControlFlow::Continue(())
429 }
430
431 fn visit_yul_stmt(&mut self, stmt: &'ast yul::Stmt<'ast>) -> ControlFlow<Self::BreakValue> {
432 use yul::StmtKind;
433 match &stmt.kind {
434 StmtKind::VarDecl(..)
435 | StmtKind::AssignSingle(..)
436 | StmtKind::AssignMulti(..)
437 | StmtKind::Leave
438 | StmtKind::Break
439 | StmtKind::Continue => {
440 self.push_stmt(stmt.span);
441 return ControlFlow::Continue(());
443 }
444 StmtKind::If(..) => {
445 let branch_id = self.next_branch_id();
446 self.push_item_kind(
447 CoverageItemKind::Branch { branch_id, path_id: 0, is_first_opcode: false },
448 stmt.span,
449 );
450 }
451 StmtKind::For(yul::StmtFor { body, .. }) => {
452 self.push_stmt(body.span);
453 }
454 StmtKind::Switch(switch) => {
455 for case in switch.cases.iter() {
456 self.push_stmt(case.span);
457 self.push_stmt(case.body.span);
458 }
459 }
460 StmtKind::FunctionDef(func) => {
461 let name = func.name.as_str();
462 self.push_item_kind(CoverageItemKind::Function { name: name.into() }, stmt.span);
463 }
464 StmtKind::Expr(_) => {
466 self.push_stmt(stmt.span);
467 return ControlFlow::Continue(());
468 }
469 StmtKind::Block(_) => {}
470 }
471 self.walk_yul_stmt(stmt)
472 }
473
474 fn visit_yul_expr(&mut self, expr: &'ast yul::Expr<'ast>) -> ControlFlow<Self::BreakValue> {
475 use yul::ExprKind;
476 match &expr.kind {
477 ExprKind::Path(_) | ExprKind::Lit(_) => {}
478 ExprKind::Call(_) => self.push_stmt(expr.span),
479 }
480 ControlFlow::Continue(())
482 }
483}
484
485impl<'gcx> hir::Visit<'gcx> for SourceVisitor<'gcx> {
486 type BreakValue = Never;
487
488 fn hir(&self) -> &'gcx hir::Hir<'gcx> {
489 &self.gcx.hir
490 }
491
492 fn visit_expr(&mut self, expr: &'gcx hir::Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
493 if let hir::ExprKind::Call(lhs, ..) = &expr.kind
494 && is_regular_call(lhs)
495 && let Some(scope) = self.function_call_scopes.get(&expr.span).cloned()
496 {
497 let prev = std::mem::replace(&mut self.contract_name, scope);
500 self.push_stmt(expr.span);
501 self.contract_name = prev;
502 }
503 self.walk_expr(expr)
504 }
505}
506
507fn is_regular_call(lhs: &hir::Expr<'_>) -> bool {
510 match lhs.peel_parens().kind {
511 hir::ExprKind::Ident([hir::Res::Item(hir::ItemId::Struct(_))]) => false,
513 hir::ExprKind::Type(_) => false,
515 _ => true,
516 }
517}
518
519fn has_statements(block: Option<&ast::Block<'_>>) -> bool {
520 block.is_some_and(|block| !block.is_empty())
521}
522
523fn stmt_has_statements(stmt: &ast::Stmt<'_>) -> bool {
524 match &stmt.kind {
525 StmtKind::Assembly(a) => !a.block.is_empty(),
526 StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => has_statements(Some(b)),
527 _ => true,
528 }
529}
530
531#[derive(Clone, Debug, Default)]
533pub struct SourceAnalysis {
534 all_items: Vec<CoverageItem>,
536 map: Vec<(u32, u32)>,
538 empty_special_functions: HashMap<u32, EmptySpecialFunctionKind>,
540 contract_empty_special_functions: HashMap<u32, HashMap<Arc<str>, Vec<u32>>>,
542}
543
544impl SourceAnalysis {
545 #[instrument(name = "SourceAnalysis::new", skip_all)]
559 pub fn new(data: &SourceFiles, output: &ProjectCompileOutput) -> eyre::Result<Self> {
560 let mut resolved_empty_special_functions = Vec::new();
561 let mut sourced_items = output.parser().solc().compiler().enter(|compiler| {
562 resolved_empty_special_functions =
563 resolve_empty_special_functions(compiler.gcx(), data);
564 data.sources
565 .par_iter()
566 .map(|(&source_id, path)| {
567 let _guard = debug_span!("SourceAnalysis::new::visit", ?path).entered();
568
569 let (_, source) = compiler.gcx().get_ast_source(path).unwrap();
570 let ast = source.ast.as_ref().unwrap();
571 let (hir_source_id, _) = compiler.gcx().get_hir_source(path).unwrap();
572
573 let mut visitor = SourceVisitor::new(source_id, compiler.gcx());
574 for item in ast.items.iter() {
575 match &item.kind {
576 ItemKind::Contract(contract) => {
578 if contract.kind.is_interface() {
580 continue;
581 }
582
583 let checkpoint = visitor.checkpoint();
584 visitor.visit_contract(contract);
585 if visitor.has_tests(&checkpoint) {
586 visitor.restore_checkpoint(checkpoint);
587 }
588 }
589 ItemKind::Function(_) => {
593 visitor.contract_name = Arc::default();
597 let _ = ast::Visit::visit_item(&mut visitor, item);
598 }
599 _ => {}
600 }
601 }
602
603 if !visitor.function_calls.is_empty() {
604 visitor.resolve_function_calls(hir_source_id);
605 }
606
607 if !visitor.items.is_empty() {
608 visitor.disambiguate_functions();
609 visitor.sort();
610 visitor.push_lines();
611 visitor.sort();
612 }
613 (source_id, visitor.items)
614 })
615 .collect::<Vec<(u32, Vec<CoverageItem>)>>()
616 });
617
618 sourced_items.sort_by_key(|(id, items)| (*id, items.first().map(|i| i.loc.bytes.start)));
620 let Some(&(max_idx, _)) = sourced_items.last() else { return Ok(Self::default()) };
621 let len = max_idx + 1;
622 let mut all_items = Vec::new();
623 let mut map = vec![(u32::MAX, 0); len as usize];
624 for (idx, items) in sourced_items {
625 let idx = idx as usize;
627 if map[idx].0 == u32::MAX {
628 map[idx].0 = all_items.len() as u32;
629 }
630 map[idx].1 += items.len() as u32;
631 all_items.extend(items);
632 }
633
634 let mut empty_special_functions = HashMap::default();
635 let mut contract_empty_special_functions =
636 HashMap::<u32, HashMap<Arc<str>, Vec<u32>>>::default();
637 for resolved in resolved_empty_special_functions {
638 let item_ids = all_items
639 .iter()
640 .enumerate()
641 .filter(|(_, item)| {
642 item.loc.source_id == resolved.function_source_id as usize
643 && item.loc.contract_name == resolved.function_contract_name
644 && item.loc.bytes == resolved.function_bytes
645 })
646 .map(|(item_id, _)| item_id as u32)
647 .collect::<Vec<_>>();
648 empty_special_functions
649 .extend(item_ids.iter().map(|&item_id| (item_id, resolved.kind)));
650 contract_empty_special_functions
651 .entry(resolved.contract_source_id)
652 .or_default()
653 .entry(resolved.contract_name)
654 .or_default()
655 .extend(item_ids);
656 }
657
658 Ok(Self { all_items, map, empty_special_functions, contract_empty_special_functions })
659 }
660
661 pub fn all_items(&self) -> &[CoverageItem] {
663 &self.all_items
664 }
665
666 pub const fn all_items_mut(&mut self) -> &mut Vec<CoverageItem> {
668 &mut self.all_items
669 }
670
671 pub fn items_for_source_enumerated(
673 &self,
674 source_id: u32,
675 ) -> impl Iterator<Item = (u32, &CoverageItem)> {
676 let (base_id, items) = self.items_for_source(source_id);
677 items.iter().enumerate().map(move |(idx, item)| (base_id + idx as u32, item))
678 }
679
680 pub fn items_for_source(&self, source_id: u32) -> (u32, &[CoverageItem]) {
682 let (mut offset, len) = self.map.get(source_id as usize).copied().unwrap_or_default();
683 if offset == u32::MAX {
684 offset = 0;
685 }
686 (offset, &self.all_items[offset as usize..][..len as usize])
687 }
688
689 #[inline]
691 pub fn get(&self, item_id: u32) -> Option<&CoverageItem> {
692 self.all_items.get(item_id as usize)
693 }
694
695 pub(crate) fn empty_special_function_kind(
696 &self,
697 item_id: u32,
698 ) -> Option<EmptySpecialFunctionKind> {
699 self.empty_special_functions.get(&item_id).copied()
700 }
701
702 pub(crate) fn empty_special_function_ids(
703 &self,
704 source_id: u32,
705 contract_name: &str,
706 ) -> impl Iterator<Item = u32> + '_ {
707 self.contract_empty_special_functions
708 .get(&source_id)
709 .and_then(|contracts| contracts.get(contract_name))
710 .into_iter()
711 .flatten()
712 .copied()
713 }
714}
715
716#[derive(Default)]
718pub struct SourceFiles {
719 pub sources: HashMap<u32, PathBuf>,
721}