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 source_items in
137                items.all_items().chunk_by(|a, b| a.loc.source_id == b.loc.source_id)
138            {
139                if let Some(path) = self.get_source_path(build_id, source_items[0].loc.source_id) {
140                    let file_items = by_file.entry(path).or_default();
141                    for item in source_items {
142                        file_items
143                            .entry(CoverageItemKey::new(item))
144                            .and_modify(|merged| {
145                                merged.hits = merged.hits.saturating_add(item.hits)
146                            })
147                            .or_insert_with(|| item.clone());
148                    }
149                }
150            }
151        }
152        by_file.into_iter().map(|(path, items)| (path, items.into_values().collect()))
153    }
154
155    /// Processes data from a [`HitMap`] and sets hit counts for coverage items in this coverage
156    /// map.
157    ///
158    /// This function should only be called *after* all the relevant sources have been processed and
159    /// added to the map (see [`add_source`](Self::add_source)).
160    pub fn add_hit_map(
161        &mut self,
162        contract_id: &ContractId,
163        hit_map: &HitMap,
164        is_deployed_code: bool,
165    ) -> Result<()> {
166        // Add bytecode level hits.
167        self.bytecode_hits
168            .entry(contract_id.clone())
169            .and_modify(|m| m.merge(hit_map))
170            .or_insert_with(|| hit_map.clone());
171
172        // Add source level hits.
173        if let Some(anchors) = self.anchors.get(contract_id) {
174            let anchors = if is_deployed_code { &anchors.1 } else { &anchors.0 };
175            for (anchor, hits) in anchors.hits(hit_map) {
176                self.analyses
177                    .get_mut(&contract_id.build_id)
178                    .and_then(|items| items.all_items_mut().get_mut(anchor.item_id as usize))
179                    .expect("Anchor refers to non-existent coverage item")
180                    .hits += hits.get();
181            }
182        }
183        if let Some(anchors) = self.execution_anchors.get(contract_id) {
184            for anchor in &anchors.anchors {
185                let hits = anchors.hits(hit_map, anchor.kind, is_deployed_code);
186                self.analyses
187                    .get_mut(&contract_id.build_id)
188                    .and_then(|items| items.all_items_mut().get_mut(anchor.item_id as usize))
189                    .expect("Anchor refers to non-existent coverage item")
190                    .hits += hits;
191            }
192        }
193
194        Ok(())
195    }
196
197    /// Returns the coverage items hit by a [`HitMap`] without mutating this report.
198    pub fn hit_items_for_hit_map<'a>(
199        &'a self,
200        contract_id: &ContractId,
201        hit_map: &HitMap,
202        is_deployed_code: bool,
203    ) -> Vec<(&'a CoverageItem, u32)> {
204        let Some(anchors) = self.anchors.get(contract_id) else { return Vec::new() };
205        let anchors = if is_deployed_code { &anchors.1 } else { &anchors.0 };
206
207        let mut hits_by_item = BTreeMap::<u32, u32>::new();
208        for (anchor, hits) in anchors.hits(hit_map) {
209            *hits_by_item.entry(anchor.item_id).or_default() += hits.get();
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    /// Hits by program counter, not yet flushed into `hits`.
346    pending: Vec<u32>,
347    /// Program counters hit in `pending`.
348    pcs: Vec<u32>,
349    bytecode: Bytes,
350    creations: u32,
351    empty_calls: CallHits,
352    short_calls: CallHits,
353    selector_calls: FxHashMap<[u8; 4], CallHits>,
354}
355
356impl HitMap {
357    /// Create a new hitmap with the given bytecode.
358    #[inline]
359    pub fn new(bytecode: Bytes) -> Self {
360        Self {
361            bytecode,
362            hits: Default::default(),
363            pending: Vec::new(),
364            pcs: Vec::new(),
365            creations: 0,
366            empty_calls: Default::default(),
367            short_calls: Default::default(),
368            selector_calls: Default::default(),
369        }
370    }
371
372    /// Returns the bytecode.
373    #[inline]
374    pub const fn bytecode(&self) -> &Bytes {
375        &self.bytecode
376    }
377
378    /// Returns the number of hits for the given program counter.
379    #[inline]
380    pub fn get(&self, pc: u32) -> Option<NonZeroU32> {
381        NonZeroU32::new(self.hits.get(&pc).copied().unwrap_or(0))
382    }
383
384    /// Increase the hit counter by 1 for the given program counter.
385    #[inline]
386    pub fn hit(&mut self, pc: u32) {
387        self.hits(pc, 1)
388    }
389
390    /// Increase the hit counter by `hits` for the given program counter.
391    #[inline]
392    pub fn hits(&mut self, pc: u32, hits: u32) {
393        *self.hits.entry(pc).or_default() += hits;
394    }
395
396    /// Records a hit, visible after [`Self::flush`].
397    ///
398    /// Saturates, so a counter never returns to zero and adds its PC to `pcs` again.
399    #[inline]
400    pub(crate) fn record(&mut self, pc: usize) {
401        if let Some(hits) = self.pending.get_mut(pc) {
402            if *hits == 0 {
403                self.pcs.push(pc as u32);
404            }
405            *hits = hits.saturating_add(1);
406        } else {
407            self.grow_pending(pc);
408        }
409    }
410
411    /// Grows `pending` to include `pc`.
412    #[cold]
413    #[inline(never)]
414    fn grow_pending(&mut self, pc: usize) {
415        self.pending.resize(self.bytecode.len().max(pc + 1), 0);
416        self.record(pc);
417    }
418
419    /// Moves the recorded hits into `hits`.
420    pub(crate) fn flush(&mut self) {
421        let pending = std::mem::take(&mut self.pending);
422        let pcs = std::mem::take(&mut self.pcs);
423        self.reserve(pcs.len());
424        for pc in pcs {
425            self.hits(pc, pending[pc as usize]);
426        }
427    }
428
429    fn call(&mut self, call: CallData, with_value: bool) {
430        let hits = match call {
431            CallData::Empty => &mut self.empty_calls,
432            CallData::Short => &mut self.short_calls,
433            CallData::Selector(selector) => self.selector_calls.entry(selector).or_default(),
434        };
435        hits.hit(with_value);
436    }
437
438    const fn creation(&mut self) {
439        self.creations += 1;
440    }
441
442    /// Reserve space for additional hits.
443    #[inline]
444    pub fn reserve(&mut self, additional: usize) {
445        self.hits.reserve(additional);
446    }
447
448    /// Merge another hitmap into this, assuming the bytecode is consistent
449    pub fn merge(&mut self, other: &Self) {
450        self.reserve(other.len());
451        for (pc, hits) in other.iter() {
452            self.hits(pc, hits);
453        }
454        self.creations += other.creations;
455        self.empty_calls.merge(other.empty_calls);
456        self.short_calls.merge(other.short_calls);
457        for (&selector, &hits) in &other.selector_calls {
458            self.selector_calls.entry(selector).or_default().merge(hits);
459        }
460    }
461
462    /// Returns an iterator over all the program counters and their hit counts.
463    #[inline]
464    pub fn iter(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
465        self.hits.iter().map(|(&pc, &hits)| (pc, hits))
466    }
467
468    /// Returns the number of program counters hit in the hitmap.
469    #[inline]
470    pub fn len(&self) -> usize {
471        self.hits.len()
472    }
473
474    /// Returns `true` if the hitmap is empty.
475    #[inline]
476    pub fn is_empty(&self) -> bool {
477        self.hits.is_empty()
478    }
479}
480
481/// A unique identifier for a contract.
482#[derive(Clone, Debug, PartialEq, Eq, Hash)]
483pub struct ContractId {
484    pub version: Version,
485    pub build_id: String,
486    pub source_id: usize,
487    pub contract_name: Arc<str>,
488}
489
490impl fmt::Display for ContractId {
491    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
492        write!(
493            f,
494            "Contract \"{}\" (solc {}, source ID {})",
495            self.contract_name, self.version, self.source_id
496        )
497    }
498}
499
500/// An item anchor describes what instruction marks a [CoverageItem] as covered.
501#[derive(Clone, Debug)]
502pub struct ItemAnchor {
503    /// The program counter for the opcode of this anchor.
504    pub instruction: u32,
505    /// The item ID this anchor points to.
506    pub item_id: u32,
507}
508
509const _: () = assert!(std::mem::size_of::<ItemAnchor>() == 8);
510
511/// Item anchors for one contract's creation or runtime bytecode.
512#[derive(Clone, Debug, Default)]
513pub struct ItemAnchors {
514    /// Anchors into the source analysis.
515    pub anchors: Vec<ItemAnchor>,
516    /// Sparse map from anchor indices to the conditional jumps whose taken edges they represent.
517    /// Indices distinguish anchors even when they share an item or destination.
518    pub jumps: FxHashMap<usize, u32>,
519}
520
521impl ItemAnchors {
522    fn hits<'a>(
523        &'a self,
524        hit_map: &'a HitMap,
525    ) -> impl Iterator<Item = (&'a ItemAnchor, NonZeroU32)> {
526        self.anchors.iter().enumerate().filter_map(|(index, anchor)| {
527            let hits = if let Some(&jump) = self.jumps.get(&index) {
528                // A destination can also be reached through the other branch. Count executions
529                // of the jump minus its fall-through instruction instead of destination hits.
530                let hits = hit_map.get(jump)?.get();
531                NonZeroU32::new(
532                    hits.saturating_sub(hit_map.get(jump + 1).map_or(0, NonZeroU32::get)),
533                )?
534            } else {
535                hit_map.get(anchor.instruction)?
536            };
537            Some((anchor, hits))
538        })
539    }
540}
541
542impl fmt::Display for ItemAnchor {
543    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
544        write!(f, "IC {} -> Item {}", self.instruction, self.item_id)
545    }
546}
547
548/// An execution-based anchor for a coverage item without source-mapped bytecode.
549#[derive(Clone, Copy, Debug)]
550pub struct ExecutionAnchor {
551    /// The item ID this anchor points to.
552    pub item_id: u32,
553    /// The execution path that marks the item as covered.
554    pub kind: ExecutionAnchorKind,
555}
556
557/// The execution path associated with an execution-based anchor.
558#[derive(Clone, Copy, Debug, PartialEq, Eq)]
559pub enum ExecutionAnchorKind {
560    /// A successful contract creation.
561    Constructor,
562    /// An empty calldata call routed to `receive`.
563    Receive,
564    /// A call routed to `fallback`.
565    Fallback,
566}
567
568#[derive(Clone, Debug)]
569struct ContractExecutionAnchors {
570    anchors: Vec<ExecutionAnchor>,
571    function_selectors: FxHashSet<[u8; 4]>,
572    has_receive: bool,
573    fallback_payable: bool,
574}
575
576impl ContractExecutionAnchors {
577    fn hits(&self, hit_map: &HitMap, kind: ExecutionAnchorKind, is_deployed_code: bool) -> u32 {
578        match (kind, is_deployed_code) {
579            (ExecutionAnchorKind::Constructor, false) => hit_map.creations,
580            (ExecutionAnchorKind::Receive, true) => hit_map.empty_calls.total(true),
581            (ExecutionAnchorKind::Fallback, true) => {
582                let empty_calls = if self.has_receive {
583                    0
584                } else {
585                    hit_map.empty_calls.total(self.fallback_payable)
586                };
587                empty_calls
588                    + hit_map.short_calls.total(self.fallback_payable)
589                    + hit_map
590                        .selector_calls
591                        .iter()
592                        .filter(|(selector, _)| !self.function_selectors.contains(*selector))
593                        .map(|(_, hits)| hits.total(self.fallback_payable))
594                        .sum::<u32>()
595            }
596            _ => 0,
597        }
598    }
599}
600
601#[derive(Clone, Debug)]
602pub enum CoverageItemKind {
603    /// An executable line in the code.
604    Line,
605    /// A statement in the code.
606    Statement,
607    /// A branch in the code.
608    Branch {
609        /// The ID that identifies the branch.
610        ///
611        /// There may be multiple items with the same branch ID - they belong to the same branch,
612        /// but represent different paths.
613        branch_id: u32,
614        /// The path ID for this branch.
615        ///
616        /// The first path has ID 0, the next ID 1, and so on.
617        path_id: u32,
618        /// If true, then the branch anchor is the first opcode within the branch source range.
619        is_first_opcode: bool,
620    },
621    /// A function in the code.
622    Function {
623        /// The name of the function.
624        name: Box<str>,
625    },
626}
627
628impl PartialEq for CoverageItemKind {
629    fn eq(&self, other: &Self) -> bool {
630        self.ord_key() == other.ord_key()
631    }
632}
633
634impl Eq for CoverageItemKind {}
635
636impl PartialOrd for CoverageItemKind {
637    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
638        Some(self.cmp(other))
639    }
640}
641
642impl Ord for CoverageItemKind {
643    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
644        self.ord_key().cmp(&other.ord_key())
645    }
646}
647
648impl CoverageItemKind {
649    fn ord_key(&self) -> impl Ord + use<> {
650        match *self {
651            Self::Line => 0,
652            Self::Statement => 1,
653            Self::Branch { .. } => 2,
654            Self::Function { .. } => 3,
655        }
656    }
657}
658
659#[derive(Clone, Debug)]
660pub struct CoverageItem {
661    /// The coverage item kind.
662    pub kind: CoverageItemKind,
663    /// The location of the item in the source code.
664    pub loc: SourceLocation,
665    /// An alternative source location used only to find the item's bytecode anchor.
666    pub anchor_loc: Option<SourceLocation>,
667    /// The number of times this item was hit.
668    pub hits: u32,
669}
670
671#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
672enum CoverageItemKindKey<'a> {
673    Line,
674    Statement,
675    Branch { branch_id: u32, path_id: u32 },
676    Function(&'a str),
677}
678
679#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
680struct CoverageItemKey<'a> {
681    line_start: u32,
682    line_end: u32,
683    kind_order: u8,
684    byte_start: u32,
685    byte_end: u32,
686    contract_name: &'a str,
687    kind: CoverageItemKindKey<'a>,
688}
689
690impl<'a> CoverageItemKey<'a> {
691    fn new(item: &'a CoverageItem) -> Self {
692        let (kind_order, kind) = match &item.kind {
693            CoverageItemKind::Line => (0, CoverageItemKindKey::Line),
694            CoverageItemKind::Statement => (1, CoverageItemKindKey::Statement),
695            CoverageItemKind::Branch { branch_id, path_id, .. } => {
696                (2, CoverageItemKindKey::Branch { branch_id: *branch_id, path_id: *path_id })
697            }
698            CoverageItemKind::Function { name } => {
699                (3, CoverageItemKindKey::Function(name.as_ref()))
700            }
701        };
702
703        Self {
704            line_start: item.loc.lines.start,
705            line_end: item.loc.lines.end,
706            kind_order,
707            byte_start: item.loc.bytes.start,
708            byte_end: item.loc.bytes.end,
709            contract_name: item.loc.contract_name.as_ref(),
710            kind,
711        }
712    }
713}
714
715impl PartialEq for CoverageItem {
716    fn eq(&self, other: &Self) -> bool {
717        self.ord_key() == other.ord_key()
718    }
719}
720
721impl Eq for CoverageItem {}
722
723impl PartialOrd for CoverageItem {
724    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
725        Some(self.cmp(other))
726    }
727}
728
729impl Ord for CoverageItem {
730    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
731        self.ord_key().cmp(&other.ord_key())
732    }
733}
734
735impl fmt::Display for CoverageItem {
736    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
737        self.fmt_with_source(None).fmt(f)
738    }
739}
740
741impl CoverageItem {
742    fn ord_key(&self) -> impl Ord + use<> {
743        (
744            self.loc.source_id,
745            self.loc.lines.start,
746            self.loc.lines.end,
747            self.kind.ord_key(),
748            self.loc.bytes.start,
749            self.loc.bytes.end,
750        )
751    }
752
753    pub fn fmt_with_source(&self, src: Option<&str>) -> impl fmt::Display {
754        solar::data_structures::fmt::from_fn(move |f| {
755            match &self.kind {
756                CoverageItemKind::Line => {
757                    write!(f, "Line")?;
758                }
759                CoverageItemKind::Statement => {
760                    write!(f, "Statement")?;
761                }
762                CoverageItemKind::Branch { branch_id, path_id, .. } => {
763                    write!(f, "Branch (branch: {branch_id}, path: {path_id})")?;
764                }
765                CoverageItemKind::Function { name } => {
766                    write!(f, r#"Function "{name}""#)?;
767                }
768            }
769            write!(f, " (location: ({}), hits: {})", self.loc, self.hits)?;
770
771            if let Some(src) = src
772                && let Some(src) = src.get(self.loc.bytes())
773            {
774                write!(f, " -> ")?;
775
776                let max_len = 64;
777                let max_half = max_len / 2;
778
779                if src.len() > max_len {
780                    write!(f, "\"{}", src[..max_half].escape_debug())?;
781                    write!(f, "...")?;
782                    write!(f, "{}\"", src[src.len() - max_half..].escape_debug())?;
783                } else {
784                    write!(f, "{src:?}")?;
785                }
786            }
787
788            Ok(())
789        })
790    }
791}
792
793/// A source location.
794#[derive(Clone, Debug)]
795pub struct SourceLocation {
796    /// The source ID.
797    pub source_id: usize,
798    /// The contract this source range is in.
799    pub contract_name: Arc<str>,
800    /// Byte range.
801    pub bytes: Range<u32>,
802    /// Line range. Indices are 1-based.
803    pub lines: Range<u32>,
804}
805
806impl fmt::Display for SourceLocation {
807    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
808        write!(f, "source ID: {}, lines: {:?}, bytes: {:?}", self.source_id, self.lines, self.bytes)
809    }
810}
811
812impl SourceLocation {
813    /// Returns the byte range as usize.
814    pub const fn bytes(&self) -> Range<usize> {
815        self.bytes.start as usize..self.bytes.end as usize
816    }
817
818    /// Returns the length of the byte range.
819    pub fn len(&self) -> u32 {
820        self.bytes.len() as u32
821    }
822
823    /// Returns true if the byte range is empty.
824    pub fn is_empty(&self) -> bool {
825        self.len() == 0
826    }
827}
828
829/// Coverage summary for a source file.
830#[derive(Clone, Debug, Default)]
831pub struct CoverageSummary {
832    /// The number of executable lines in the source file.
833    pub line_count: usize,
834    /// The number of lines that were hit.
835    pub line_hits: usize,
836    /// The number of statements in the source file.
837    pub statement_count: usize,
838    /// The number of statements that were hit.
839    pub statement_hits: usize,
840    /// The number of branches in the source file.
841    pub branch_count: usize,
842    /// The number of branches that were hit.
843    pub branch_hits: usize,
844    /// The number of functions in the source file.
845    pub function_count: usize,
846    /// The number of functions hit.
847    pub function_hits: usize,
848}
849
850impl CoverageSummary {
851    /// Creates a new, empty coverage summary.
852    pub fn new() -> Self {
853        Self::default()
854    }
855
856    /// Creates a coverage summary from a collection of coverage items.
857    pub fn from_items<'a>(items: impl IntoIterator<Item = &'a CoverageItem>) -> Self {
858        let mut summary = Self::new();
859        summary.add_items(items);
860        summary
861    }
862
863    /// Adds another coverage summary to this one.
864    pub const fn merge(&mut self, other: &Self) {
865        let Self {
866            line_count,
867            line_hits,
868            statement_count,
869            statement_hits,
870            branch_count,
871            branch_hits,
872            function_count,
873            function_hits,
874        } = self;
875        *line_count += other.line_count;
876        *line_hits += other.line_hits;
877        *statement_count += other.statement_count;
878        *statement_hits += other.statement_hits;
879        *branch_count += other.branch_count;
880        *branch_hits += other.branch_hits;
881        *function_count += other.function_count;
882        *function_hits += other.function_hits;
883    }
884
885    /// Adds a coverage item to this summary.
886    pub const fn add_item(&mut self, item: &CoverageItem) {
887        match item.kind {
888            CoverageItemKind::Line => {
889                self.line_count += 1;
890                if item.hits > 0 {
891                    self.line_hits += 1;
892                }
893            }
894            CoverageItemKind::Statement => {
895                self.statement_count += 1;
896                if item.hits > 0 {
897                    self.statement_hits += 1;
898                }
899            }
900            CoverageItemKind::Branch { .. } => {
901                self.branch_count += 1;
902                if item.hits > 0 {
903                    self.branch_hits += 1;
904                }
905            }
906            CoverageItemKind::Function { .. } => {
907                self.function_count += 1;
908                if item.hits > 0 {
909                    self.function_hits += 1;
910                }
911            }
912        }
913    }
914
915    /// Adds multiple coverage items to this summary.
916    pub fn add_items<'a>(&mut self, items: impl IntoIterator<Item = &'a CoverageItem>) {
917        for item in items {
918            self.add_item(item);
919        }
920    }
921}
922
923#[cfg(test)]
924mod tests {
925    use super::*;
926
927    #[test]
928    fn sparse_jump_hits_distinguish_shared_items_and_destinations() {
929        let anchors = ItemAnchors {
930            anchors: vec![
931                ItemAnchor { instruction: 20, item_id: 0 },
932                ItemAnchor { instruction: 20, item_id: 0 },
933                ItemAnchor { instruction: 20, item_id: 0 },
934            ],
935            jumps: [(1, 4), (2, 10)].into_iter().collect(),
936        };
937        for (jump_hits, fallthrough_hits, taken_hits) in
938            [(3, 2, 1), (3, 0, 3), (3, 3, 0), (0, 0, 0)]
939        {
940            let mut hit_map = HitMap::new(Bytes::new());
941            hit_map.hits(20, 5);
942            hit_map.hits(4, jump_hits);
943            hit_map.hits(5, fallthrough_hits);
944            hit_map.hits(10, 2);
945            hit_map.hits(11, 2);
946
947            let hits = anchors.hits(&hit_map).map(|(_, hits)| hits.get()).collect::<Vec<_>>();
948            let expected = if taken_hits == 0 { vec![5] } else { vec![5, taken_hits] };
949            assert_eq!(hits, expected);
950        }
951    }
952
953    #[test]
954    fn ordinary_anchors_do_not_allocate_jump_metadata() {
955        let anchors = ItemAnchors {
956            anchors: vec![ItemAnchor { instruction: 20, item_id: 0 }],
957            ..Default::default()
958        };
959        assert_eq!(anchors.jumps.capacity(), 0);
960        let mut hit_map = HitMap::new(Bytes::new());
961        assert_eq!(anchors.hits(&hit_map).count(), 0);
962        hit_map.hits(20, 3);
963        assert_eq!(anchors.hits(&hit_map).next().unwrap().1.get(), 3);
964    }
965
966    #[test]
967    fn saturated_pending_hits_flush_once() {
968        let mut hit_map = HitMap::new(Bytes::from_static(&[0]));
969        hit_map.record(0);
970        hit_map.pending[0] = u32::MAX;
971        hit_map.record(0);
972        hit_map.record(0);
973        hit_map.flush();
974        assert_eq!(hit_map.iter().collect::<Vec<_>>(), [(0, u32::MAX)]);
975    }
976}