Skip to main content

foundry_evm_coverage/
analysis.rs

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/// 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    /// Deferred function-call spans, each paired with the contract scope active where the call
37    /// was collected, so scope travels with the span to the delayed HIR resolution pass.
38    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    /// Returns `true` if the contract has any test functions.
148    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    /// Disambiguate overloaded functions that share a name within the same scope (a contract, or
159    /// the file level for free functions). Keyed by scope so a contract method and a same-named
160    /// free function are not treated as duplicates of each other.
161    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        // Items are already ordered by line, so advance through them only once.
195        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    /// Creates a coverage item for a given kind and source location. Pushes item to the internal
217    /// collection (plus additional coverage line if item is a statement).
218    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        // Statements' ranges in the solc source map do not include the semicolon.
231        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                // Empty modifiers have no bytecode of their own to anchor. Empty constructors,
283                // receive functions, and fallbacks are handled separately.
284                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                // Exclude function from coverage report if it is virtual without implementation.
298                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        // Only walk functions.
311        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                // TODO(dani): these probably shouldn't be excluded.
319                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                // Add branch coverage items only if one of true/branch bodies contains statements.
329                if stmt_has_statements(then_stmt)
330                    || else_stmt.as_ref().is_some_and(|s| stmt_has_statements(s))
331                {
332                    // The branch instruction is mapped to the first opcode within the true
333                    // body source range.
334                    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                        // Add coverage for clause with parameters and empty statements.
366                        // (`catch (bytes memory reason) {}`).
367                        // Catch all clause without statements is ignored (`catch {}`).
368                        self.push_stmt(span);
369                    }
370                }
371            }
372
373            // Skip placeholder statements as they are never referenced in source maps.
374            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                // Resolve later. Capture the current contract scope so it travels with the span.
399                self.function_calls.push((expr.span, self.contract_name.clone()));
400
401                if let ExprKind::Ident(ident) = &callee.kind {
402                    // Might be a require call, add branch coverage.
403                    // Asserts should not be considered branches: <https://github.com/foundry-rs/foundry/issues/9460>.
404                    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        // Intentionally do not walk all expressions.
428        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                // Don't walk assignments.
442                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            // TODO(dani): merge with Block below on next solar release: https://github.com/paradigmxyz/solar/pull/496
465            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        // Intentionally do not walk all expressions.
481        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            // Attribute the call with the scope captured where it was collected, not the
498            // visitor's final scope (which leaks across free functions and contracts).
499            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
507// https://github.com/argotorg/solidity/blob/965166317bbc2b02067eb87f222a2dce9d24e289/libsolidity/ast/ASTAnnotations.h#L336-L341
508// https://github.com/argotorg/solidity/blob/965166317bbc2b02067eb87f222a2dce9d24e289/libsolidity/analysis/TypeChecker.cpp#L2720
509fn is_regular_call(lhs: &hir::Expr<'_>) -> bool {
510    match lhs.peel_parens().kind {
511        // StructConstructorCall
512        hir::ExprKind::Ident([hir::Res::Item(hir::ItemId::Struct(_))]) => false,
513        // TypeConversion
514        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/// Coverage source analysis.
532#[derive(Clone, Debug, Default)]
533pub struct SourceAnalysis {
534    /// All the coverage items.
535    all_items: Vec<CoverageItem>,
536    /// Source ID to `(offset, len)` into `all_items`.
537    map: Vec<(u32, u32)>,
538    /// Empty receive and fallback items keyed by coverage item ID.
539    empty_special_functions: HashMap<u32, EmptySpecialFunctionKind>,
540    /// Empty receive and fallback item IDs resolved for each contract, including inheritance.
541    contract_empty_special_functions: HashMap<u32, HashMap<Arc<str>, Vec<u32>>>,
542}
543
544impl SourceAnalysis {
545    /// Analyzes the sources held by the source analyzer.
546    ///
547    /// Coverage items are found by:
548    /// - Walking the AST of each contract (except interfaces)
549    /// - Walking file-level (free) functions
550    /// - Recording the items found
551    ///
552    /// Each coverage item contains relevant information to find opcodes corresponding to them: the
553    /// source ID the item is in, the source code range of the item, and the contract name the item
554    /// is in.
555    ///
556    /// Note: Source IDs are only unique per compilation job, so report-level source identity must
557    /// also include the compiler build ID.
558    #[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                            // Contracts: walk their functions, dropping test contracts.
577                            ItemKind::Contract(contract) => {
578                                // Skip interfaces which have no function implementations.
579                                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                            // File-level (free) functions are covered too, not only functions
590                            // defined inside a contract. Without this, a file of only free
591                            // functions gets no coverage record at all.
592                            ItemKind::Function(_) => {
593                                // A free function is not scoped to any contract. Clear any scope
594                                // left by a previously visited contract so it is attributed at
595                                // file level, not as `Contract.freeFn`.
596                                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        // Create mapping and merge items.
619        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            // Assumes that all `idx` items are consecutive, guaranteed by the sort above.
626            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    /// Returns all the coverage items.
662    pub fn all_items(&self) -> &[CoverageItem] {
663        &self.all_items
664    }
665
666    /// Returns all the mutable coverage items.
667    pub const fn all_items_mut(&mut self) -> &mut Vec<CoverageItem> {
668        &mut self.all_items
669    }
670
671    /// Returns an iterator over the coverage items and their IDs for the given source.
672    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    /// Returns the base item ID and all the coverage items for the given source.
681    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    /// Returns the coverage item for the given item ID.
690    #[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/// A list of sources from one compiler build.
717#[derive(Default)]
718pub struct SourceFiles {
719    /// The sources keyed by their IDs within the compiler build.
720    pub sources: HashMap<u32, PathBuf>,
721}