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::{
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#[derive(Clone, Debug, Default)]
42pub struct CoverageReport {
43 pub source_paths: HashMap<String, HashMap<usize, PathBuf>>,
45 pub source_paths_to_ids: HashMap<String, HashMap<PathBuf, usize>>,
47 pub analyses: HashMap<String, SourceAnalysis>,
49 pub anchors: HashMap<ContractId, (Vec<ItemAnchor>, Vec<ItemAnchor>)>,
53 execution_anchors: HashMap<ContractId, ContractExecutionAnchors>,
55 pub bytecode_hits: HashMap<ContractId, HitMap>,
57 pub source_maps: HashMap<ContractId, (SourceMap, SourceMap)>,
59}
60
61impl CoverageReport {
62 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 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 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 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 pub fn add_analysis(&mut self, build_id: String, analysis: SourceAnalysis) {
88 self.analyses.insert(build_id, analysis);
89 }
90
91 pub fn add_anchors(
95 &mut self,
96 anchors: impl IntoIterator<Item = (ContractId, (Vec<ItemAnchor>, Vec<ItemAnchor>))>,
97 ) {
98 self.anchors.extend(anchors);
99 }
100
101 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 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 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 item in items.all_items() {
137 let Some(path) = self.get_source_path(build_id, item.loc.source_id) else {
138 continue;
139 };
140 by_file
141 .entry(path)
142 .or_default()
143 .entry(CoverageItemKey::new(item))
144 .and_modify(|merged| merged.hits = merged.hits.saturating_add(item.hits))
145 .or_insert_with(|| item.clone());
146 }
147 }
148 by_file.into_iter().map(|(path, items)| (path, items.into_values().collect()))
149 }
150
151 pub fn add_hit_map(
157 &mut self,
158 contract_id: &ContractId,
159 hit_map: &HitMap,
160 is_deployed_code: bool,
161 ) -> Result<()> {
162 self.bytecode_hits
164 .entry(contract_id.clone())
165 .and_modify(|m| m.merge(hit_map))
166 .or_insert_with(|| hit_map.clone());
167
168 if let Some(anchors) = self.anchors.get(contract_id) {
170 let anchors = if is_deployed_code { &anchors.1 } else { &anchors.0 };
171 for anchor in anchors {
172 if let Some(hits) = hit_map.get(anchor.instruction) {
173 self.analyses
174 .get_mut(&contract_id.build_id)
175 .and_then(|items| items.all_items_mut().get_mut(anchor.item_id as usize))
176 .expect("Anchor refers to non-existent coverage item")
177 .hits += hits.get();
178 }
179 }
180 }
181 if let Some(anchors) = self.execution_anchors.get(contract_id) {
182 for anchor in &anchors.anchors {
183 let hits = anchors.hits(hit_map, anchor.kind, is_deployed_code);
184 self.analyses
185 .get_mut(&contract_id.build_id)
186 .and_then(|items| items.all_items_mut().get_mut(anchor.item_id as usize))
187 .expect("Anchor refers to non-existent coverage item")
188 .hits += hits;
189 }
190 }
191
192 Ok(())
193 }
194
195 pub fn hit_items_for_hit_map<'a>(
197 &'a self,
198 contract_id: &ContractId,
199 hit_map: &HitMap,
200 is_deployed_code: bool,
201 ) -> Vec<(&'a CoverageItem, u32)> {
202 let Some(anchors) = self.anchors.get(contract_id) else { return Vec::new() };
203 let anchors = if is_deployed_code { &anchors.1 } else { &anchors.0 };
204
205 let mut hits_by_item = BTreeMap::<u32, u32>::new();
206 for anchor in anchors {
207 if let Some(hits) = hit_map.get(anchor.instruction) {
208 *hits_by_item.entry(anchor.item_id).or_default() += hits.get();
209 }
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 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#[derive(Clone, Debug, Default)]
254pub struct HitMaps(pub B256HashMap<HitMap>);
255
256impl HitMaps {
257 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 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 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#[derive(Clone, Debug)]
343pub struct HitMap {
344 hits: FxHashMap<u32, u32>,
345 bytecode: Bytes,
346 creations: u32,
347 empty_calls: CallHits,
348 short_calls: CallHits,
349 selector_calls: FxHashMap<[u8; 4], CallHits>,
350}
351
352impl HitMap {
353 #[inline]
355 pub fn new(bytecode: Bytes) -> Self {
356 Self {
357 bytecode,
358 hits: HashMap::with_capacity_and_hasher(1024, Default::default()),
359 creations: 0,
360 empty_calls: Default::default(),
361 short_calls: Default::default(),
362 selector_calls: Default::default(),
363 }
364 }
365
366 #[inline]
368 pub const fn bytecode(&self) -> &Bytes {
369 &self.bytecode
370 }
371
372 #[inline]
374 pub fn get(&self, pc: u32) -> Option<NonZeroU32> {
375 NonZeroU32::new(self.hits.get(&pc).copied().unwrap_or(0))
376 }
377
378 #[inline]
380 pub fn hit(&mut self, pc: u32) {
381 self.hits(pc, 1)
382 }
383
384 #[inline]
386 pub fn hits(&mut self, pc: u32, hits: u32) {
387 *self.hits.entry(pc).or_default() += hits;
388 }
389
390 fn call(&mut self, call: CallData, with_value: bool) {
391 let hits = match call {
392 CallData::Empty => &mut self.empty_calls,
393 CallData::Short => &mut self.short_calls,
394 CallData::Selector(selector) => self.selector_calls.entry(selector).or_default(),
395 };
396 hits.hit(with_value);
397 }
398
399 const fn creation(&mut self) {
400 self.creations += 1;
401 }
402
403 #[inline]
405 pub fn reserve(&mut self, additional: usize) {
406 self.hits.reserve(additional);
407 }
408
409 pub fn merge(&mut self, other: &Self) {
411 self.reserve(other.len());
412 for (pc, hits) in other.iter() {
413 self.hits(pc, hits);
414 }
415 self.creations += other.creations;
416 self.empty_calls.merge(other.empty_calls);
417 self.short_calls.merge(other.short_calls);
418 for (&selector, &hits) in &other.selector_calls {
419 self.selector_calls.entry(selector).or_default().merge(hits);
420 }
421 }
422
423 #[inline]
425 pub fn iter(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
426 self.hits.iter().map(|(&pc, &hits)| (pc, hits))
427 }
428
429 #[inline]
431 pub fn len(&self) -> usize {
432 self.hits.len()
433 }
434
435 #[inline]
437 pub fn is_empty(&self) -> bool {
438 self.hits.is_empty()
439 }
440}
441
442#[derive(Clone, Debug, PartialEq, Eq, Hash)]
444pub struct ContractId {
445 pub version: Version,
446 pub build_id: String,
447 pub source_id: usize,
448 pub contract_name: Arc<str>,
449}
450
451impl fmt::Display for ContractId {
452 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453 write!(
454 f,
455 "Contract \"{}\" (solc {}, source ID {})",
456 self.contract_name, self.version, self.source_id
457 )
458 }
459}
460
461#[derive(Clone, Debug)]
463pub struct ItemAnchor {
464 pub instruction: u32,
466 pub item_id: u32,
468}
469
470impl fmt::Display for ItemAnchor {
471 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472 write!(f, "IC {} -> Item {}", self.instruction, self.item_id)
473 }
474}
475
476#[derive(Clone, Copy, Debug)]
478pub struct ExecutionAnchor {
479 pub item_id: u32,
481 pub kind: ExecutionAnchorKind,
483}
484
485#[derive(Clone, Copy, Debug, PartialEq, Eq)]
487pub enum ExecutionAnchorKind {
488 Constructor,
490 Receive,
492 Fallback,
494}
495
496#[derive(Clone, Debug)]
497struct ContractExecutionAnchors {
498 anchors: Vec<ExecutionAnchor>,
499 function_selectors: FxHashSet<[u8; 4]>,
500 has_receive: bool,
501 fallback_payable: bool,
502}
503
504impl ContractExecutionAnchors {
505 fn hits(&self, hit_map: &HitMap, kind: ExecutionAnchorKind, is_deployed_code: bool) -> u32 {
506 match (kind, is_deployed_code) {
507 (ExecutionAnchorKind::Constructor, false) => hit_map.creations,
508 (ExecutionAnchorKind::Receive, true) => hit_map.empty_calls.total(true),
509 (ExecutionAnchorKind::Fallback, true) => {
510 let empty_calls = if self.has_receive {
511 0
512 } else {
513 hit_map.empty_calls.total(self.fallback_payable)
514 };
515 empty_calls
516 + hit_map.short_calls.total(self.fallback_payable)
517 + hit_map
518 .selector_calls
519 .iter()
520 .filter(|(selector, _)| !self.function_selectors.contains(*selector))
521 .map(|(_, hits)| hits.total(self.fallback_payable))
522 .sum::<u32>()
523 }
524 _ => 0,
525 }
526 }
527}
528
529#[derive(Clone, Debug)]
530pub enum CoverageItemKind {
531 Line,
533 Statement,
535 Branch {
537 branch_id: u32,
542 path_id: u32,
546 is_first_opcode: bool,
548 },
549 Function {
551 name: Box<str>,
553 },
554}
555
556impl PartialEq for CoverageItemKind {
557 fn eq(&self, other: &Self) -> bool {
558 self.ord_key() == other.ord_key()
559 }
560}
561
562impl Eq for CoverageItemKind {}
563
564impl PartialOrd for CoverageItemKind {
565 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
566 Some(self.cmp(other))
567 }
568}
569
570impl Ord for CoverageItemKind {
571 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
572 self.ord_key().cmp(&other.ord_key())
573 }
574}
575
576impl CoverageItemKind {
577 fn ord_key(&self) -> impl Ord + use<> {
578 match *self {
579 Self::Line => 0,
580 Self::Statement => 1,
581 Self::Branch { .. } => 2,
582 Self::Function { .. } => 3,
583 }
584 }
585}
586
587#[derive(Clone, Debug)]
588pub struct CoverageItem {
589 pub kind: CoverageItemKind,
591 pub loc: SourceLocation,
593 pub anchor_loc: Option<SourceLocation>,
595 pub hits: u32,
597}
598
599#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
600enum CoverageItemKindKey<'a> {
601 Line,
602 Statement,
603 Branch { branch_id: u32, path_id: u32 },
604 Function(&'a str),
605}
606
607#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
608struct CoverageItemKey<'a> {
609 line_start: u32,
610 line_end: u32,
611 kind_order: u8,
612 byte_start: u32,
613 byte_end: u32,
614 contract_name: &'a str,
615 kind: CoverageItemKindKey<'a>,
616}
617
618impl<'a> CoverageItemKey<'a> {
619 fn new(item: &'a CoverageItem) -> Self {
620 let (kind_order, kind) = match &item.kind {
621 CoverageItemKind::Line => (0, CoverageItemKindKey::Line),
622 CoverageItemKind::Statement => (1, CoverageItemKindKey::Statement),
623 CoverageItemKind::Branch { branch_id, path_id, .. } => {
624 (2, CoverageItemKindKey::Branch { branch_id: *branch_id, path_id: *path_id })
625 }
626 CoverageItemKind::Function { name } => {
627 (3, CoverageItemKindKey::Function(name.as_ref()))
628 }
629 };
630
631 Self {
632 line_start: item.loc.lines.start,
633 line_end: item.loc.lines.end,
634 kind_order,
635 byte_start: item.loc.bytes.start,
636 byte_end: item.loc.bytes.end,
637 contract_name: item.loc.contract_name.as_ref(),
638 kind,
639 }
640 }
641}
642
643impl PartialEq for CoverageItem {
644 fn eq(&self, other: &Self) -> bool {
645 self.ord_key() == other.ord_key()
646 }
647}
648
649impl Eq for CoverageItem {}
650
651impl PartialOrd for CoverageItem {
652 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
653 Some(self.cmp(other))
654 }
655}
656
657impl Ord for CoverageItem {
658 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
659 self.ord_key().cmp(&other.ord_key())
660 }
661}
662
663impl fmt::Display for CoverageItem {
664 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
665 self.fmt_with_source(None).fmt(f)
666 }
667}
668
669impl CoverageItem {
670 fn ord_key(&self) -> impl Ord + use<> {
671 (
672 self.loc.source_id,
673 self.loc.lines.start,
674 self.loc.lines.end,
675 self.kind.ord_key(),
676 self.loc.bytes.start,
677 self.loc.bytes.end,
678 )
679 }
680
681 pub fn fmt_with_source(&self, src: Option<&str>) -> impl fmt::Display {
682 solar::data_structures::fmt::from_fn(move |f| {
683 match &self.kind {
684 CoverageItemKind::Line => {
685 write!(f, "Line")?;
686 }
687 CoverageItemKind::Statement => {
688 write!(f, "Statement")?;
689 }
690 CoverageItemKind::Branch { branch_id, path_id, .. } => {
691 write!(f, "Branch (branch: {branch_id}, path: {path_id})")?;
692 }
693 CoverageItemKind::Function { name } => {
694 write!(f, r#"Function "{name}""#)?;
695 }
696 }
697 write!(f, " (location: ({}), hits: {})", self.loc, self.hits)?;
698
699 if let Some(src) = src
700 && let Some(src) = src.get(self.loc.bytes())
701 {
702 write!(f, " -> ")?;
703
704 let max_len = 64;
705 let max_half = max_len / 2;
706
707 if src.len() > max_len {
708 write!(f, "\"{}", src[..max_half].escape_debug())?;
709 write!(f, "...")?;
710 write!(f, "{}\"", src[src.len() - max_half..].escape_debug())?;
711 } else {
712 write!(f, "{src:?}")?;
713 }
714 }
715
716 Ok(())
717 })
718 }
719}
720
721#[derive(Clone, Debug)]
723pub struct SourceLocation {
724 pub source_id: usize,
726 pub contract_name: Arc<str>,
728 pub bytes: Range<u32>,
730 pub lines: Range<u32>,
732}
733
734impl fmt::Display for SourceLocation {
735 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
736 write!(f, "source ID: {}, lines: {:?}, bytes: {:?}", self.source_id, self.lines, self.bytes)
737 }
738}
739
740impl SourceLocation {
741 pub const fn bytes(&self) -> Range<usize> {
743 self.bytes.start as usize..self.bytes.end as usize
744 }
745
746 pub fn len(&self) -> u32 {
748 self.bytes.len() as u32
749 }
750
751 pub fn is_empty(&self) -> bool {
753 self.len() == 0
754 }
755}
756
757#[derive(Clone, Debug, Default)]
759pub struct CoverageSummary {
760 pub line_count: usize,
762 pub line_hits: usize,
764 pub statement_count: usize,
766 pub statement_hits: usize,
768 pub branch_count: usize,
770 pub branch_hits: usize,
772 pub function_count: usize,
774 pub function_hits: usize,
776}
777
778impl CoverageSummary {
779 pub fn new() -> Self {
781 Self::default()
782 }
783
784 pub fn from_items<'a>(items: impl IntoIterator<Item = &'a CoverageItem>) -> Self {
786 let mut summary = Self::default();
787 summary.add_items(items);
788 summary
789 }
790
791 pub const fn merge(&mut self, other: &Self) {
793 let Self {
794 line_count,
795 line_hits,
796 statement_count,
797 statement_hits,
798 branch_count,
799 branch_hits,
800 function_count,
801 function_hits,
802 } = self;
803 *line_count += other.line_count;
804 *line_hits += other.line_hits;
805 *statement_count += other.statement_count;
806 *statement_hits += other.statement_hits;
807 *branch_count += other.branch_count;
808 *branch_hits += other.branch_hits;
809 *function_count += other.function_count;
810 *function_hits += other.function_hits;
811 }
812
813 pub const fn add_item(&mut self, item: &CoverageItem) {
815 match item.kind {
816 CoverageItemKind::Line => {
817 self.line_count += 1;
818 if item.hits > 0 {
819 self.line_hits += 1;
820 }
821 }
822 CoverageItemKind::Statement => {
823 self.statement_count += 1;
824 if item.hits > 0 {
825 self.statement_hits += 1;
826 }
827 }
828 CoverageItemKind::Branch { .. } => {
829 self.branch_count += 1;
830 if item.hits > 0 {
831 self.branch_hits += 1;
832 }
833 }
834 CoverageItemKind::Function { .. } => {
835 self.function_count += 1;
836 if item.hits > 0 {
837 self.function_hits += 1;
838 }
839 }
840 }
841 }
842
843 pub fn add_items<'a>(&mut self, items: impl IntoIterator<Item = &'a CoverageItem>) {
845 for item in items {
846 self.add_item(item);
847 }
848 }
849}