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