1#![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#[derive(Clone, Debug, Default)]
39pub struct CoverageReport {
40 pub source_paths: HashMap<String, HashMap<usize, PathBuf>>,
42 pub source_paths_to_ids: HashMap<String, HashMap<PathBuf, usize>>,
44 pub analyses: HashMap<String, SourceAnalysis>,
46 pub anchors: HashMap<ContractId, (Vec<ItemAnchor>, Vec<ItemAnchor>)>,
50 pub bytecode_hits: HashMap<ContractId, HitMap>,
52 pub source_maps: HashMap<ContractId, (SourceMap, SourceMap)>,
54}
55
56impl CoverageReport {
57 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 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 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 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 pub fn add_analysis(&mut self, build_id: String, analysis: SourceAnalysis) {
83 self.analyses.insert(build_id, analysis);
84 }
85
86 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 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 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 pub fn add_hit_map(
129 &mut self,
130 contract_id: &ContractId,
131 hit_map: &HitMap,
132 is_deployed_code: bool,
133 ) -> Result<()> {
134 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 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 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 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#[derive(Clone, Debug, Default)]
208pub struct HitMaps(pub B256HashMap<HitMap>);
209
210impl HitMaps {
211 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 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 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#[derive(Clone, Debug)]
253pub struct HitMap {
254 hits: FxHashMap<u32, u32>,
255 bytecode: Bytes,
256}
257
258impl HitMap {
259 #[inline]
261 pub fn new(bytecode: Bytes) -> Self {
262 Self { bytecode, hits: HashMap::with_capacity_and_hasher(1024, Default::default()) }
263 }
264
265 #[inline]
267 pub const fn bytecode(&self) -> &Bytes {
268 &self.bytecode
269 }
270
271 #[inline]
273 pub fn get(&self, pc: u32) -> Option<NonZeroU32> {
274 NonZeroU32::new(self.hits.get(&pc).copied().unwrap_or(0))
275 }
276
277 #[inline]
279 pub fn hit(&mut self, pc: u32) {
280 self.hits(pc, 1)
281 }
282
283 #[inline]
285 pub fn hits(&mut self, pc: u32, hits: u32) {
286 *self.hits.entry(pc).or_default() += hits;
287 }
288
289 #[inline]
291 pub fn reserve(&mut self, additional: usize) {
292 self.hits.reserve(additional);
293 }
294
295 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 #[inline]
305 pub fn iter(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
306 self.hits.iter().map(|(&pc, &hits)| (pc, hits))
307 }
308
309 #[inline]
311 pub fn len(&self) -> usize {
312 self.hits.len()
313 }
314
315 #[inline]
317 pub fn is_empty(&self) -> bool {
318 self.hits.is_empty()
319 }
320}
321
322#[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#[derive(Clone, Debug)]
343pub struct ItemAnchor {
344 pub instruction: u32,
346 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 Line,
360 Statement,
362 Branch {
364 branch_id: u32,
369 path_id: u32,
373 is_first_opcode: bool,
375 },
376 Function {
378 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 pub kind: CoverageItemKind,
418 pub loc: SourceLocation,
420 pub anchor_loc: Option<SourceLocation>,
422 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#[derive(Clone, Debug)]
550pub struct SourceLocation {
551 pub source_id: usize,
553 pub contract_name: Arc<str>,
555 pub bytes: Range<u32>,
557 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 pub const fn bytes(&self) -> Range<usize> {
570 self.bytes.start as usize..self.bytes.end as usize
571 }
572
573 pub fn len(&self) -> u32 {
575 self.bytes.len() as u32
576 }
577
578 pub fn is_empty(&self) -> bool {
580 self.len() == 0
581 }
582}
583
584#[derive(Clone, Debug, Default)]
586pub struct CoverageSummary {
587 pub line_count: usize,
589 pub line_hits: usize,
591 pub statement_count: usize,
593 pub statement_hits: usize,
595 pub branch_count: usize,
597 pub branch_hits: usize,
599 pub function_count: usize,
601 pub function_hits: usize,
603}
604
605impl CoverageSummary {
606 pub fn new() -> Self {
608 Self::default()
609 }
610
611 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 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 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 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}