1#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_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::Display,
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 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#[derive(Clone, Debug, Default)]
169pub struct HitMaps(pub B256HashMap<HitMap>);
170
171impl HitMaps {
172 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 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 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#[derive(Clone, Debug)]
214pub struct HitMap {
215 hits: FxHashMap<u32, u32>,
216 bytecode: Bytes,
217}
218
219impl HitMap {
220 #[inline]
222 pub fn new(bytecode: Bytes) -> Self {
223 Self { bytecode, hits: HashMap::with_capacity_and_hasher(1024, Default::default()) }
224 }
225
226 #[inline]
228 pub fn bytecode(&self) -> &Bytes {
229 &self.bytecode
230 }
231
232 #[inline]
234 pub fn get(&self, pc: u32) -> Option<NonZeroU32> {
235 NonZeroU32::new(self.hits.get(&pc).copied().unwrap_or(0))
236 }
237
238 #[inline]
240 pub fn hit(&mut self, pc: u32) {
241 self.hits(pc, 1)
242 }
243
244 #[inline]
246 pub fn hits(&mut self, pc: u32, hits: u32) {
247 *self.hits.entry(pc).or_default() += hits;
248 }
249
250 pub fn merge(&mut self, other: &Self) {
252 self.hits.reserve(other.len());
253 for (pc, hits) in other.iter() {
254 self.hits(pc, hits);
255 }
256 }
257
258 #[inline]
260 pub fn iter(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
261 self.hits.iter().map(|(&pc, &hits)| (pc, hits))
262 }
263
264 #[inline]
266 pub fn len(&self) -> usize {
267 self.hits.len()
268 }
269
270 #[inline]
272 pub fn is_empty(&self) -> bool {
273 self.hits.is_empty()
274 }
275}
276
277#[derive(Clone, Debug, PartialEq, Eq, Hash)]
279pub struct ContractId {
280 pub version: Version,
281 pub source_id: usize,
282 pub contract_name: Arc<str>,
283}
284
285impl Display for ContractId {
286 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287 write!(
288 f,
289 "Contract \"{}\" (solc {}, source ID {})",
290 self.contract_name, self.version, self.source_id
291 )
292 }
293}
294
295#[derive(Clone, Debug)]
297pub struct ItemAnchor {
298 pub instruction: u32,
300 pub item_id: u32,
302}
303
304impl Display for ItemAnchor {
305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306 write!(f, "IC {} -> Item {}", self.instruction, self.item_id)
307 }
308}
309
310#[derive(Clone, Debug)]
311pub enum CoverageItemKind {
312 Line,
314 Statement,
316 Branch {
318 branch_id: u32,
323 path_id: u32,
327 is_first_opcode: bool,
329 },
330 Function {
332 name: Box<str>,
334 },
335}
336
337#[derive(Clone, Debug)]
338pub struct CoverageItem {
339 pub kind: CoverageItemKind,
341 pub loc: SourceLocation,
343 pub hits: u32,
345}
346
347impl Display for CoverageItem {
348 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349 match &self.kind {
350 CoverageItemKind::Line => {
351 write!(f, "Line")?;
352 }
353 CoverageItemKind::Statement => {
354 write!(f, "Statement")?;
355 }
356 CoverageItemKind::Branch { branch_id, path_id, .. } => {
357 write!(f, "Branch (branch: {branch_id}, path: {path_id})")?;
358 }
359 CoverageItemKind::Function { name } => {
360 write!(f, r#"Function "{name}""#)?;
361 }
362 }
363 write!(f, " (location: ({}), hits: {})", self.loc, self.hits)
364 }
365}
366
367#[derive(Clone, Debug)]
369pub struct SourceLocation {
370 pub source_id: usize,
372 pub contract_name: Arc<str>,
374 pub bytes: Range<u32>,
376 pub lines: Range<u32>,
378}
379
380impl Display for SourceLocation {
381 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
382 write!(f, "source ID: {}, lines: {:?}, bytes: {:?}", self.source_id, self.lines, self.bytes)
383 }
384}
385
386impl SourceLocation {
387 pub fn len(&self) -> u32 {
389 self.bytes.len() as u32
390 }
391
392 pub fn is_empty(&self) -> bool {
394 self.len() == 0
395 }
396}
397
398#[derive(Clone, Debug, Default)]
400pub struct CoverageSummary {
401 pub line_count: usize,
403 pub line_hits: usize,
405 pub statement_count: usize,
407 pub statement_hits: usize,
409 pub branch_count: usize,
411 pub branch_hits: usize,
413 pub function_count: usize,
415 pub function_hits: usize,
417}
418
419impl CoverageSummary {
420 pub fn new() -> Self {
422 Self::default()
423 }
424
425 pub fn from_items<'a>(items: impl IntoIterator<Item = &'a CoverageItem>) -> Self {
427 let mut summary = Self::default();
428 summary.add_items(items);
429 summary
430 }
431
432 pub fn merge(&mut self, other: &Self) {
434 let Self {
435 line_count,
436 line_hits,
437 statement_count,
438 statement_hits,
439 branch_count,
440 branch_hits,
441 function_count,
442 function_hits,
443 } = self;
444 *line_count += other.line_count;
445 *line_hits += other.line_hits;
446 *statement_count += other.statement_count;
447 *statement_hits += other.statement_hits;
448 *branch_count += other.branch_count;
449 *branch_hits += other.branch_hits;
450 *function_count += other.function_count;
451 *function_hits += other.function_hits;
452 }
453
454 pub fn add_item(&mut self, item: &CoverageItem) {
456 match item.kind {
457 CoverageItemKind::Line => {
458 self.line_count += 1;
459 if item.hits > 0 {
460 self.line_hits += 1;
461 }
462 }
463 CoverageItemKind::Statement => {
464 self.statement_count += 1;
465 if item.hits > 0 {
466 self.statement_hits += 1;
467 }
468 }
469 CoverageItemKind::Branch { .. } => {
470 self.branch_count += 1;
471 if item.hits > 0 {
472 self.branch_hits += 1;
473 }
474 }
475 CoverageItemKind::Function { .. } => {
476 self.function_count += 1;
477 if item.hits > 0 {
478 self.function_hits += 1;
479 }
480 }
481 }
482 }
483
484 pub fn add_items<'a>(&mut self, items: impl IntoIterator<Item = &'a CoverageItem>) {
486 for item in items {
487 self.add_item(item);
488 }
489 }
490}