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    /// Retains all the coverage items specified by `predicate`.
151    ///
152    /// This function should only be called after all the sources were used, otherwise, the output
153    /// will be missing the ones that are dependent on them.
154    pub fn retain_sources(&mut self, mut predicate: impl FnMut(&Path) -> bool) {
155        self.analyses.retain(|version, analysis| {
156            analysis.all_items_mut().retain(|item| {
157                self.source_paths
158                    .get(&(version.clone(), item.loc.source_id))
159                    .map(|path| predicate(path))
160                    .unwrap_or(false)
161            });
162            !analysis.all_items().is_empty()
163        });
164    }
165}
166
167/// A collection of [`HitMap`]s.
168#[derive(Clone, Debug, Default)]
169pub struct HitMaps(pub B256HashMap<HitMap>);
170
171impl HitMaps {
172    /// Merges two `Option<HitMaps>`.
173    pub fn merge_opt(a: &mut Option<Self>, b: Option<Self>) {
174        match (a, b) {
175            (_, None) => {}
176            (a @ None, Some(b)) => *a = Some(b),
177            (Some(a), Some(b)) => a.merge(b),
178        }
179    }
180
181    /// Merges two `HitMaps`.
182    pub fn merge(&mut self, other: Self) {
183        self.reserve(other.len());
184        for (code_hash, other) in other.0 {
185            self.entry(code_hash).and_modify(|e| e.merge(&other)).or_insert(other);
186        }
187    }
188
189    /// Merges two `HitMaps`.
190    pub fn merged(mut self, other: Self) -> Self {
191        self.merge(other);
192        self
193    }
194}
195
196impl Deref for HitMaps {
197    type Target = B256HashMap<HitMap>;
198
199    fn deref(&self) -> &Self::Target {
200        &self.0
201    }
202}
203
204impl DerefMut for HitMaps {
205    fn deref_mut(&mut self) -> &mut Self::Target {
206        &mut self.0
207    }
208}
209
210/// Hit data for an address.
211///
212/// Contains low-level data about hit counters for the instructions in the bytecode of a contract.
213#[derive(Clone, Debug)]
214pub struct HitMap {
215    hits: FxHashMap<u32, u32>,
216    bytecode: Bytes,
217}
218
219impl HitMap {
220    /// Create a new hitmap with the given bytecode.
221    #[inline]
222    pub fn new(bytecode: Bytes) -> Self {
223        Self { bytecode, hits: HashMap::with_capacity_and_hasher(1024, Default::default()) }
224    }
225
226    /// Returns the bytecode.
227    #[inline]
228    pub fn bytecode(&self) -> &Bytes {
229        &self.bytecode
230    }
231
232    /// Returns the number of hits for the given program counter.
233    #[inline]
234    pub fn get(&self, pc: u32) -> Option<NonZeroU32> {
235        NonZeroU32::new(self.hits.get(&pc).copied().unwrap_or(0))
236    }
237
238    /// Increase the hit counter by 1 for the given program counter.
239    #[inline]
240    pub fn hit(&mut self, pc: u32) {
241        self.hits(pc, 1)
242    }
243
244    /// Increase the hit counter by `hits` for the given program counter.
245    #[inline]
246    pub fn hits(&mut self, pc: u32, hits: u32) {
247        *self.hits.entry(pc).or_default() += hits;
248    }
249
250    /// Reserve space for additional hits.
251    #[inline]
252    pub fn reserve(&mut self, additional: usize) {
253        self.hits.reserve(additional);
254    }
255
256    /// Merge another hitmap into this, assuming the bytecode is consistent
257    pub fn merge(&mut self, other: &Self) {
258        self.reserve(other.len());
259        for (pc, hits) in other.iter() {
260            self.hits(pc, hits);
261        }
262    }
263
264    /// Returns an iterator over all the program counters and their hit counts.
265    #[inline]
266    pub fn iter(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
267        self.hits.iter().map(|(&pc, &hits)| (pc, hits))
268    }
269
270    /// Returns the number of program counters hit in the hitmap.
271    #[inline]
272    pub fn len(&self) -> usize {
273        self.hits.len()
274    }
275
276    /// Returns `true` if the hitmap is empty.
277    #[inline]
278    pub fn is_empty(&self) -> bool {
279        self.hits.is_empty()
280    }
281}
282
283/// A unique identifier for a contract
284#[derive(Clone, Debug, PartialEq, Eq, Hash)]
285pub struct ContractId {
286    pub version: Version,
287    pub source_id: usize,
288    pub contract_name: Arc<str>,
289}
290
291impl fmt::Display for ContractId {
292    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293        write!(
294            f,
295            "Contract \"{}\" (solc {}, source ID {})",
296            self.contract_name, self.version, self.source_id
297        )
298    }
299}
300
301/// An item anchor describes what instruction marks a [CoverageItem] as covered.
302#[derive(Clone, Debug)]
303pub struct ItemAnchor {
304    /// The program counter for the opcode of this anchor.
305    pub instruction: u32,
306    /// The item ID this anchor points to.
307    pub item_id: u32,
308}
309
310impl fmt::Display for ItemAnchor {
311    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312        write!(f, "IC {} -> Item {}", self.instruction, self.item_id)
313    }
314}
315
316#[derive(Clone, Debug)]
317pub enum CoverageItemKind {
318    /// An executable line in the code.
319    Line,
320    /// A statement in the code.
321    Statement,
322    /// A branch in the code.
323    Branch {
324        /// The ID that identifies the branch.
325        ///
326        /// There may be multiple items with the same branch ID - they belong to the same branch,
327        /// but represent different paths.
328        branch_id: u32,
329        /// The path ID for this branch.
330        ///
331        /// The first path has ID 0, the next ID 1, and so on.
332        path_id: u32,
333        /// If true, then the branch anchor is the first opcode within the branch source range.
334        is_first_opcode: bool,
335    },
336    /// A function in the code.
337    Function {
338        /// The name of the function.
339        name: Box<str>,
340    },
341}
342
343impl PartialEq for CoverageItemKind {
344    fn eq(&self, other: &Self) -> bool {
345        self.ord_key() == other.ord_key()
346    }
347}
348
349impl Eq for CoverageItemKind {}
350
351impl PartialOrd for CoverageItemKind {
352    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
353        Some(self.cmp(other))
354    }
355}
356
357impl Ord for CoverageItemKind {
358    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
359        self.ord_key().cmp(&other.ord_key())
360    }
361}
362
363impl CoverageItemKind {
364    fn ord_key(&self) -> impl Ord + use<> {
365        match *self {
366            Self::Line => 0,
367            Self::Statement => 1,
368            Self::Branch { .. } => 2,
369            Self::Function { .. } => 3,
370        }
371    }
372}
373
374#[derive(Clone, Debug)]
375pub struct CoverageItem {
376    /// The coverage item kind.
377    pub kind: CoverageItemKind,
378    /// The location of the item in the source code.
379    pub loc: SourceLocation,
380    /// The number of times this item was hit.
381    pub hits: u32,
382}
383
384impl PartialEq for CoverageItem {
385    fn eq(&self, other: &Self) -> bool {
386        self.ord_key() == other.ord_key()
387    }
388}
389
390impl Eq for CoverageItem {}
391
392impl PartialOrd for CoverageItem {
393    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
394        Some(self.cmp(other))
395    }
396}
397
398impl Ord for CoverageItem {
399    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
400        self.ord_key().cmp(&other.ord_key())
401    }
402}
403
404impl fmt::Display for CoverageItem {
405    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406        self.fmt_with_source(None).fmt(f)
407    }
408}
409
410impl CoverageItem {
411    fn ord_key(&self) -> impl Ord + use<> {
412        (
413            self.loc.source_id,
414            self.loc.lines.start,
415            self.loc.lines.end,
416            self.kind.ord_key(),
417            self.loc.bytes.start,
418            self.loc.bytes.end,
419        )
420    }
421
422    pub fn fmt_with_source(&self, src: Option<&str>) -> impl fmt::Display {
423        solar::data_structures::fmt::from_fn(move |f| {
424            match &self.kind {
425                CoverageItemKind::Line => {
426                    write!(f, "Line")?;
427                }
428                CoverageItemKind::Statement => {
429                    write!(f, "Statement")?;
430                }
431                CoverageItemKind::Branch { branch_id, path_id, .. } => {
432                    write!(f, "Branch (branch: {branch_id}, path: {path_id})")?;
433                }
434                CoverageItemKind::Function { name } => {
435                    write!(f, r#"Function "{name}""#)?;
436                }
437            }
438            write!(f, " (location: ({}), hits: {})", self.loc, self.hits)?;
439
440            if let Some(src) = src
441                && let Some(src) = src.get(self.loc.bytes())
442            {
443                write!(f, " -> ")?;
444
445                let max_len = 64;
446                let max_half = max_len / 2;
447
448                if src.len() > max_len {
449                    write!(f, "\"{}", src[..max_half].escape_debug())?;
450                    write!(f, "...")?;
451                    write!(f, "{}\"", src[src.len() - max_half..].escape_debug())?;
452                } else {
453                    write!(f, "{src:?}")?;
454                }
455            }
456
457            Ok(())
458        })
459    }
460}
461
462/// A source location.
463#[derive(Clone, Debug)]
464pub struct SourceLocation {
465    /// The source ID.
466    pub source_id: usize,
467    /// The contract this source range is in.
468    pub contract_name: Arc<str>,
469    /// Byte range.
470    pub bytes: Range<u32>,
471    /// Line range. Indices are 1-based.
472    pub lines: Range<u32>,
473}
474
475impl fmt::Display for SourceLocation {
476    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
477        write!(f, "source ID: {}, lines: {:?}, bytes: {:?}", self.source_id, self.lines, self.bytes)
478    }
479}
480
481impl SourceLocation {
482    /// Returns the byte range as usize.
483    pub fn bytes(&self) -> Range<usize> {
484        self.bytes.start as usize..self.bytes.end as usize
485    }
486
487    /// Returns the length of the byte range.
488    pub fn len(&self) -> u32 {
489        self.bytes.len() as u32
490    }
491
492    /// Returns true if the byte range is empty.
493    pub fn is_empty(&self) -> bool {
494        self.len() == 0
495    }
496}
497
498/// Coverage summary for a source file.
499#[derive(Clone, Debug, Default)]
500pub struct CoverageSummary {
501    /// The number of executable lines in the source file.
502    pub line_count: usize,
503    /// The number of lines that were hit.
504    pub line_hits: usize,
505    /// The number of statements in the source file.
506    pub statement_count: usize,
507    /// The number of statements that were hit.
508    pub statement_hits: usize,
509    /// The number of branches in the source file.
510    pub branch_count: usize,
511    /// The number of branches that were hit.
512    pub branch_hits: usize,
513    /// The number of functions in the source file.
514    pub function_count: usize,
515    /// The number of functions hit.
516    pub function_hits: usize,
517}
518
519impl CoverageSummary {
520    /// Creates a new, empty coverage summary.
521    pub fn new() -> Self {
522        Self::default()
523    }
524
525    /// Creates a coverage summary from a collection of coverage items.
526    pub fn from_items<'a>(items: impl IntoIterator<Item = &'a CoverageItem>) -> Self {
527        let mut summary = Self::default();
528        summary.add_items(items);
529        summary
530    }
531
532    /// Adds another coverage summary to this one.
533    pub fn merge(&mut self, other: &Self) {
534        let Self {
535            line_count,
536            line_hits,
537            statement_count,
538            statement_hits,
539            branch_count,
540            branch_hits,
541            function_count,
542            function_hits,
543        } = self;
544        *line_count += other.line_count;
545        *line_hits += other.line_hits;
546        *statement_count += other.statement_count;
547        *statement_hits += other.statement_hits;
548        *branch_count += other.branch_count;
549        *branch_hits += other.branch_hits;
550        *function_count += other.function_count;
551        *function_hits += other.function_hits;
552    }
553
554    /// Adds a coverage item to this summary.
555    pub fn add_item(&mut self, item: &CoverageItem) {
556        match item.kind {
557            CoverageItemKind::Line => {
558                self.line_count += 1;
559                if item.hits > 0 {
560                    self.line_hits += 1;
561                }
562            }
563            CoverageItemKind::Statement => {
564                self.statement_count += 1;
565                if item.hits > 0 {
566                    self.statement_hits += 1;
567                }
568            }
569            CoverageItemKind::Branch { .. } => {
570                self.branch_count += 1;
571                if item.hits > 0 {
572                    self.branch_hits += 1;
573                }
574            }
575            CoverageItemKind::Function { .. } => {
576                self.function_count += 1;
577                if item.hits > 0 {
578                    self.function_hits += 1;
579                }
580            }
581        }
582    }
583
584    /// Adds multiple coverage items to this summary.
585    pub fn add_items<'a>(&mut self, items: impl IntoIterator<Item = &'a CoverageItem>) {
586        for item in items {
587            self.add_item(item);
588        }
589    }
590}