Skip to main content

foundry_evm_coverage/
analysis.rs

1use super::{CoverageItem, CoverageItemKind, SourceLocation};
2use alloy_primitives::map::{HashMap, HashSet};
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/// A visitor that walks the AST of a single contract and finds coverage items.
19#[derive(Clone)]
20struct SourceVisitor<'gcx> {
21    /// The source ID of the contract.
22    source_id: u32,
23    /// The solar session for span resolution.
24    gcx: Gcx<'gcx>,
25
26    /// The name of the contract being walked.
27    contract_name: Arc<str>,
28
29    /// The current branch ID
30    branch_id: u32,
31
32    /// Coverage items
33    items: Vec<CoverageItem>,
34
35    all_lines: Vec<u32>,
36    /// Branch IDs whose jumps must match the exact ternary expression span.
37    ternary_branches: Vec<u32>,
38    /// Deferred function-call spans, each paired with the contract scope active where the call
39    /// was collected, so scope travels with the span to the delayed HIR resolution pass.
40    function_calls: Vec<(Span, Arc<str>)>,
41    function_call_scopes: FxHashMap<Span, Arc<str>>,
42}
43
44struct SourceVisitorCheckpoint {
45    items: usize,
46    all_lines: usize,
47    function_calls: usize,
48    ternary_branches: usize,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub(crate) enum EmptySpecialFunctionKind {
53    Constructor,
54    Receive,
55    Fallback,
56}
57
58struct ResolvedEmptySpecialFunction {
59    contract_source_id: u32,
60    contract_name: Arc<str>,
61    function_source_id: u32,
62    function_contract_name: Arc<str>,
63    function_bytes: Range<u32>,
64    kind: EmptySpecialFunctionKind,
65}
66
67fn resolve_empty_special_functions(
68    gcx: Gcx<'_>,
69    data: &SourceFiles,
70) -> Vec<ResolvedEmptySpecialFunction> {
71    let hir_source_ids = data
72        .sources
73        .iter()
74        .map(|(&source_id, path)| {
75            let (hir_source_id, _) = gcx.get_hir_source(path).unwrap();
76            (hir_source_id, source_id)
77        })
78        .collect::<HashMap<_, _>>();
79    let mut resolved = Vec::new();
80    for contract_id in gcx.hir.contract_ids() {
81        let contract = gcx.hir.contract(contract_id);
82        let Some(&contract_source_id) = hir_source_ids.get(&contract.source) else { continue };
83        let constructors = contract.linearized_bases.iter().filter_map(|&base_id| {
84            gcx.hir
85                .contract(base_id)
86                .ctor
87                .map(|function_id| (function_id, EmptySpecialFunctionKind::Constructor))
88        });
89        let runtime_functions = [
90            (contract.receive, EmptySpecialFunctionKind::Receive),
91            (contract.fallback, EmptySpecialFunctionKind::Fallback),
92        ]
93        .into_iter()
94        .filter_map(|(function_id, kind)| function_id.map(|function_id| (function_id, kind)));
95        for (function_id, kind) in constructors.chain(runtime_functions) {
96            let function = gcx.hir.function(function_id);
97            if !function.body.is_some_and(|body| body.is_empty()) {
98                continue;
99            }
100            let Some(&function_source_id) = hir_source_ids.get(&function.source) else { continue };
101            let Some(function_contract_id) = function.contract else { continue };
102            let function_contract = gcx.hir.contract(function_contract_id);
103            let function_bytes = gcx.sess.source_map().span_to_source(function.span).unwrap().data;
104            resolved.push(ResolvedEmptySpecialFunction {
105                contract_source_id,
106                contract_name: contract.name.as_str().into(),
107                function_source_id,
108                function_contract_name: function_contract.name.as_str().into(),
109                function_bytes: function_bytes.start as u32..function_bytes.end as u32,
110                kind,
111            });
112        }
113    }
114    resolved
115}
116
117impl<'gcx> SourceVisitor<'gcx> {
118    fn new(source_id: u32, gcx: Gcx<'gcx>) -> Self {
119        Self {
120            source_id,
121            gcx,
122            contract_name: Arc::default(),
123            branch_id: 0,
124            ternary_branches: Default::default(),
125            all_lines: Default::default(),
126            function_calls: Default::default(),
127            function_call_scopes: Default::default(),
128            items: Default::default(),
129        }
130    }
131
132    const fn checkpoint(&self) -> SourceVisitorCheckpoint {
133        SourceVisitorCheckpoint {
134            items: self.items.len(),
135            all_lines: self.all_lines.len(),
136            function_calls: self.function_calls.len(),
137            ternary_branches: self.ternary_branches.len(),
138        }
139    }
140
141    fn restore_checkpoint(&mut self, checkpoint: SourceVisitorCheckpoint) {
142        let SourceVisitorCheckpoint { items, all_lines, function_calls, ternary_branches } =
143            checkpoint;
144        self.items.truncate(items);
145        self.all_lines.truncate(all_lines);
146        self.function_calls.truncate(function_calls);
147        self.ternary_branches.truncate(ternary_branches);
148    }
149
150    fn visit_contract<'ast>(&mut self, contract: &'ast ast::ItemContract<'ast>) {
151        let _ = ast::Visit::visit_item_contract(self, contract);
152    }
153
154    /// Returns `true` if the contract has any test functions.
155    fn has_tests(&self, checkpoint: &SourceVisitorCheckpoint) -> bool {
156        self.items[checkpoint.items..].iter().any(|item| {
157            if let CoverageItemKind::Function { name } = &item.kind {
158                name.is_any_test()
159            } else {
160                false
161            }
162        })
163    }
164
165    /// Disambiguate overloaded functions that share a name within the same scope (a contract, or
166    /// the file level for free functions). Keyed by scope so a contract method and a same-named
167    /// free function are not treated as duplicates of each other.
168    fn disambiguate_functions(&mut self) {
169        let mut dups = HashMap::<_, Vec<usize>>::default();
170        for (i, item) in self.items.iter().enumerate() {
171            if let CoverageItemKind::Function { name } = &item.kind {
172                dups.entry((item.loc.contract_name.clone(), name.clone())).or_default().push(i);
173            }
174        }
175        for dups in dups.values() {
176            if dups.len() > 1 {
177                for (i, &dup) in dups.iter().enumerate() {
178                    let item = &mut self.items[dup];
179                    if let CoverageItemKind::Function { name } = &item.kind {
180                        item.kind =
181                            CoverageItemKind::Function { name: format!("{name}.{i}").into() };
182                    }
183                }
184            }
185        }
186    }
187
188    fn resolve_function_calls(&mut self, hir_source_id: hir::SourceId) {
189        self.function_call_scopes = self.function_calls.iter().cloned().collect();
190        let _ = hir::Visit::visit_nested_source(self, hir_source_id);
191    }
192
193    fn sort(&mut self) {
194        self.items.sort();
195    }
196
197    fn push_lines(&mut self) {
198        self.all_lines.sort_unstable();
199        self.all_lines.dedup();
200        let mut lines = Vec::with_capacity(self.all_lines.len());
201        // Items are already ordered by line, so advance through them only once.
202        let mut items = self.items.iter().peekable();
203        for &line in &self.all_lines {
204            while items.peek().is_some_and(|item| item.loc.lines.start < line) {
205                items.next();
206            }
207            if let Some(mut reference_item) = items.next_if(|item| item.loc.lines.start == line) {
208                // Anchor the line to its earliest source item, not the shortest span or a
209                // statement in a conditional body that may never execute.
210                while let Some(item) = items.next_if(|item| item.loc.lines.start == line) {
211                    if item.loc.bytes.start < reference_item.loc.bytes.start {
212                        reference_item = item;
213                    }
214                }
215                lines.push(CoverageItem {
216                    kind: CoverageItemKind::Line,
217                    loc: reference_item.loc.clone(),
218                    anchor_loc: None,
219                    hits: 0,
220                });
221            }
222        }
223        self.items.extend(lines);
224    }
225
226    fn push_stmt(&mut self, span: Span) {
227        self.push_item_kind(CoverageItemKind::Statement, span);
228    }
229
230    /// Creates a coverage item for a given kind and source location. Pushes item to the internal
231    /// collection (plus additional coverage line if item is a statement).
232    fn push_item_kind(&mut self, kind: CoverageItemKind, span: Span) -> &mut CoverageItem {
233        let item =
234            CoverageItem { kind, loc: self.source_location_for(span), anchor_loc: None, hits: 0 };
235
236        debug_assert!(!matches!(item.kind, CoverageItemKind::Line));
237        self.all_lines.push(item.loc.lines.start);
238
239        self.items.push(item);
240        self.items.last_mut().unwrap()
241    }
242
243    fn source_location_for(&self, mut span: Span) -> SourceLocation {
244        // Statements' ranges in the solc source map do not include the semicolon.
245        if let Ok(snippet) = self.gcx.sess.source_map().span_to_snippet(span)
246            && let Some(stripped) = snippet.strip_suffix(';')
247        {
248            let stripped = stripped.trim_end();
249            let skipped = snippet.len() - stripped.len();
250            span = span.with_hi(span.hi() - BytePos::from_usize(skipped));
251        }
252
253        SourceLocation {
254            source_id: self.source_id as usize,
255            contract_name: self.contract_name.clone(),
256            bytes: self.byte_range(span),
257            lines: self.line_range(span),
258        }
259    }
260
261    fn byte_range(&self, span: Span) -> Range<u32> {
262        let bytes_usize = self.gcx.sess.source_map().span_to_source(span).unwrap().data;
263        bytes_usize.start as u32..bytes_usize.end as u32
264    }
265
266    fn line_range(&self, span: Span) -> Range<u32> {
267        let lines = self.gcx.sess.source_map().span_to_lines(span).unwrap().data;
268        assert!(!lines.is_empty());
269        let first = lines.first().unwrap();
270        let last = lines.last().unwrap();
271        first.line_index as u32 + 1..last.line_index as u32 + 2
272    }
273
274    const fn next_branch_id(&mut self) -> u32 {
275        let id = self.branch_id;
276        self.branch_id = id + 1;
277        id
278    }
279}
280
281impl<'ast> ast::Visit<'ast> for SourceVisitor<'_> {
282    type BreakValue = Never;
283
284    fn visit_item_contract(
285        &mut self,
286        contract: &'ast ast::ItemContract<'ast>,
287    ) -> ControlFlow<Self::BreakValue> {
288        self.contract_name = contract.name.as_str().into();
289        self.walk_item_contract(contract)
290    }
291
292    #[expect(clippy::single_match)]
293    fn visit_item(&mut self, item: &'ast ast::Item<'ast>) -> ControlFlow<Self::BreakValue> {
294        match &item.kind {
295            ItemKind::Function(func) => {
296                // Empty modifiers have no bytecode of their own to anchor. Empty constructors,
297                // receive functions, and fallbacks are handled separately.
298                if func.kind == ast::FunctionKind::Modifier && !has_statements(func.body.as_ref()) {
299                    return ControlFlow::Continue(());
300                }
301
302                let name = func.header.name.as_ref().map(|n| n.as_str()).unwrap_or_else(|| {
303                    match func.kind {
304                        ast::FunctionKind::Constructor => "constructor",
305                        ast::FunctionKind::Receive => "receive",
306                        ast::FunctionKind::Fallback => "fallback",
307                        ast::FunctionKind::Function | ast::FunctionKind::Modifier => unreachable!(),
308                    }
309                });
310
311                // Exclude function from coverage report if it is virtual without implementation.
312                let exclude_func = func.header.virtual_() && !func.is_implemented();
313                if !exclude_func {
314                    self.push_item_kind(
315                        CoverageItemKind::Function { name: name.into() },
316                        item.span,
317                    );
318                }
319
320                // Discover decisions without changing the ordinary statement/call traversal.
321                TernaryVisitor(self).walk_item(item)?;
322                self.walk_item(item)?;
323            }
324            _ => {}
325        }
326        // Only walk functions.
327        ControlFlow::Continue(())
328    }
329
330    fn visit_stmt(&mut self, stmt: &'ast ast::Stmt<'ast>) -> ControlFlow<Self::BreakValue> {
331        match &stmt.kind {
332            StmtKind::Break | StmtKind::Continue | StmtKind::Emit(..) | StmtKind::Revert(..) => {
333                self.push_stmt(stmt.span);
334                // TODO(dani): these probably shouldn't be excluded.
335                return ControlFlow::Continue(());
336            }
337            StmtKind::Return(_) | StmtKind::DeclSingle(_) | StmtKind::DeclMulti(..) => {
338                self.push_stmt(stmt.span);
339            }
340
341            StmtKind::If(_cond, then_stmt, else_stmt) => {
342                let branch_id = self.next_branch_id();
343
344                // Add branch coverage items only if one of true/branch bodies contains statements.
345                if stmt_has_statements(then_stmt)
346                    || else_stmt.as_ref().is_some_and(|s| stmt_has_statements(s))
347                {
348                    // Report the branch at the condition, but count hits in the true body.
349                    // Line coverage uses the reported span so it also sees false conditions.
350                    let anchor_loc = self.source_location_for(then_stmt.span);
351                    self.push_item_kind(
352                        CoverageItemKind::Branch { branch_id, path_id: 0, is_first_opcode: true },
353                        stmt.span,
354                    )
355                    .anchor_loc = Some(anchor_loc);
356                    if let Some(else_stmt) = else_stmt {
357                        let is_first_opcode = stmt_has_statements(else_stmt);
358                        let anchor_loc =
359                            is_first_opcode.then(|| self.source_location_for(else_stmt.span));
360                        self.push_item_kind(
361                            CoverageItemKind::Branch { branch_id, path_id: 1, is_first_opcode },
362                            stmt.span,
363                        )
364                        .anchor_loc = anchor_loc;
365                    }
366                }
367            }
368
369            StmtKind::Try(ast::StmtTry { expr: _, clauses }) => {
370                let branch_id = self.next_branch_id();
371
372                let mut path_id = 0;
373                for catch in clauses.iter() {
374                    let ast::TryCatchClause { span, name: _, args, block } = catch;
375                    let span = if path_id == 0 { stmt.span.to(*span) } else { *span };
376                    if path_id == 0 || has_statements(Some(block)) {
377                        self.push_item_kind(
378                            CoverageItemKind::Branch { branch_id, path_id, is_first_opcode: true },
379                            span,
380                        );
381                        path_id += 1;
382                    } else if !args.is_empty() {
383                        // Add coverage for clause with parameters and empty statements.
384                        // (`catch (bytes memory reason) {}`).
385                        // Catch all clause without statements is ignored (`catch {}`).
386                        self.push_stmt(span);
387                    }
388                }
389            }
390
391            // Skip placeholder statements as they are never referenced in source maps.
392            StmtKind::Assembly(_)
393            | StmtKind::Block(_)
394            | StmtKind::UncheckedBlock(_)
395            | StmtKind::Placeholder
396            | StmtKind::Expr(_)
397            | StmtKind::While(..)
398            | StmtKind::DoWhile(..)
399            | StmtKind::For { .. } => {}
400        }
401        self.walk_stmt(stmt)
402    }
403
404    fn visit_expr(&mut self, expr: &'ast ast::Expr<'ast>) -> ControlFlow<Self::BreakValue> {
405        match &expr.kind {
406            ExprKind::Assign(..)
407            | ExprKind::Unary(..)
408            | ExprKind::Binary(..)
409            | ExprKind::Ternary(..) => {
410                self.push_stmt(expr.span);
411                if matches!(expr.kind, ExprKind::Binary(..)) {
412                    return self.walk_expr(expr);
413                }
414            }
415            ExprKind::Call(callee, _args) => {
416                // Resolve later. Capture the current contract scope so it travels with the span.
417                self.function_calls.push((expr.span, self.contract_name.clone()));
418
419                if let ExprKind::Ident(ident) = &callee.kind {
420                    // Might be a require call, add branch coverage.
421                    // Asserts should not be considered branches: <https://github.com/foundry-rs/foundry/issues/9460>.
422                    if ident.as_str() == "require" {
423                        let branch_id = self.next_branch_id();
424                        self.push_item_kind(
425                            CoverageItemKind::Branch {
426                                branch_id,
427                                path_id: 0,
428                                is_first_opcode: false,
429                            },
430                            expr.span,
431                        );
432                        self.push_item_kind(
433                            CoverageItemKind::Branch {
434                                branch_id,
435                                path_id: 1,
436                                is_first_opcode: false,
437                            },
438                            expr.span,
439                        );
440                    }
441                }
442            }
443            _ => {}
444        }
445        // Intentionally do not walk all expressions.
446        ControlFlow::Continue(())
447    }
448
449    fn visit_yul_stmt(&mut self, stmt: &'ast yul::Stmt<'ast>) -> ControlFlow<Self::BreakValue> {
450        use yul::StmtKind;
451        match &stmt.kind {
452            StmtKind::VarDecl(..)
453            | StmtKind::AssignSingle(..)
454            | StmtKind::AssignMulti(..)
455            | StmtKind::Leave
456            | StmtKind::Break
457            | StmtKind::Continue => {
458                self.push_stmt(stmt.span);
459                // Don't walk assignments.
460                return ControlFlow::Continue(());
461            }
462            StmtKind::If(..) => {
463                let branch_id = self.next_branch_id();
464                // Track both outcomes, including the implicit path that skips the body.
465                for path_id in 0..2 {
466                    self.push_item_kind(
467                        CoverageItemKind::Branch { branch_id, path_id, is_first_opcode: false },
468                        stmt.span,
469                    );
470                }
471            }
472            StmtKind::For(yul::StmtFor { body, .. }) => {
473                self.push_stmt(body.span);
474            }
475            StmtKind::Switch(switch) => {
476                for case in switch.cases.iter() {
477                    self.push_stmt(case.span);
478                    self.push_stmt(case.body.span);
479                }
480            }
481            StmtKind::FunctionDef(func) => {
482                let name = func.name.as_str();
483                self.push_item_kind(CoverageItemKind::Function { name: name.into() }, stmt.span);
484            }
485            // TODO(dani): merge with Block below on next solar release: https://github.com/paradigmxyz/solar/pull/496
486            StmtKind::Expr(_) => {
487                self.push_stmt(stmt.span);
488                return ControlFlow::Continue(());
489            }
490            StmtKind::Block(_) => {}
491        }
492        self.walk_yul_stmt(stmt)
493    }
494
495    fn visit_yul_expr(&mut self, expr: &'ast yul::Expr<'ast>) -> ControlFlow<Self::BreakValue> {
496        use yul::ExprKind;
497        match &expr.kind {
498            ExprKind::Path(_) | ExprKind::Lit(_) => {}
499            ExprKind::Call(_) => self.push_stmt(expr.span),
500        }
501        // Intentionally do not walk all expressions.
502        ControlFlow::Continue(())
503    }
504}
505
506/// Walks all expression shapes, adding only ternary decisions. Statement coverage remains owned
507/// by the enclosing statement or the ordinary expression visitor.
508struct TernaryVisitor<'a, 'gcx>(&'a mut SourceVisitor<'gcx>);
509
510impl<'ast> ast::Visit<'ast> for TernaryVisitor<'_, '_> {
511    type BreakValue = Never;
512
513    fn visit_expr(&mut self, expr: &'ast ast::Expr<'ast>) -> ControlFlow<Self::BreakValue> {
514        if matches!(expr.kind, ExprKind::Ternary(..)) {
515            let branch_id = self.0.next_branch_id();
516            self.0.ternary_branches.push(branch_id);
517            // Ternary path 0 is the false arm (fallthrough); path 1 is the true arm (jump target).
518            for path_id in 0..2 {
519                self.0.push_item_kind(
520                    CoverageItemKind::Branch { branch_id, path_id, is_first_opcode: false },
521                    expr.span,
522                );
523            }
524        }
525        self.walk_expr(expr)
526    }
527}
528
529impl<'gcx> hir::Visit<'gcx> for SourceVisitor<'gcx> {
530    type BreakValue = Never;
531
532    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
533        &self.gcx.hir
534    }
535
536    fn visit_expr(&mut self, expr: &'gcx hir::Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
537        if let hir::ExprKind::Call(lhs, ..) = &expr.kind
538            && is_regular_call(lhs)
539            && let Some(scope) = self.function_call_scopes.get(&expr.span).cloned()
540        {
541            // Attribute the call with the scope captured where it was collected, not the
542            // visitor's final scope (which leaks across free functions and contracts).
543            let prev = std::mem::replace(&mut self.contract_name, scope);
544            self.push_stmt(expr.span);
545            self.contract_name = prev;
546        }
547        self.walk_expr(expr)
548    }
549}
550
551// https://github.com/argotorg/solidity/blob/965166317bbc2b02067eb87f222a2dce9d24e289/libsolidity/ast/ASTAnnotations.h#L336-L341
552// https://github.com/argotorg/solidity/blob/965166317bbc2b02067eb87f222a2dce9d24e289/libsolidity/analysis/TypeChecker.cpp#L2720
553fn is_regular_call(lhs: &hir::Expr<'_>) -> bool {
554    match lhs.peel_parens().kind {
555        // StructConstructorCall
556        hir::ExprKind::Ident([hir::Res::Item(hir::ItemId::Struct(_))]) => false,
557        // TypeConversion
558        hir::ExprKind::Type(_) => false,
559        _ => true,
560    }
561}
562
563fn has_statements(block: Option<&ast::Block<'_>>) -> bool {
564    block.is_some_and(|block| !block.is_empty())
565}
566
567fn stmt_has_statements(stmt: &ast::Stmt<'_>) -> bool {
568    match &stmt.kind {
569        StmtKind::Assembly(a) => !a.block.is_empty(),
570        StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => has_statements(Some(b)),
571        _ => true,
572    }
573}
574
575/// Coverage source analysis.
576#[derive(Clone, Debug, Default)]
577pub struct SourceAnalysis {
578    /// All the coverage items.
579    all_items: Vec<CoverageItem>,
580    /// Source and branch IDs requiring exact decision-node source-map matching.
581    ternary_branches: HashSet<(u32, u32)>,
582    /// Source ID to `(offset, len)` into `all_items`.
583    map: Vec<(u32, u32)>,
584    /// Empty receive and fallback items keyed by coverage item ID.
585    empty_special_functions: HashMap<u32, EmptySpecialFunctionKind>,
586    /// Empty receive and fallback item IDs resolved for each contract, including inheritance.
587    contract_empty_special_functions: HashMap<u32, HashMap<Arc<str>, Vec<u32>>>,
588}
589
590impl SourceAnalysis {
591    /// Analyzes the sources held by the source analyzer.
592    ///
593    /// Coverage items are found by:
594    /// - Walking the AST of each contract (except interfaces)
595    /// - Walking file-level (free) functions
596    /// - Recording the items found
597    ///
598    /// Each coverage item contains relevant information to find opcodes corresponding to them: the
599    /// source ID the item is in, the source code range of the item, and the contract name the item
600    /// is in.
601    ///
602    /// Note: Source IDs are only unique per compilation job, so report-level source identity must
603    /// also include the compiler build ID.
604    #[instrument(name = "SourceAnalysis::new", skip_all)]
605    pub fn new(data: &SourceFiles, output: &ProjectCompileOutput) -> eyre::Result<Self> {
606        let mut resolved_empty_special_functions = Vec::new();
607        let mut sourced_items = output.parser().solc().compiler().enter(|compiler| {
608            resolved_empty_special_functions =
609                resolve_empty_special_functions(compiler.gcx(), data);
610            data.sources
611                .par_iter()
612                .map(|(&source_id, path)| {
613                    let _guard = debug_span!("SourceAnalysis::new::visit", ?path).entered();
614
615                    let (_, source) = compiler.gcx().get_ast_source(path).unwrap();
616                    let ast = source.ast.as_ref().unwrap();
617                    let (hir_source_id, _) = compiler.gcx().get_hir_source(path).unwrap();
618
619                    let mut visitor = SourceVisitor::new(source_id, compiler.gcx());
620                    for item in ast.items.iter() {
621                        match &item.kind {
622                            // Contracts: walk their functions, dropping test contracts.
623                            ItemKind::Contract(contract) => {
624                                // Skip interfaces which have no function implementations.
625                                if contract.kind.is_interface() {
626                                    continue;
627                                }
628
629                                let checkpoint = visitor.checkpoint();
630                                visitor.visit_contract(contract);
631                                if visitor.has_tests(&checkpoint) {
632                                    visitor.restore_checkpoint(checkpoint);
633                                }
634                            }
635                            // File-level (free) functions are covered too, not only functions
636                            // defined inside a contract. Without this, a file of only free
637                            // functions gets no coverage record at all.
638                            ItemKind::Function(_) => {
639                                // A free function is not scoped to any contract. Clear any scope
640                                // left by a previously visited contract so it is attributed at
641                                // file level, not as `Contract.freeFn`.
642                                visitor.contract_name = Arc::default();
643                                let _ = ast::Visit::visit_item(&mut visitor, item);
644                            }
645                            _ => {}
646                        }
647                    }
648
649                    if !visitor.function_calls.is_empty() {
650                        visitor.resolve_function_calls(hir_source_id);
651                    }
652
653                    if !visitor.items.is_empty() {
654                        visitor.disambiguate_functions();
655                        visitor.sort();
656                        visitor.push_lines();
657                        visitor.sort();
658                    }
659                    (source_id, visitor.items, visitor.ternary_branches)
660                })
661                .collect::<Vec<_>>()
662        });
663
664        // Create mapping and merge items.
665        sourced_items.sort_by_key(|(id, items, _)| (*id, items.first().map(|i| i.loc.bytes.start)));
666        let Some(&(max_idx, _, _)) = sourced_items.last() else { return Ok(Self::default()) };
667        let len = max_idx + 1;
668        let mut all_items = Vec::new();
669        let mut map = vec![(u32::MAX, 0); len as usize];
670        let mut ternary_branches = HashSet::default();
671        for (idx, items, branches) in sourced_items {
672            ternary_branches.extend(branches.into_iter().map(|branch| (idx, branch)));
673            // Assumes that all `idx` items are consecutive, guaranteed by the sort above.
674            let idx = idx as usize;
675            if map[idx].0 == u32::MAX {
676                map[idx].0 = all_items.len() as u32;
677            }
678            map[idx].1 += items.len() as u32;
679            all_items.extend(items);
680        }
681
682        let mut empty_special_functions = HashMap::default();
683        let mut contract_empty_special_functions =
684            HashMap::<u32, HashMap<Arc<str>, Vec<u32>>>::default();
685        for resolved in resolved_empty_special_functions {
686            let item_ids = all_items
687                .iter()
688                .enumerate()
689                .filter(|(_, item)| {
690                    item.loc.source_id == resolved.function_source_id as usize
691                        && item.loc.contract_name == resolved.function_contract_name
692                        && item.loc.bytes == resolved.function_bytes
693                })
694                .map(|(item_id, _)| item_id as u32)
695                .collect::<Vec<_>>();
696            empty_special_functions
697                .extend(item_ids.iter().map(|&item_id| (item_id, resolved.kind)));
698            contract_empty_special_functions
699                .entry(resolved.contract_source_id)
700                .or_default()
701                .entry(resolved.contract_name)
702                .or_default()
703                .extend(item_ids);
704        }
705
706        Ok(Self {
707            all_items,
708            map,
709            ternary_branches,
710            empty_special_functions,
711            contract_empty_special_functions,
712        })
713    }
714
715    pub(crate) fn is_ternary_branch(&self, source_id: u32, branch_id: u32) -> bool {
716        self.ternary_branches.contains(&(source_id, branch_id))
717    }
718
719    /// Returns all the coverage items.
720    pub fn all_items(&self) -> &[CoverageItem] {
721        &self.all_items
722    }
723
724    /// Returns all the mutable coverage items.
725    pub const fn all_items_mut(&mut self) -> &mut Vec<CoverageItem> {
726        &mut self.all_items
727    }
728
729    /// Returns an iterator over the coverage items and their IDs for the given source.
730    pub fn items_for_source_enumerated(
731        &self,
732        source_id: u32,
733    ) -> impl Iterator<Item = (u32, &CoverageItem)> {
734        let (base_id, items) = self.items_for_source(source_id);
735        items.iter().enumerate().map(move |(idx, item)| (base_id + idx as u32, item))
736    }
737
738    /// Returns the base item ID and all the coverage items for the given source.
739    pub fn items_for_source(&self, source_id: u32) -> (u32, &[CoverageItem]) {
740        let (mut offset, len) = self.map.get(source_id as usize).copied().unwrap_or_default();
741        if offset == u32::MAX {
742            offset = 0;
743        }
744        (offset, &self.all_items[offset as usize..][..len as usize])
745    }
746
747    /// Returns the coverage item for the given item ID.
748    #[inline]
749    pub fn get(&self, item_id: u32) -> Option<&CoverageItem> {
750        self.all_items.get(item_id as usize)
751    }
752
753    pub(crate) fn empty_special_function_kind(
754        &self,
755        item_id: u32,
756    ) -> Option<EmptySpecialFunctionKind> {
757        self.empty_special_functions.get(&item_id).copied()
758    }
759
760    pub(crate) fn empty_special_function_ids(
761        &self,
762        source_id: u32,
763        contract_name: &str,
764    ) -> impl Iterator<Item = u32> + '_ {
765        self.contract_empty_special_functions
766            .get(&source_id)
767            .and_then(|contracts| contracts.get(contract_name))
768            .into_iter()
769            .flatten()
770            .copied()
771    }
772}
773
774/// A list of sources from one compiler build.
775#[derive(Default)]
776pub struct SourceFiles {
777    /// The sources keyed by their IDs within the compiler build.
778    pub sources: HashMap<u32, PathBuf>,
779}