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, (ItemAnchors, ItemAnchors)>,
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, (ItemAnchors, ItemAnchors))>,
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, hits) in anchors.hits(hit_map) {
172                self.analyses
173                    .get_mut(&contract_id.build_id)
174                    .and_then(|items| items.all_items_mut().get_mut(anchor.item_id as usize))
175                    .expect("Anchor refers to non-existent coverage item")
176                    .hits += hits.get();
177            }
178        }
179        if let Some(anchors) = self.execution_anchors.get(contract_id) {
180            for anchor in &anchors.anchors {
181                let hits = anchors.hits(hit_map, anchor.kind, is_deployed_code);
182                self.analyses
183                    .get_mut(&contract_id.build_id)
184                    .and_then(|items| items.all_items_mut().get_mut(anchor.item_id as usize))
185                    .expect("Anchor refers to non-existent coverage item")
186                    .hits += hits;
187            }
188        }
189
190        Ok(())
191    }
192
193    /// Returns the coverage items hit by a [`HitMap`] without mutating this report.
194    pub fn hit_items_for_hit_map<'a>(
195        &'a self,
196        contract_id: &ContractId,
197        hit_map: &HitMap,
198        is_deployed_code: bool,
199    ) -> Vec<(&'a CoverageItem, u32)> {
200        let Some(anchors) = self.anchors.get(contract_id) else { return Vec::new() };
201        let anchors = if is_deployed_code { &anchors.1 } else { &anchors.0 };
202
203        let mut hits_by_item = BTreeMap::<u32, u32>::new();
204        for (anchor, hits) in anchors.hits(hit_map) {
205            *hits_by_item.entry(anchor.item_id).or_default() += hits.get();
206        }
207        if let Some(anchors) = self.execution_anchors.get(contract_id) {
208            for anchor in &anchors.anchors {
209                let hits = anchors.hits(hit_map, anchor.kind, is_deployed_code);
210                if hits > 0 {
211                    *hits_by_item.entry(anchor.item_id).or_default() += hits;
212                }
213            }
214        }
215
216        let Some(items) = self.analyses.get(&contract_id.build_id) else {
217            return Vec::new();
218        };
219        hits_by_item
220            .into_iter()
221            .filter_map(|(item_id, hits)| {
222                let item = items.get(item_id)?;
223                Some((item, hits))
224            })
225            .collect()
226    }
227
228    /// Retains all the sources specified by `predicate`.
229    ///
230    /// This function should only be called after all the sources were used, otherwise, the output
231    /// will be missing the ones that are dependent on them.
232    pub fn retain_sources(&mut self, mut predicate: impl FnMut(&Path) -> bool) {
233        self.source_paths.retain(|_, paths| {
234            paths.retain(|_, path| predicate(path));
235            !paths.is_empty()
236        });
237
238        let source_paths = &self.source_paths;
239        self.source_paths_to_ids.retain(|build_id, paths| {
240            paths.retain(|_, source_id| {
241                source_paths.get(build_id).is_some_and(|paths| paths.contains_key(source_id))
242            });
243            !paths.is_empty()
244        });
245    }
246}
247
248/// A collection of [`HitMap`]s.
249#[derive(Clone, Debug, Default)]
250pub struct HitMaps(pub B256HashMap<HitMap>);
251
252impl HitMaps {
253    /// Merges two `Option<HitMaps>`.
254    pub fn merge_opt(a: &mut Option<Self>, b: Option<Self>) {
255        match (a, b) {
256            (_, None) => {}
257            (a @ None, Some(b)) => *a = Some(b),
258            (Some(a), Some(b)) => a.merge(b),
259        }
260    }
261
262    /// Merges two `HitMaps`.
263    pub fn merge(&mut self, other: Self) {
264        self.reserve(other.len());
265        for (code_hash, other) in other.0 {
266            self.entry(code_hash).and_modify(|e| e.merge(&other)).or_insert(other);
267        }
268    }
269
270    /// Merges two `HitMaps`.
271    pub fn merged(mut self, other: Self) -> Self {
272        self.merge(other);
273        self
274    }
275}
276
277impl Deref for HitMaps {
278    type Target = B256HashMap<HitMap>;
279
280    fn deref(&self) -> &Self::Target {
281        &self.0
282    }
283}
284
285impl DerefMut for HitMaps {
286    fn deref_mut(&mut self) -> &mut Self::Target {
287        &mut self.0
288    }
289}
290
291#[derive(Clone, Copy, Debug)]
292enum CallData {
293    Empty,
294    Short,
295    Selector([u8; 4]),
296}
297
298impl CallData {
299    fn new(input: &[u8]) -> Self {
300        if input.is_empty() {
301            Self::Empty
302        } else if let Some(selector) = input.get(..4) {
303            Self::Selector(selector.try_into().unwrap())
304        } else {
305            Self::Short
306        }
307    }
308}
309
310#[derive(Clone, Copy, Debug, Default)]
311struct CallHits {
312    without_value: u32,
313    with_value: u32,
314}
315
316impl CallHits {
317    const fn hit(&mut self, with_value: bool) {
318        if with_value {
319            self.with_value += 1;
320        } else {
321            self.without_value += 1;
322        }
323    }
324
325    const fn merge(&mut self, other: Self) {
326        self.without_value += other.without_value;
327        self.with_value += other.with_value;
328    }
329
330    const fn total(self, payable: bool) -> u32 {
331        self.without_value + if payable { self.with_value } else { 0 }
332    }
333}
334
335/// Hit data for an address.
336///
337/// Contains low-level data about hit counters for the instructions in the bytecode of a contract.
338#[derive(Clone, Debug)]
339pub struct HitMap {
340    hits: FxHashMap<u32, u32>,
341    bytecode: Bytes,
342    creations: u32,
343    empty_calls: CallHits,
344    short_calls: CallHits,
345    selector_calls: FxHashMap<[u8; 4], CallHits>,
346}
347
348impl HitMap {
349    /// Create a new hitmap with the given bytecode.
350    #[inline]
351    pub fn new(bytecode: Bytes) -> Self {
352        Self {
353            bytecode,
354            hits: HashMap::with_capacity_and_hasher(1024, Default::default()),
355            creations: 0,
356            empty_calls: Default::default(),
357            short_calls: Default::default(),
358            selector_calls: Default::default(),
359        }
360    }
361
362    /// Returns the bytecode.
363    #[inline]
364    pub const fn bytecode(&self) -> &Bytes {
365        &self.bytecode
366    }
367
368    /// Returns the number of hits for the given program counter.
369    #[inline]
370    pub fn get(&self, pc: u32) -> Option<NonZeroU32> {
371        NonZeroU32::new(self.hits.get(&pc).copied().unwrap_or(0))
372    }
373
374    /// Increase the hit counter by 1 for the given program counter.
375    #[inline]
376    pub fn hit(&mut self, pc: u32) {
377        self.hits(pc, 1)
378    }
379
380    /// Increase the hit counter by `hits` for the given program counter.
381    #[inline]
382    pub fn hits(&mut self, pc: u32, hits: u32) {
383        *self.hits.entry(pc).or_default() += hits;
384    }
385
386    fn call(&mut self, call: CallData, with_value: bool) {
387        let hits = match call {
388            CallData::Empty => &mut self.empty_calls,
389            CallData::Short => &mut self.short_calls,
390            CallData::Selector(selector) => self.selector_calls.entry(selector).or_default(),
391        };
392        hits.hit(with_value);
393    }
394
395    const fn creation(&mut self) {
396        self.creations += 1;
397    }
398
399    /// Reserve space for additional hits.
400    #[inline]
401    pub fn reserve(&mut self, additional: usize) {
402        self.hits.reserve(additional);
403    }
404
405    /// Merge another hitmap into this, assuming the bytecode is consistent
406    pub fn merge(&mut self, other: &Self) {
407        self.reserve(other.len());
408        for (pc, hits) in other.iter() {
409            self.hits(pc, hits);
410        }
411        self.creations += other.creations;
412        self.empty_calls.merge(other.empty_calls);
413        self.short_calls.merge(other.short_calls);
414        for (&selector, &hits) in &other.selector_calls {
415            self.selector_calls.entry(selector).or_default().merge(hits);
416        }
417    }
418
419    /// Returns an iterator over all the program counters and their hit counts.
420    #[inline]
421    pub fn iter(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
422        self.hits.iter().map(|(&pc, &hits)| (pc, hits))
423    }
424
425    /// Returns the number of program counters hit in the hitmap.
426    #[inline]
427    pub fn len(&self) -> usize {
428        self.hits.len()
429    }
430
431    /// Returns `true` if the hitmap is empty.
432    #[inline]
433    pub fn is_empty(&self) -> bool {
434        self.hits.is_empty()
435    }
436}
437
438/// A unique identifier for a contract.
439#[derive(Clone, Debug, PartialEq, Eq, Hash)]
440pub struct ContractId {
441    pub version: Version,
442    pub build_id: String,
443    pub source_id: usize,
444    pub contract_name: Arc<str>,
445}
446
447impl fmt::Display for ContractId {
448    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449        write!(
450            f,
451            "Contract \"{}\" (solc {}, source ID {})",
452            self.contract_name, self.version, self.source_id
453        )
454    }
455}
456
457/// An item anchor describes what instruction marks a [CoverageItem] as covered.
458#[derive(Clone, Debug)]
459pub struct ItemAnchor {
460    /// The program counter for the opcode of this anchor.
461    pub instruction: u32,
462    /// The item ID this anchor points to.
463    pub item_id: u32,
464}
465
466const _: () = assert!(std::mem::size_of::<ItemAnchor>() == 8);
467
468/// Item anchors for one contract's creation or runtime bytecode.
469#[derive(Clone, Debug, Default)]
470pub struct ItemAnchors {
471    /// Anchors into the source analysis.
472    pub anchors: Vec<ItemAnchor>,
473    /// Sparse map from anchor indices to the conditional jumps whose taken edges they represent.
474    /// Indices distinguish anchors even when they share an item or destination.
475    pub jumps: FxHashMap<usize, u32>,
476}
477
478impl ItemAnchors {
479    fn hits<'a>(
480        &'a self,
481        hit_map: &'a HitMap,
482    ) -> impl Iterator<Item = (&'a ItemAnchor, NonZeroU32)> {
483        self.anchors.iter().enumerate().filter_map(|(index, anchor)| {
484            let hits = if let Some(&jump) = self.jumps.get(&index) {
485                // A destination can also be reached through the other branch. Count executions
486                // of the jump minus its fall-through instruction instead of destination hits.
487                let hits = hit_map.get(jump)?.get();
488                NonZeroU32::new(
489                    hits.saturating_sub(hit_map.get(jump + 1).map_or(0, NonZeroU32::get)),
490                )?
491            } else {
492                hit_map.get(anchor.instruction)?
493            };
494            Some((anchor, hits))
495        })
496    }
497}
498
499impl fmt::Display for ItemAnchor {
500    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501        write!(f, "IC {} -> Item {}", self.instruction, self.item_id)
502    }
503}
504
505/// An execution-based anchor for a coverage item without source-mapped bytecode.
506#[derive(Clone, Copy, Debug)]
507pub struct ExecutionAnchor {
508    /// The item ID this anchor points to.
509    pub item_id: u32,
510    /// The execution path that marks the item as covered.
511    pub kind: ExecutionAnchorKind,
512}
513
514/// The execution path associated with an execution-based anchor.
515#[derive(Clone, Copy, Debug, PartialEq, Eq)]
516pub enum ExecutionAnchorKind {
517    /// A successful contract creation.
518    Constructor,
519    /// An empty calldata call routed to `receive`.
520    Receive,
521    /// A call routed to `fallback`.
522    Fallback,
523}
524
525#[derive(Clone, Debug)]
526struct ContractExecutionAnchors {
527    anchors: Vec<ExecutionAnchor>,
528    function_selectors: FxHashSet<[u8; 4]>,
529    has_receive: bool,
530    fallback_payable: bool,
531}
532
533impl ContractExecutionAnchors {
534    fn hits(&self, hit_map: &HitMap, kind: ExecutionAnchorKind, is_deployed_code: bool) -> u32 {
535        match (kind, is_deployed_code) {
536            (ExecutionAnchorKind::Constructor, false) => hit_map.creations,
537            (ExecutionAnchorKind::Receive, true) => hit_map.empty_calls.total(true),
538            (ExecutionAnchorKind::Fallback, true) => {
539                let empty_calls = if self.has_receive {
540                    0
541                } else {
542                    hit_map.empty_calls.total(self.fallback_payable)
543                };
544                empty_calls
545                    + hit_map.short_calls.total(self.fallback_payable)
546                    + hit_map
547                        .selector_calls
548                        .iter()
549                        .filter(|(selector, _)| !self.function_selectors.contains(*selector))
550                        .map(|(_, hits)| hits.total(self.fallback_payable))
551                        .sum::<u32>()
552            }
553            _ => 0,
554        }
555    }
556}
557
558#[derive(Clone, Debug)]
559pub enum CoverageItemKind {
560    /// An executable line in the code.
561    Line,
562    /// A statement in the code.
563    Statement,
564    /// A branch in the code.
565    Branch {
566        /// The ID that identifies the branch.
567        ///
568        /// There may be multiple items with the same branch ID - they belong to the same branch,
569        /// but represent different paths.
570        branch_id: u32,
571        /// The path ID for this branch.
572        ///
573        /// The first path has ID 0, the next ID 1, and so on.
574        path_id: u32,
575        /// If true, then the branch anchor is the first opcode within the branch source range.
576        is_first_opcode: bool,
577    },
578    /// A function in the code.
579    Function {
580        /// The name of the function.
581        name: Box<str>,
582    },
583}
584
585impl PartialEq for CoverageItemKind {
586    fn eq(&self, other: &Self) -> bool {
587        self.ord_key() == other.ord_key()
588    }
589}
590
591impl Eq for CoverageItemKind {}
592
593impl PartialOrd for CoverageItemKind {
594    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
595        Some(self.cmp(other))
596    }
597}
598
599impl Ord for CoverageItemKind {
600    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
601        self.ord_key().cmp(&other.ord_key())
602    }
603}
604
605impl CoverageItemKind {
606    fn ord_key(&self) -> impl Ord + use<> {
607        match *self {
608            Self::Line => 0,
609            Self::Statement => 1,
610            Self::Branch { .. } => 2,
611            Self::Function { .. } => 3,
612        }
613    }
614}
615
616#[derive(Clone, Debug)]
617pub struct CoverageItem {
618    /// The coverage item kind.
619    pub kind: CoverageItemKind,
620    /// The location of the item in the source code.
621    pub loc: SourceLocation,
622    /// An alternative source location used only to find the item's bytecode anchor.
623    pub anchor_loc: Option<SourceLocation>,
624    /// The number of times this item was hit.
625    pub hits: u32,
626}
627
628#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
629enum CoverageItemKindKey<'a> {
630    Line,
631    Statement,
632    Branch { branch_id: u32, path_id: u32 },
633    Function(&'a str),
634}
635
636#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
637struct CoverageItemKey<'a> {
638    line_start: u32,
639    line_end: u32,
640    kind_order: u8,
641    byte_start: u32,
642    byte_end: u32,
643    contract_name: &'a str,
644    kind: CoverageItemKindKey<'a>,
645}
646
647impl<'a> CoverageItemKey<'a> {
648    fn new(item: &'a CoverageItem) -> Self {
649        let (kind_order, kind) = match &item.kind {
650            CoverageItemKind::Line => (0, CoverageItemKindKey::Line),
651            CoverageItemKind::Statement => (1, CoverageItemKindKey::Statement),
652            CoverageItemKind::Branch { branch_id, path_id, .. } => {
653                (2, CoverageItemKindKey::Branch { branch_id: *branch_id, path_id: *path_id })
654            }
655            CoverageItemKind::Function { name } => {
656                (3, CoverageItemKindKey::Function(name.as_ref()))
657            }
658        };
659
660        Self {
661            line_start: item.loc.lines.start,
662            line_end: item.loc.lines.end,
663            kind_order,
664            byte_start: item.loc.bytes.start,
665            byte_end: item.loc.bytes.end,
666            contract_name: item.loc.contract_name.as_ref(),
667            kind,
668        }
669    }
670}
671
672impl PartialEq for CoverageItem {
673    fn eq(&self, other: &Self) -> bool {
674        self.ord_key() == other.ord_key()
675    }
676}
677
678impl Eq for CoverageItem {}
679
680impl PartialOrd for CoverageItem {
681    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
682        Some(self.cmp(other))
683    }
684}
685
686impl Ord for CoverageItem {
687    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
688        self.ord_key().cmp(&other.ord_key())
689    }
690}
691
692impl fmt::Display for CoverageItem {
693    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
694        self.fmt_with_source(None).fmt(f)
695    }
696}
697
698impl CoverageItem {
699    fn ord_key(&self) -> impl Ord + use<> {
700        (
701            self.loc.source_id,
702            self.loc.lines.start,
703            self.loc.lines.end,
704            self.kind.ord_key(),
705            self.loc.bytes.start,
706            self.loc.bytes.end,
707        )
708    }
709
710    pub fn fmt_with_source(&self, src: Option<&str>) -> impl fmt::Display {
711        solar::data_structures::fmt::from_fn(move |f| {
712            match &self.kind {
713                CoverageItemKind::Line => {
714                    write!(f, "Line")?;
715                }
716                CoverageItemKind::Statement => {
717                    write!(f, "Statement")?;
718                }
719                CoverageItemKind::Branch { branch_id, path_id, .. } => {
720                    write!(f, "Branch (branch: {branch_id}, path: {path_id})")?;
721                }
722                CoverageItemKind::Function { name } => {
723                    write!(f, r#"Function "{name}""#)?;
724                }
725            }
726            write!(f, " (location: ({}), hits: {})", self.loc, self.hits)?;
727
728            if let Some(src) = src
729                && let Some(src) = src.get(self.loc.bytes())
730            {
731                write!(f, " -> ")?;
732
733                let max_len = 64;
734                let max_half = max_len / 2;
735
736                if src.len() > max_len {
737                    write!(f, "\"{}", src[..max_half].escape_debug())?;
738                    write!(f, "...")?;
739                    write!(f, "{}\"", src[src.len() - max_half..].escape_debug())?;
740                } else {
741                    write!(f, "{src:?}")?;
742                }
743            }
744
745            Ok(())
746        })
747    }
748}
749
750/// A source location.
751#[derive(Clone, Debug)]
752pub struct SourceLocation {
753    /// The source ID.
754    pub source_id: usize,
755    /// The contract this source range is in.
756    pub contract_name: Arc<str>,
757    /// Byte range.
758    pub bytes: Range<u32>,
759    /// Line range. Indices are 1-based.
760    pub lines: Range<u32>,
761}
762
763impl fmt::Display for SourceLocation {
764    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
765        write!(f, "source ID: {}, lines: {:?}, bytes: {:?}", self.source_id, self.lines, self.bytes)
766    }
767}
768
769impl SourceLocation {
770    /// Returns the byte range as usize.
771    pub const fn bytes(&self) -> Range<usize> {
772        self.bytes.start as usize..self.bytes.end as usize
773    }
774
775    /// Returns the length of the byte range.
776    pub fn len(&self) -> u32 {
777        self.bytes.len() as u32
778    }
779
780    /// Returns true if the byte range is empty.
781    pub fn is_empty(&self) -> bool {
782        self.len() == 0
783    }
784}
785
786/// Coverage summary for a source file.
787#[derive(Clone, Debug, Default)]
788pub struct CoverageSummary {
789    /// The number of executable lines in the source file.
790    pub line_count: usize,
791    /// The number of lines that were hit.
792    pub line_hits: usize,
793    /// The number of statements in the source file.
794    pub statement_count: usize,
795    /// The number of statements that were hit.
796    pub statement_hits: usize,
797    /// The number of branches in the source file.
798    pub branch_count: usize,
799    /// The number of branches that were hit.
800    pub branch_hits: usize,
801    /// The number of functions in the source file.
802    pub function_count: usize,
803    /// The number of functions hit.
804    pub function_hits: usize,
805}
806
807impl CoverageSummary {
808    /// Creates a new, empty coverage summary.
809    pub fn new() -> Self {
810        Self::default()
811    }
812
813    /// Creates a coverage summary from a collection of coverage items.
814    pub fn from_items<'a>(items: impl IntoIterator<Item = &'a CoverageItem>) -> Self {
815        let mut summary = Self::default();
816        summary.add_items(items);
817        summary
818    }
819
820    /// Adds another coverage summary to this one.
821    pub const fn merge(&mut self, other: &Self) {
822        let Self {
823            line_count,
824            line_hits,
825            statement_count,
826            statement_hits,
827            branch_count,
828            branch_hits,
829            function_count,
830            function_hits,
831        } = self;
832        *line_count += other.line_count;
833        *line_hits += other.line_hits;
834        *statement_count += other.statement_count;
835        *statement_hits += other.statement_hits;
836        *branch_count += other.branch_count;
837        *branch_hits += other.branch_hits;
838        *function_count += other.function_count;
839        *function_hits += other.function_hits;
840    }
841
842    /// Adds a coverage item to this summary.
843    pub const fn add_item(&mut self, item: &CoverageItem) {
844        match item.kind {
845            CoverageItemKind::Line => {
846                self.line_count += 1;
847                if item.hits > 0 {
848                    self.line_hits += 1;
849                }
850            }
851            CoverageItemKind::Statement => {
852                self.statement_count += 1;
853                if item.hits > 0 {
854                    self.statement_hits += 1;
855                }
856            }
857            CoverageItemKind::Branch { .. } => {
858                self.branch_count += 1;
859                if item.hits > 0 {
860                    self.branch_hits += 1;
861                }
862            }
863            CoverageItemKind::Function { .. } => {
864                self.function_count += 1;
865                if item.hits > 0 {
866                    self.function_hits += 1;
867                }
868            }
869        }
870    }
871
872    /// Adds multiple coverage items to this summary.
873    pub fn add_items<'a>(&mut self, items: impl IntoIterator<Item = &'a CoverageItem>) {
874        for item in items {
875            self.add_item(item);
876        }
877    }
878}
879
880#[cfg(test)]
881mod tests {
882    use super::*;
883
884    #[test]
885    fn sparse_jump_hits_distinguish_shared_items_and_destinations() {
886        let anchors = ItemAnchors {
887            anchors: vec![
888                ItemAnchor { instruction: 20, item_id: 0 },
889                ItemAnchor { instruction: 20, item_id: 0 },
890                ItemAnchor { instruction: 20, item_id: 0 },
891            ],
892            jumps: [(1, 4), (2, 10)].into_iter().collect(),
893        };
894        for (jump_hits, fallthrough_hits, taken_hits) in
895            [(3, 2, 1), (3, 0, 3), (3, 3, 0), (0, 0, 0)]
896        {
897            let mut hit_map = HitMap::new(Bytes::new());
898            hit_map.hits(20, 5);
899            hit_map.hits(4, jump_hits);
900            hit_map.hits(5, fallthrough_hits);
901            hit_map.hits(10, 2);
902            hit_map.hits(11, 2);
903
904            let hits = anchors.hits(&hit_map).map(|(_, hits)| hits.get()).collect::<Vec<_>>();
905            let expected = if taken_hits == 0 { vec![5] } else { vec![5, taken_hits] };
906            assert_eq!(hits, expected);
907        }
908    }
909
910    #[test]
911    fn ordinary_anchors_do_not_allocate_jump_metadata() {
912        let anchors = ItemAnchors {
913            anchors: vec![ItemAnchor { instruction: 20, item_id: 0 }],
914            ..Default::default()
915        };
916        assert_eq!(anchors.jumps.capacity(), 0);
917        let mut hit_map = HitMap::new(Bytes::new());
918        assert_eq!(anchors.hits(&hit_map).count(), 0);
919        hit_map.hits(20, 3);
920        assert_eq!(anchors.hits(&hit_map).next().unwrap().1.get(), 3);
921    }
922}