Skip to main content

foundry_evm_coverage/
lib.rs

1//! # foundry-evm-coverage
2//!
3//! EVM bytecode coverage analysis.
4
5#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8#[macro_use]
9extern crate tracing;
10
11use alloy_primitives::{
12    Bytes,
13    map::{
14        B256HashMap, HashMap,
15        rustc_hash::{FxHashMap, FxHashSet},
16    },
17};
18use analysis::SourceAnalysis;
19use eyre::Result;
20use foundry_compilers::artifacts::sourcemap::SourceMap;
21use semver::Version;
22use std::{
23    collections::BTreeMap,
24    fmt,
25    num::NonZeroU32,
26    ops::{Deref, DerefMut, Range},
27    path::{Path, PathBuf},
28    sync::Arc,
29};
30
31pub mod analysis;
32pub mod anchors;
33
34mod inspector;
35pub use inspector::LineCoverageCollector;
36
37/// A coverage report.
38///
39/// A coverage report contains coverage items and opcodes corresponding to those items (called
40/// "anchors"). A single coverage item may be referred to by multiple anchors.
41#[derive(Clone, Debug, Default)]
42pub struct CoverageReport {
43    /// A map of compiler build IDs and source IDs to source paths.
44    pub source_paths: HashMap<String, HashMap<usize, PathBuf>>,
45    /// A map of compiler build IDs and source paths to source IDs.
46    pub source_paths_to_ids: HashMap<String, HashMap<PathBuf, usize>>,
47    /// All coverage items for the codebase, keyed by the compiler build ID.
48    pub analyses: HashMap<String, SourceAnalysis>,
49    /// All item anchors for the codebase, keyed by their contract ID.
50    ///
51    /// `(id, (creation, runtime))`
52    pub anchors: HashMap<ContractId, (Vec<ItemAnchor>, Vec<ItemAnchor>)>,
53    /// Execution-based anchors for coverage items without source-mapped bytecode.
54    execution_anchors: HashMap<ContractId, ContractExecutionAnchors>,
55    /// All the bytecode hits for the codebase.
56    pub bytecode_hits: HashMap<ContractId, HitMap>,
57    /// The bytecode -> source mappings.
58    pub source_maps: HashMap<ContractId, (SourceMap, SourceMap)>,
59}
60
61impl CoverageReport {
62    /// Add a source file path.
63    pub fn add_source(&mut self, build_id: String, source_id: usize, path: PathBuf) {
64        self.source_paths.entry(build_id.clone()).or_default().insert(source_id, path.clone());
65        self.source_paths_to_ids.entry(build_id).or_default().insert(path, source_id);
66    }
67
68    /// Get the source ID for a specific source file path.
69    pub fn get_source_id(&self, build_id: &str, path: &Path) -> Option<usize> {
70        self.source_paths_to_ids.get(build_id)?.get(path).copied()
71    }
72
73    /// Get the source path for a source ID in a compiler build.
74    pub fn get_source_path(&self, build_id: &str, source_id: usize) -> Option<&Path> {
75        self.source_paths.get(build_id)?.get(&source_id).map(PathBuf::as_path)
76    }
77
78    /// Add the source maps.
79    pub fn add_source_maps(
80        &mut self,
81        source_maps: impl IntoIterator<Item = (ContractId, (SourceMap, SourceMap))>,
82    ) {
83        self.source_maps.extend(source_maps);
84    }
85
86    /// Add a [`SourceAnalysis`] to this report.
87    pub fn add_analysis(&mut self, build_id: String, analysis: SourceAnalysis) {
88        self.analyses.insert(build_id, analysis);
89    }
90
91    /// Add anchors to this report.
92    ///
93    /// `(id, (creation, runtime))`
94    pub fn add_anchors(
95        &mut self,
96        anchors: impl IntoIterator<Item = (ContractId, (Vec<ItemAnchor>, Vec<ItemAnchor>))>,
97    ) {
98        self.anchors.extend(anchors);
99    }
100
101    /// Adds execution-based anchors for a contract.
102    pub fn add_execution_anchors(
103        &mut self,
104        contract_id: ContractId,
105        anchors: Vec<ExecutionAnchor>,
106        function_selectors: impl IntoIterator<Item = [u8; 4]>,
107        has_receive: bool,
108        fallback_payable: bool,
109    ) {
110        if anchors.is_empty() {
111            return;
112        }
113        self.execution_anchors.insert(
114            contract_id,
115            ContractExecutionAnchors {
116                anchors,
117                function_selectors: function_selectors.into_iter().collect(),
118                has_receive,
119                fallback_payable,
120            },
121        );
122    }
123
124    /// Returns an iterator over coverage summaries by source file path.
125    pub fn summary_by_file(&self) -> impl Iterator<Item = (&Path, CoverageSummary)> {
126        self.items_by_file().map(|(path, items)| {
127            let summary = CoverageSummary::from_items(&items);
128            (path, summary)
129        })
130    }
131
132    /// Returns coverage items by source file path, merging duplicate items from compiler builds.
133    pub fn items_by_file(&self) -> impl Iterator<Item = (&Path, Vec<CoverageItem>)> {
134        let mut by_file = BTreeMap::<&Path, BTreeMap<CoverageItemKey<'_>, CoverageItem>>::new();
135        for (build_id, items) in &self.analyses {
136            for item in items.all_items() {
137                let Some(path) = self.get_source_path(build_id, item.loc.source_id) else {
138                    continue;
139                };
140                by_file
141                    .entry(path)
142                    .or_default()
143                    .entry(CoverageItemKey::new(item))
144                    .and_modify(|merged| merged.hits = merged.hits.saturating_add(item.hits))
145                    .or_insert_with(|| item.clone());
146            }
147        }
148        by_file.into_iter().map(|(path, items)| (path, items.into_values().collect()))
149    }
150
151    /// Processes data from a [`HitMap`] and sets hit counts for coverage items in this coverage
152    /// map.
153    ///
154    /// This function should only be called *after* all the relevant sources have been processed and
155    /// added to the map (see [`add_source`](Self::add_source)).
156    pub fn add_hit_map(
157        &mut self,
158        contract_id: &ContractId,
159        hit_map: &HitMap,
160        is_deployed_code: bool,
161    ) -> Result<()> {
162        // Add bytecode level hits.
163        self.bytecode_hits
164            .entry(contract_id.clone())
165            .and_modify(|m| m.merge(hit_map))
166            .or_insert_with(|| hit_map.clone());
167
168        // Add source level hits.
169        if let Some(anchors) = self.anchors.get(contract_id) {
170            let anchors = if is_deployed_code { &anchors.1 } else { &anchors.0 };
171            for anchor in anchors {
172                if let Some(hits) = hit_map.get(anchor.instruction) {
173                    self.analyses
174                        .get_mut(&contract_id.build_id)
175                        .and_then(|items| items.all_items_mut().get_mut(anchor.item_id as usize))
176                        .expect("Anchor refers to non-existent coverage item")
177                        .hits += hits.get();
178                }
179            }
180        }
181        if let Some(anchors) = self.execution_anchors.get(contract_id) {
182            for anchor in &anchors.anchors {
183                let hits = anchors.hits(hit_map, anchor.kind, is_deployed_code);
184                self.analyses
185                    .get_mut(&contract_id.build_id)
186                    .and_then(|items| items.all_items_mut().get_mut(anchor.item_id as usize))
187                    .expect("Anchor refers to non-existent coverage item")
188                    .hits += hits;
189            }
190        }
191
192        Ok(())
193    }
194
195    /// Returns the coverage items hit by a [`HitMap`] without mutating this report.
196    pub fn hit_items_for_hit_map<'a>(
197        &'a self,
198        contract_id: &ContractId,
199        hit_map: &HitMap,
200        is_deployed_code: bool,
201    ) -> Vec<(&'a CoverageItem, u32)> {
202        let Some(anchors) = self.anchors.get(contract_id) else { return Vec::new() };
203        let anchors = if is_deployed_code { &anchors.1 } else { &anchors.0 };
204
205        let mut hits_by_item = BTreeMap::<u32, u32>::new();
206        for anchor in anchors {
207            if let Some(hits) = hit_map.get(anchor.instruction) {
208                *hits_by_item.entry(anchor.item_id).or_default() += hits.get();
209            }
210        }
211        if let Some(anchors) = self.execution_anchors.get(contract_id) {
212            for anchor in &anchors.anchors {
213                let hits = anchors.hits(hit_map, anchor.kind, is_deployed_code);
214                if hits > 0 {
215                    *hits_by_item.entry(anchor.item_id).or_default() += hits;
216                }
217            }
218        }
219
220        let Some(items) = self.analyses.get(&contract_id.build_id) else {
221            return Vec::new();
222        };
223        hits_by_item
224            .into_iter()
225            .filter_map(|(item_id, hits)| {
226                let item = items.get(item_id)?;
227                Some((item, hits))
228            })
229            .collect()
230    }
231
232    /// Retains all the sources specified by `predicate`.
233    ///
234    /// This function should only be called after all the sources were used, otherwise, the output
235    /// will be missing the ones that are dependent on them.
236    pub fn retain_sources(&mut self, mut predicate: impl FnMut(&Path) -> bool) {
237        self.source_paths.retain(|_, paths| {
238            paths.retain(|_, path| predicate(path));
239            !paths.is_empty()
240        });
241
242        let source_paths = &self.source_paths;
243        self.source_paths_to_ids.retain(|build_id, paths| {
244            paths.retain(|_, source_id| {
245                source_paths.get(build_id).is_some_and(|paths| paths.contains_key(source_id))
246            });
247            !paths.is_empty()
248        });
249    }
250}
251
252/// A collection of [`HitMap`]s.
253#[derive(Clone, Debug, Default)]
254pub struct HitMaps(pub B256HashMap<HitMap>);
255
256impl HitMaps {
257    /// Merges two `Option<HitMaps>`.
258    pub fn merge_opt(a: &mut Option<Self>, b: Option<Self>) {
259        match (a, b) {
260            (_, None) => {}
261            (a @ None, Some(b)) => *a = Some(b),
262            (Some(a), Some(b)) => a.merge(b),
263        }
264    }
265
266    /// Merges two `HitMaps`.
267    pub fn merge(&mut self, other: Self) {
268        self.reserve(other.len());
269        for (code_hash, other) in other.0 {
270            self.entry(code_hash).and_modify(|e| e.merge(&other)).or_insert(other);
271        }
272    }
273
274    /// Merges two `HitMaps`.
275    pub fn merged(mut self, other: Self) -> Self {
276        self.merge(other);
277        self
278    }
279}
280
281impl Deref for HitMaps {
282    type Target = B256HashMap<HitMap>;
283
284    fn deref(&self) -> &Self::Target {
285        &self.0
286    }
287}
288
289impl DerefMut for HitMaps {
290    fn deref_mut(&mut self) -> &mut Self::Target {
291        &mut self.0
292    }
293}
294
295#[derive(Clone, Copy, Debug)]
296enum CallData {
297    Empty,
298    Short,
299    Selector([u8; 4]),
300}
301
302impl CallData {
303    fn new(input: &[u8]) -> Self {
304        if input.is_empty() {
305            Self::Empty
306        } else if let Some(selector) = input.get(..4) {
307            Self::Selector(selector.try_into().unwrap())
308        } else {
309            Self::Short
310        }
311    }
312}
313
314#[derive(Clone, Copy, Debug, Default)]
315struct CallHits {
316    without_value: u32,
317    with_value: u32,
318}
319
320impl CallHits {
321    const fn hit(&mut self, with_value: bool) {
322        if with_value {
323            self.with_value += 1;
324        } else {
325            self.without_value += 1;
326        }
327    }
328
329    const fn merge(&mut self, other: Self) {
330        self.without_value += other.without_value;
331        self.with_value += other.with_value;
332    }
333
334    const fn total(self, payable: bool) -> u32 {
335        self.without_value + if payable { self.with_value } else { 0 }
336    }
337}
338
339/// Hit data for an address.
340///
341/// Contains low-level data about hit counters for the instructions in the bytecode of a contract.
342#[derive(Clone, Debug)]
343pub struct HitMap {
344    hits: FxHashMap<u32, u32>,
345    bytecode: Bytes,
346    creations: u32,
347    empty_calls: CallHits,
348    short_calls: CallHits,
349    selector_calls: FxHashMap<[u8; 4], CallHits>,
350}
351
352impl HitMap {
353    /// Create a new hitmap with the given bytecode.
354    #[inline]
355    pub fn new(bytecode: Bytes) -> Self {
356        Self {
357            bytecode,
358            hits: HashMap::with_capacity_and_hasher(1024, Default::default()),
359            creations: 0,
360            empty_calls: Default::default(),
361            short_calls: Default::default(),
362            selector_calls: Default::default(),
363        }
364    }
365
366    /// Returns the bytecode.
367    #[inline]
368    pub const fn bytecode(&self) -> &Bytes {
369        &self.bytecode
370    }
371
372    /// Returns the number of hits for the given program counter.
373    #[inline]
374    pub fn get(&self, pc: u32) -> Option<NonZeroU32> {
375        NonZeroU32::new(self.hits.get(&pc).copied().unwrap_or(0))
376    }
377
378    /// Increase the hit counter by 1 for the given program counter.
379    #[inline]
380    pub fn hit(&mut self, pc: u32) {
381        self.hits(pc, 1)
382    }
383
384    /// Increase the hit counter by `hits` for the given program counter.
385    #[inline]
386    pub fn hits(&mut self, pc: u32, hits: u32) {
387        *self.hits.entry(pc).or_default() += hits;
388    }
389
390    fn call(&mut self, call: CallData, with_value: bool) {
391        let hits = match call {
392            CallData::Empty => &mut self.empty_calls,
393            CallData::Short => &mut self.short_calls,
394            CallData::Selector(selector) => self.selector_calls.entry(selector).or_default(),
395        };
396        hits.hit(with_value);
397    }
398
399    const fn creation(&mut self) {
400        self.creations += 1;
401    }
402
403    /// Reserve space for additional hits.
404    #[inline]
405    pub fn reserve(&mut self, additional: usize) {
406        self.hits.reserve(additional);
407    }
408
409    /// Merge another hitmap into this, assuming the bytecode is consistent
410    pub fn merge(&mut self, other: &Self) {
411        self.reserve(other.len());
412        for (pc, hits) in other.iter() {
413            self.hits(pc, hits);
414        }
415        self.creations += other.creations;
416        self.empty_calls.merge(other.empty_calls);
417        self.short_calls.merge(other.short_calls);
418        for (&selector, &hits) in &other.selector_calls {
419            self.selector_calls.entry(selector).or_default().merge(hits);
420        }
421    }
422
423    /// Returns an iterator over all the program counters and their hit counts.
424    #[inline]
425    pub fn iter(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
426        self.hits.iter().map(|(&pc, &hits)| (pc, hits))
427    }
428
429    /// Returns the number of program counters hit in the hitmap.
430    #[inline]
431    pub fn len(&self) -> usize {
432        self.hits.len()
433    }
434
435    /// Returns `true` if the hitmap is empty.
436    #[inline]
437    pub fn is_empty(&self) -> bool {
438        self.hits.is_empty()
439    }
440}
441
442/// A unique identifier for a contract.
443#[derive(Clone, Debug, PartialEq, Eq, Hash)]
444pub struct ContractId {
445    pub version: Version,
446    pub build_id: String,
447    pub source_id: usize,
448    pub contract_name: Arc<str>,
449}
450
451impl fmt::Display for ContractId {
452    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453        write!(
454            f,
455            "Contract \"{}\" (solc {}, source ID {})",
456            self.contract_name, self.version, self.source_id
457        )
458    }
459}
460
461/// An item anchor describes what instruction marks a [CoverageItem] as covered.
462#[derive(Clone, Debug)]
463pub struct ItemAnchor {
464    /// The program counter for the opcode of this anchor.
465    pub instruction: u32,
466    /// The item ID this anchor points to.
467    pub item_id: u32,
468}
469
470impl fmt::Display for ItemAnchor {
471    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472        write!(f, "IC {} -> Item {}", self.instruction, self.item_id)
473    }
474}
475
476/// An execution-based anchor for a coverage item without source-mapped bytecode.
477#[derive(Clone, Copy, Debug)]
478pub struct ExecutionAnchor {
479    /// The item ID this anchor points to.
480    pub item_id: u32,
481    /// The execution path that marks the item as covered.
482    pub kind: ExecutionAnchorKind,
483}
484
485/// The execution path associated with an execution-based anchor.
486#[derive(Clone, Copy, Debug, PartialEq, Eq)]
487pub enum ExecutionAnchorKind {
488    /// A successful contract creation.
489    Constructor,
490    /// An empty calldata call routed to `receive`.
491    Receive,
492    /// A call routed to `fallback`.
493    Fallback,
494}
495
496#[derive(Clone, Debug)]
497struct ContractExecutionAnchors {
498    anchors: Vec<ExecutionAnchor>,
499    function_selectors: FxHashSet<[u8; 4]>,
500    has_receive: bool,
501    fallback_payable: bool,
502}
503
504impl ContractExecutionAnchors {
505    fn hits(&self, hit_map: &HitMap, kind: ExecutionAnchorKind, is_deployed_code: bool) -> u32 {
506        match (kind, is_deployed_code) {
507            (ExecutionAnchorKind::Constructor, false) => hit_map.creations,
508            (ExecutionAnchorKind::Receive, true) => hit_map.empty_calls.total(true),
509            (ExecutionAnchorKind::Fallback, true) => {
510                let empty_calls = if self.has_receive {
511                    0
512                } else {
513                    hit_map.empty_calls.total(self.fallback_payable)
514                };
515                empty_calls
516                    + hit_map.short_calls.total(self.fallback_payable)
517                    + hit_map
518                        .selector_calls
519                        .iter()
520                        .filter(|(selector, _)| !self.function_selectors.contains(*selector))
521                        .map(|(_, hits)| hits.total(self.fallback_payable))
522                        .sum::<u32>()
523            }
524            _ => 0,
525        }
526    }
527}
528
529#[derive(Clone, Debug)]
530pub enum CoverageItemKind {
531    /// An executable line in the code.
532    Line,
533    /// A statement in the code.
534    Statement,
535    /// A branch in the code.
536    Branch {
537        /// The ID that identifies the branch.
538        ///
539        /// There may be multiple items with the same branch ID - they belong to the same branch,
540        /// but represent different paths.
541        branch_id: u32,
542        /// The path ID for this branch.
543        ///
544        /// The first path has ID 0, the next ID 1, and so on.
545        path_id: u32,
546        /// If true, then the branch anchor is the first opcode within the branch source range.
547        is_first_opcode: bool,
548    },
549    /// A function in the code.
550    Function {
551        /// The name of the function.
552        name: Box<str>,
553    },
554}
555
556impl PartialEq for CoverageItemKind {
557    fn eq(&self, other: &Self) -> bool {
558        self.ord_key() == other.ord_key()
559    }
560}
561
562impl Eq for CoverageItemKind {}
563
564impl PartialOrd for CoverageItemKind {
565    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
566        Some(self.cmp(other))
567    }
568}
569
570impl Ord for CoverageItemKind {
571    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
572        self.ord_key().cmp(&other.ord_key())
573    }
574}
575
576impl CoverageItemKind {
577    fn ord_key(&self) -> impl Ord + use<> {
578        match *self {
579            Self::Line => 0,
580            Self::Statement => 1,
581            Self::Branch { .. } => 2,
582            Self::Function { .. } => 3,
583        }
584    }
585}
586
587#[derive(Clone, Debug)]
588pub struct CoverageItem {
589    /// The coverage item kind.
590    pub kind: CoverageItemKind,
591    /// The location of the item in the source code.
592    pub loc: SourceLocation,
593    /// An alternative source location used only to find the item's bytecode anchor.
594    pub anchor_loc: Option<SourceLocation>,
595    /// The number of times this item was hit.
596    pub hits: u32,
597}
598
599#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
600enum CoverageItemKindKey<'a> {
601    Line,
602    Statement,
603    Branch { branch_id: u32, path_id: u32 },
604    Function(&'a str),
605}
606
607#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
608struct CoverageItemKey<'a> {
609    line_start: u32,
610    line_end: u32,
611    kind_order: u8,
612    byte_start: u32,
613    byte_end: u32,
614    contract_name: &'a str,
615    kind: CoverageItemKindKey<'a>,
616}
617
618impl<'a> CoverageItemKey<'a> {
619    fn new(item: &'a CoverageItem) -> Self {
620        let (kind_order, kind) = match &item.kind {
621            CoverageItemKind::Line => (0, CoverageItemKindKey::Line),
622            CoverageItemKind::Statement => (1, CoverageItemKindKey::Statement),
623            CoverageItemKind::Branch { branch_id, path_id, .. } => {
624                (2, CoverageItemKindKey::Branch { branch_id: *branch_id, path_id: *path_id })
625            }
626            CoverageItemKind::Function { name } => {
627                (3, CoverageItemKindKey::Function(name.as_ref()))
628            }
629        };
630
631        Self {
632            line_start: item.loc.lines.start,
633            line_end: item.loc.lines.end,
634            kind_order,
635            byte_start: item.loc.bytes.start,
636            byte_end: item.loc.bytes.end,
637            contract_name: item.loc.contract_name.as_ref(),
638            kind,
639        }
640    }
641}
642
643impl PartialEq for CoverageItem {
644    fn eq(&self, other: &Self) -> bool {
645        self.ord_key() == other.ord_key()
646    }
647}
648
649impl Eq for CoverageItem {}
650
651impl PartialOrd for CoverageItem {
652    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
653        Some(self.cmp(other))
654    }
655}
656
657impl Ord for CoverageItem {
658    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
659        self.ord_key().cmp(&other.ord_key())
660    }
661}
662
663impl fmt::Display for CoverageItem {
664    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
665        self.fmt_with_source(None).fmt(f)
666    }
667}
668
669impl CoverageItem {
670    fn ord_key(&self) -> impl Ord + use<> {
671        (
672            self.loc.source_id,
673            self.loc.lines.start,
674            self.loc.lines.end,
675            self.kind.ord_key(),
676            self.loc.bytes.start,
677            self.loc.bytes.end,
678        )
679    }
680
681    pub fn fmt_with_source(&self, src: Option<&str>) -> impl fmt::Display {
682        solar::data_structures::fmt::from_fn(move |f| {
683            match &self.kind {
684                CoverageItemKind::Line => {
685                    write!(f, "Line")?;
686                }
687                CoverageItemKind::Statement => {
688                    write!(f, "Statement")?;
689                }
690                CoverageItemKind::Branch { branch_id, path_id, .. } => {
691                    write!(f, "Branch (branch: {branch_id}, path: {path_id})")?;
692                }
693                CoverageItemKind::Function { name } => {
694                    write!(f, r#"Function "{name}""#)?;
695                }
696            }
697            write!(f, " (location: ({}), hits: {})", self.loc, self.hits)?;
698
699            if let Some(src) = src
700                && let Some(src) = src.get(self.loc.bytes())
701            {
702                write!(f, " -> ")?;
703
704                let max_len = 64;
705                let max_half = max_len / 2;
706
707                if src.len() > max_len {
708                    write!(f, "\"{}", src[..max_half].escape_debug())?;
709                    write!(f, "...")?;
710                    write!(f, "{}\"", src[src.len() - max_half..].escape_debug())?;
711                } else {
712                    write!(f, "{src:?}")?;
713                }
714            }
715
716            Ok(())
717        })
718    }
719}
720
721/// A source location.
722#[derive(Clone, Debug)]
723pub struct SourceLocation {
724    /// The source ID.
725    pub source_id: usize,
726    /// The contract this source range is in.
727    pub contract_name: Arc<str>,
728    /// Byte range.
729    pub bytes: Range<u32>,
730    /// Line range. Indices are 1-based.
731    pub lines: Range<u32>,
732}
733
734impl fmt::Display for SourceLocation {
735    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
736        write!(f, "source ID: {}, lines: {:?}, bytes: {:?}", self.source_id, self.lines, self.bytes)
737    }
738}
739
740impl SourceLocation {
741    /// Returns the byte range as usize.
742    pub const fn bytes(&self) -> Range<usize> {
743        self.bytes.start as usize..self.bytes.end as usize
744    }
745
746    /// Returns the length of the byte range.
747    pub fn len(&self) -> u32 {
748        self.bytes.len() as u32
749    }
750
751    /// Returns true if the byte range is empty.
752    pub fn is_empty(&self) -> bool {
753        self.len() == 0
754    }
755}
756
757/// Coverage summary for a source file.
758#[derive(Clone, Debug, Default)]
759pub struct CoverageSummary {
760    /// The number of executable lines in the source file.
761    pub line_count: usize,
762    /// The number of lines that were hit.
763    pub line_hits: usize,
764    /// The number of statements in the source file.
765    pub statement_count: usize,
766    /// The number of statements that were hit.
767    pub statement_hits: usize,
768    /// The number of branches in the source file.
769    pub branch_count: usize,
770    /// The number of branches that were hit.
771    pub branch_hits: usize,
772    /// The number of functions in the source file.
773    pub function_count: usize,
774    /// The number of functions hit.
775    pub function_hits: usize,
776}
777
778impl CoverageSummary {
779    /// Creates a new, empty coverage summary.
780    pub fn new() -> Self {
781        Self::default()
782    }
783
784    /// Creates a coverage summary from a collection of coverage items.
785    pub fn from_items<'a>(items: impl IntoIterator<Item = &'a CoverageItem>) -> Self {
786        let mut summary = Self::default();
787        summary.add_items(items);
788        summary
789    }
790
791    /// Adds another coverage summary to this one.
792    pub const fn merge(&mut self, other: &Self) {
793        let Self {
794            line_count,
795            line_hits,
796            statement_count,
797            statement_hits,
798            branch_count,
799            branch_hits,
800            function_count,
801            function_hits,
802        } = self;
803        *line_count += other.line_count;
804        *line_hits += other.line_hits;
805        *statement_count += other.statement_count;
806        *statement_hits += other.statement_hits;
807        *branch_count += other.branch_count;
808        *branch_hits += other.branch_hits;
809        *function_count += other.function_count;
810        *function_hits += other.function_hits;
811    }
812
813    /// Adds a coverage item to this summary.
814    pub const fn add_item(&mut self, item: &CoverageItem) {
815        match item.kind {
816            CoverageItemKind::Line => {
817                self.line_count += 1;
818                if item.hits > 0 {
819                    self.line_hits += 1;
820                }
821            }
822            CoverageItemKind::Statement => {
823                self.statement_count += 1;
824                if item.hits > 0 {
825                    self.statement_hits += 1;
826                }
827            }
828            CoverageItemKind::Branch { .. } => {
829                self.branch_count += 1;
830                if item.hits > 0 {
831                    self.branch_hits += 1;
832                }
833            }
834            CoverageItemKind::Function { .. } => {
835                self.function_count += 1;
836                if item.hits > 0 {
837                    self.function_hits += 1;
838                }
839            }
840        }
841    }
842
843    /// Adds multiple coverage items to this summary.
844    pub fn add_items<'a>(&mut self, items: impl IntoIterator<Item = &'a CoverageItem>) {
845        for item in items {
846            self.add_item(item);
847        }
848    }
849}