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