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<(Version, usize), PathBuf>,
42 pub source_paths_to_ids: HashMap<(Version, PathBuf), usize>,
44 pub analyses: HashMap<Version, 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, 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 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 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 pub fn add_analysis(&mut self, version: Version, analysis: SourceAnalysis) {
78 self.analyses.insert(version, analysis);
79 }
80
81 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 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 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 pub fn add_hit_map(
122 &mut self,
123 contract_id: &ContractId,
124 hit_map: &HitMap,
125 is_deployed_code: bool,
126 ) -> Result<()> {
127 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 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 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 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#[derive(Clone, Debug, Default)]
191pub struct HitMaps(pub B256HashMap<HitMap>);
192
193impl HitMaps {
194 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 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 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#[derive(Clone, Debug)]
236pub struct HitMap {
237 hits: FxHashMap<u32, u32>,
238 bytecode: Bytes,
239}
240
241impl HitMap {
242 #[inline]
244 pub fn new(bytecode: Bytes) -> Self {
245 Self { bytecode, hits: HashMap::with_capacity_and_hasher(1024, Default::default()) }
246 }
247
248 #[inline]
250 pub const fn bytecode(&self) -> &Bytes {
251 &self.bytecode
252 }
253
254 #[inline]
256 pub fn get(&self, pc: u32) -> Option<NonZeroU32> {
257 NonZeroU32::new(self.hits.get(&pc).copied().unwrap_or(0))
258 }
259
260 #[inline]
262 pub fn hit(&mut self, pc: u32) {
263 self.hits(pc, 1)
264 }
265
266 #[inline]
268 pub fn hits(&mut self, pc: u32, hits: u32) {
269 *self.hits.entry(pc).or_default() += hits;
270 }
271
272 #[inline]
274 pub fn reserve(&mut self, additional: usize) {
275 self.hits.reserve(additional);
276 }
277
278 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 #[inline]
288 pub fn iter(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
289 self.hits.iter().map(|(&pc, &hits)| (pc, hits))
290 }
291
292 #[inline]
294 pub fn len(&self) -> usize {
295 self.hits.len()
296 }
297
298 #[inline]
300 pub fn is_empty(&self) -> bool {
301 self.hits.is_empty()
302 }
303}
304
305#[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#[derive(Clone, Debug)]
325pub struct ItemAnchor {
326 pub instruction: u32,
328 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 Line,
342 Statement,
344 Branch {
346 branch_id: u32,
351 path_id: u32,
355 is_first_opcode: bool,
357 },
358 Function {
360 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 pub kind: CoverageItemKind,
400 pub loc: SourceLocation,
402 pub anchor_loc: Option<SourceLocation>,
404 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#[derive(Clone, Debug)]
488pub struct SourceLocation {
489 pub source_id: usize,
491 pub contract_name: Arc<str>,
493 pub bytes: Range<u32>,
495 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 pub const fn bytes(&self) -> Range<usize> {
508 self.bytes.start as usize..self.bytes.end as usize
509 }
510
511 pub fn len(&self) -> u32 {
513 self.bytes.len() as u32
514 }
515
516 pub fn is_empty(&self) -> bool {
518 self.len() == 0
519 }
520}
521
522#[derive(Clone, Debug, Default)]
524pub struct CoverageSummary {
525 pub line_count: usize,
527 pub line_hits: usize,
529 pub statement_count: usize,
531 pub statement_hits: usize,
533 pub branch_count: usize,
535 pub branch_hits: usize,
537 pub function_count: usize,
539 pub function_hits: usize,
541}
542
543impl CoverageSummary {
544 pub fn new() -> Self {
546 Self::default()
547 }
548
549 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 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 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 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}