1use std::collections::BTreeSet;
9
10use ra_ap_syntax::{
11 AstNode, Edition, SourceFile, SyntaxKind, TextRange,
12 ast::{self, BinaryOp, HasAttrs, HasLoopBody, HasName, LogicOp},
13};
14use serde_json::json;
15use sha2::{Digest, Sha256};
16
17use crate::{
18 coverage_analysis::PointKind,
19 coverage_report::{
20 BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
21 },
22};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum RustInstrumenterError {
26 SourceTooLarge,
27 Parse(Vec<String>),
28 InvalidRange,
29 InvalidRuntimePath,
30}
31
32impl std::fmt::Display for RustInstrumenterError {
33 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 Self::SourceTooLarge => write!(formatter, "Rust source exceeds the parser range"),
36 Self::Parse(errors) => write!(formatter, "Rust parse failed: {}", errors.join("; ")),
37 Self::InvalidRange => write!(formatter, "Rust parser returned an invalid range"),
38 Self::InvalidRuntimePath => write!(formatter, "invalid generated Rust runtime path"),
39 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq)]
44pub struct RustInstrumentedSource {
45 pub code: String,
46 pub manifest: CoverageManifest,
47 pub probes: RustProbeLayout,
50}
51
52#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
56pub struct RustProbeLayout {
57 #[serde(default)]
60 pub token: String,
61 pub hits: Vec<String>,
63 pub assertions: Vec<String>,
65 pub decisions: Vec<String>,
66 #[serde(default)]
71 pub cfgs: Vec<RustCfgGate>,
72 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
77 pub crates: std::collections::BTreeMap<String, Vec<String>>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
82pub struct RustCfgGate {
83 pub file: String,
84 pub predicate: String,
87 pub line: usize,
89 pub end_line: usize,
90 pub obligations: Vec<String>,
91}
92
93#[derive(Default)]
94struct ProbeNumbering {
95 layout: RustProbeLayout,
96 hits: std::collections::HashMap<String, u32>,
97 assertions: std::collections::HashMap<String, u32>,
98 decisions: std::collections::HashMap<String, u32>,
99}
100
101impl ProbeNumbering {
102 fn from_layout(layout: RustProbeLayout) -> Self {
103 let index = |ids: &[String]| {
104 ids.iter()
105 .enumerate()
106 .map(|(ordinal, id)| (id.clone(), ordinal as u32))
107 .collect()
108 };
109 Self {
110 hits: index(&layout.hits),
111 assertions: index(&layout.assertions),
112 decisions: index(&layout.decisions),
113 layout,
114 }
115 }
116
117 fn number(
118 ids: &mut Vec<String>,
119 index: &mut std::collections::HashMap<String, u32>,
120 id: &str,
121 ) -> u32 {
122 if let Some(ordinal) = index.get(id) {
123 return *ordinal;
124 }
125 let ordinal = ids.len() as u32;
126 ids.push(id.to_owned());
127 index.insert(id.to_owned(), ordinal);
128 ordinal
129 }
130}
131
132thread_local! {
133 static NUMBERING: std::cell::RefCell<ProbeNumbering> =
137 std::cell::RefCell::new(ProbeNumbering::default());
138}
139
140fn hit_ordinal(id: &str) -> u32 {
141 NUMBERING.with(|numbering| {
142 let numbering = &mut *numbering.borrow_mut();
143 ProbeNumbering::number(&mut numbering.layout.hits, &mut numbering.hits, id)
144 })
145}
146
147fn assertion_ordinal(id: &str) -> u32 {
148 NUMBERING.with(|numbering| {
149 let numbering = &mut *numbering.borrow_mut();
150 ProbeNumbering::number(
151 &mut numbering.layout.assertions,
152 &mut numbering.assertions,
153 id,
154 )
155 })
156}
157
158fn cfg_gate_ordinal(gate: RustCfgGate) -> u32 {
159 NUMBERING.with(|numbering| {
160 let gates = &mut numbering.borrow_mut().layout.cfgs;
161 gates.push(gate);
162 (gates.len() - 1) as u32
163 })
164}
165
166fn decision_ordinal(id: &str) -> u32 {
167 NUMBERING.with(|numbering| {
168 let numbering = &mut *numbering.borrow_mut();
169 ProbeNumbering::number(
170 &mut numbering.layout.decisions,
171 &mut numbering.decisions,
172 id,
173 )
174 })
175}
176
177struct NumberingScope<'a> {
180 layout: &'a mut RustProbeLayout,
181}
182
183impl<'a> NumberingScope<'a> {
184 fn enter(layout: &'a mut RustProbeLayout) -> Self {
185 let taken = std::mem::take(layout);
186 NUMBERING.with(|numbering| *numbering.borrow_mut() = ProbeNumbering::from_layout(taken));
187 Self { layout }
188 }
189}
190
191impl Drop for NumberingScope<'_> {
192 fn drop(&mut self) {
193 *self.layout =
194 NUMBERING.with(|numbering| std::mem::take(&mut *numbering.borrow_mut()).layout);
195 }
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199enum InsertionKind {
200 End,
201 Direct,
202 Start,
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
206struct Insertion {
207 offset: usize,
208 kind: InsertionKind,
209 scope_len: usize,
210 rank: usize,
211 text: String,
212}
213
214fn valid_runtime_path(path: &str) -> bool {
215 let mut parts = path.split("::");
216 if !matches!(parts.next(), Some("crate")) {
217 return false;
218 }
219 let parts = parts.collect::<Vec<_>>();
220 !parts.is_empty()
221 && parts.into_iter().all(|part| {
222 !part.is_empty()
223 && part.bytes().enumerate().all(|(index, byte)| {
224 byte == b'_'
225 || byte.is_ascii_alphabetic()
226 || (index > 0 && byte.is_ascii_digit())
227 })
228 })
229}
230
231fn in_const_context(node: &ra_ap_syntax::SyntaxNode) -> bool {
249 let start = node.text_range().start();
250 node.ancestors().any(|ancestor| {
251 ast::Fn::cast(ancestor.clone()).is_some_and(|function| function.const_token().is_some())
252 || ast::BlockExpr::cast(ancestor.clone())
253 .is_some_and(|block| block.const_token().is_some())
254 || ast::Const::can_cast(ancestor.kind())
255 || ast::Static::can_cast(ancestor.kind())
256 || ast::ConstArg::can_cast(ancestor.kind())
257 || ast::ArrayExpr::cast(ancestor).is_some_and(|array| {
258 array
259 .semicolon_token()
260 .is_some_and(|semicolon| start >= semicolon.text_range().end())
261 })
262 })
263}
264
265fn in_global_allocator(node: &ra_ap_syntax::SyntaxNode) -> bool {
280 node.ancestors()
281 .any(|ancestor| in_global_allocator_impl(&ancestor))
282}
283
284fn in_global_allocator_impl(node: &ra_ap_syntax::SyntaxNode) -> bool {
286 ast::Impl::cast(node.clone()).is_some_and(|block| {
287 block.trait_().is_some_and(|implemented| {
288 implemented
289 .syntax()
290 .descendants_with_tokens()
291 .filter_map(|element| element.into_token())
292 .any(|token| token.kind() == SyntaxKind::IDENT && token.text() == "GlobalAlloc")
293 })
294 })
295}
296
297fn cannot_carry_probe(node: &ra_ap_syntax::SyntaxNode) -> bool {
299 in_const_context(node) || in_global_allocator(node)
300}
301
302fn range_offsets(range: TextRange) -> (usize, usize) {
303 (usize::from(range.start()), usize::from(range.end()))
304}
305
306fn push_wrapper(
307 insertions: &mut Vec<Insertion>,
308 range: TextRange,
309 scope: TextRange,
310 rank: usize,
311 prefix: String,
312 suffix: String,
313) {
314 let (start, end) = range_offsets(range);
315 let (scope_start, scope_end) = range_offsets(scope);
316 let scope_len = scope_end - scope_start;
317 insertions.push(Insertion {
318 offset: start,
319 kind: InsertionKind::Start,
320 scope_len,
321 rank,
322 text: prefix,
323 });
324 insertions.push(Insertion {
325 offset: end,
326 kind: InsertionKind::End,
327 scope_len,
328 rank,
329 text: suffix,
330 });
331}
332
333fn push_direct(insertions: &mut Vec<Insertion>, offset: usize, text: String) {
334 insertions.push(Insertion {
335 offset,
336 kind: InsertionKind::Direct,
337 scope_len: 0,
338 rank: 0,
339 text,
340 });
341}
342
343fn apply_insertions(
344 source: &str,
345 mut insertions: Vec<Insertion>,
346) -> Result<String, RustInstrumenterError> {
347 if insertions
348 .iter()
349 .any(|edit| edit.offset > source.len() || !source.is_char_boundary(edit.offset))
350 {
351 return Err(RustInstrumenterError::InvalidRange);
352 }
353 insertions.sort_by(|left, right| {
354 left.offset.cmp(&right.offset).then_with(|| {
355 let kind_order = |kind: InsertionKind| match kind {
356 InsertionKind::End => 0,
357 InsertionKind::Direct => 1,
358 InsertionKind::Start => 2,
359 };
360 kind_order(left.kind)
361 .cmp(&kind_order(right.kind))
362 .then_with(|| match left.kind {
363 InsertionKind::End => left
364 .scope_len
365 .cmp(&right.scope_len)
366 .then_with(|| right.rank.cmp(&left.rank)),
367 InsertionKind::Direct => std::cmp::Ordering::Equal,
368 InsertionKind::Start => right
369 .scope_len
370 .cmp(&left.scope_len)
371 .then_with(|| left.rank.cmp(&right.rank)),
372 })
373 })
374 });
375
376 let mut output = source.to_owned();
377 let mut index = insertions.len();
378 while index > 0 {
379 let offset = insertions[index - 1].offset;
380 let start = insertions[..index].partition_point(|insertion| insertion.offset < offset);
381 let text = insertions[start..index]
382 .iter()
383 .map(|insertion| insertion.text.as_str())
384 .collect::<String>();
385 output.insert_str(offset, &text);
386 index = start;
387 }
388 Ok(output)
389}
390
391fn limitation_id(kind: &str, file: &str) -> String {
395 format!("{kind}#{file}")
396}
397
398fn add_manifest_limitation(manifest: &mut CoverageManifest, file: &str, id: &str, reason: &str) {
399 let id = limitation_id(id, file);
400 if manifest.limitations.iter().any(|limitation| {
401 limitation.get("id").and_then(|value| value.as_str()) == Some(id.as_str())
402 }) {
403 return;
404 }
405 manifest.limitations.push(json!({
406 "id": id,
407 "kind": "source-scope",
408 "file": file,
409 "line": 1,
410 "column": 0,
411 "source": "",
412 "reason": reason,
413 "blocking": false
416 }));
417}
418
419fn allocate_frame_name(
420 file: &str,
421 condition: &ast::Expr,
422 kind: &str,
423 identifiers: &mut BTreeSet<String>,
424) -> String {
425 let id = stable_id(file, "decision", condition.syntax().text_range(), kind);
426 let suffix = id.rsplit(':').next().unwrap_or("decision");
427 let base = format!("__supercov_decision_{suffix}");
428 let mut candidate = base.clone();
429 let mut attempt = 0_usize;
430 while !identifiers.insert(candidate.clone()) {
431 attempt += 1;
432 candidate = format!("{base}_{attempt}");
433 }
434 candidate
435}
436
437fn allocate_table_name(
440 file: &str,
441 expression: &ast::MatchExpr,
442 identifiers: &mut BTreeSet<String>,
443) -> String {
444 let id = stable_id(file, "match", expression.syntax().text_range(), "arms");
445 let suffix = id
446 .rsplit(':')
447 .next()
448 .unwrap_or("match")
449 .to_ascii_uppercase();
450 let base = format!("__SUPERCOV_ARMS_{suffix}");
451 let mut candidate = base.clone();
452 let mut attempt = 0_usize;
453 while !identifiers.insert(candidate.clone()) {
454 attempt += 1;
455 candidate = format!("{base}_{attempt}");
456 }
457 candidate
458}
459
460fn allocate_flag_name(
462 file: &str,
463 expression: &ast::WhileExpr,
464 identifiers: &mut BTreeSet<String>,
465) -> String {
466 let id = stable_id(file, "loop", expression.syntax().text_range(), "flag");
467 let suffix = id.rsplit(':').next().unwrap_or("loop");
468 let base = format!("__supercov_loop_{suffix}");
469 let mut candidate = base.clone();
470 let mut attempt = 0_usize;
471 while !identifiers.insert(candidate.clone()) {
472 attempt += 1;
473 candidate = format!("{base}_{attempt}");
474 }
475 candidate
476}
477
478impl std::error::Error for RustInstrumenterError {}
479
480struct SourceLocations<'a> {
481 source: &'a str,
482 line_starts: Vec<usize>,
483}
484
485impl<'a> SourceLocations<'a> {
486 fn new(source: &'a str) -> Self {
487 let mut line_starts = vec![0];
488 line_starts.extend(
489 source
490 .bytes()
491 .enumerate()
492 .filter_map(|(index, byte)| (byte == b'\n').then_some(index + 1)),
493 );
494 Self {
495 source,
496 line_starts,
497 }
498 }
499
500 fn range(&self, range: TextRange) -> Result<(usize, usize), RustInstrumenterError> {
501 let start = usize::from(range.start());
502 let end = usize::from(range.end());
503 if start > end
504 || end > self.source.len()
505 || !self.source.is_char_boundary(start)
506 || !self.source.is_char_boundary(end)
507 {
508 return Err(RustInstrumenterError::InvalidRange);
509 }
510 Ok((start, end))
511 }
512
513 fn line_column(&self, offset: usize) -> (usize, usize) {
514 let line_index = self.line_starts.partition_point(|start| *start <= offset) - 1;
515 (line_index + 1, offset - self.line_starts[line_index])
516 }
517
518 fn text(&self, range: TextRange) -> Result<String, RustInstrumenterError> {
519 let (start, end) = self.range(range)?;
520 Ok(self.source[start..end].trim().to_owned())
521 }
522}
523
524fn stable_id(file: &str, kind: &str, range: TextRange, suffix: &str) -> String {
525 let mut hash = Sha256::new();
526 let start = usize::from(range.start()).to_string();
527 let end = usize::from(range.end()).to_string();
528 for value in [file, kind, &start, &end, suffix] {
529 hash.update(value.as_bytes());
530 hash.update([0]);
531 }
532 let digest = hash.finalize();
533 let mut encoded = String::with_capacity(24);
534 for byte in &digest[..12] {
535 use std::fmt::Write as _;
536 write!(&mut encoded, "{byte:02x}").expect("writing to a string cannot fail");
537 }
538 format!("rs:{kind}:{encoded}")
539}
540
541struct RustObligationCollector<'a> {
542 file: &'a str,
543 locations: SourceLocations<'a>,
544 manifest: CoverageManifest,
545 point_ids: BTreeSet<String>,
546 decision_ids: BTreeSet<String>,
547 branch_ids: BTreeSet<String>,
548 site_limitations: BTreeSet<&'static str>,
550 obligation_ranges: Vec<(String, TextRange)>,
553 tests: Vec<TextRange>,
555 compiled_out: Vec<TextRange>,
557 gates: Vec<TextRange>,
560 gated: Vec<Vec<String>>,
561 error: Option<RustInstrumenterError>,
562}
563
564impl<'a> RustObligationCollector<'a> {
565 fn new(file: &'a str, source: &'a str) -> Self {
566 Self {
567 file,
568 locations: SourceLocations::new(source),
569 manifest: CoverageManifest {
570 unmeasured: Vec::new(),
571 decisions: Vec::new(),
572 points: Vec::new(),
573 branches: Vec::new(),
574 limitations: Vec::new(),
575 scope: None,
576 assertion_sites: Vec::new(),
577 },
578 point_ids: BTreeSet::new(),
579 decision_ids: BTreeSet::new(),
580 branch_ids: BTreeSet::new(),
581 site_limitations: BTreeSet::new(),
582 obligation_ranges: Vec::new(),
583 tests: Vec::new(),
584 compiled_out: Vec::new(),
585 gates: Vec::new(),
586 gated: Vec::new(),
587 error: None,
588 }
589 }
590
591 fn leaving_out(mut self, tests: Vec<TextRange>, compiled_out: Vec<TextRange>) -> Self {
592 self.tests = tests;
593 self.compiled_out = compiled_out;
594 self
595 }
596
597 fn gating(mut self, gates: Vec<TextRange>) -> Self {
598 self.gates = gates;
599 self
600 }
601
602 fn gate_obligations(&mut self) {
604 let kept = self
605 .manifest
606 .points
607 .iter()
608 .map(|point| point.id.as_str())
609 .chain(
610 self.manifest
611 .decisions
612 .iter()
613 .map(|decision| decision.id.as_str()),
614 )
615 .chain(
616 self.manifest
617 .branches
618 .iter()
619 .map(|branch| branch.id.as_str()),
620 )
621 .collect::<BTreeSet<_>>();
622 self.gated = self
623 .gates
624 .iter()
625 .map(|gate| {
626 let mut ids = self
627 .obligation_ranges
628 .iter()
629 .filter(|(id, range)| gate.contains_range(*range) && kept.contains(id.as_str()))
630 .map(|(id, _)| id.clone())
631 .collect::<Vec<_>>();
632 ids.sort();
633 ids.dedup();
634 ids
635 })
636 .collect();
637 }
638
639 fn leave_out_test_code(&mut self) {
643 let uncompiled = self
646 .obligation_ranges
647 .iter()
648 .filter(|(_, range)| in_test_code(&self.compiled_out, *range))
649 .map(|(id, _)| id.clone())
650 .collect::<BTreeSet<_>>();
651 let inside = self
652 .obligation_ranges
653 .iter()
654 .filter(|(_, range)| in_test_code(&self.tests, *range))
655 .map(|(id, _)| id.clone())
656 .chain(uncompiled.iter().cloned())
657 .collect::<BTreeSet<_>>();
658 if inside.is_empty() {
659 return;
660 }
661 for point in std::mem::take(&mut self.manifest.points) {
662 if !inside.contains(&point.id) {
663 self.manifest.points.push(point);
664 } else if point.kind == PointKind::Statement && !uncompiled.contains(&point.id) {
665 self.manifest.assertion_sites.push(point);
666 }
667 }
668 self.manifest
669 .decisions
670 .retain(|decision| !inside.contains(&decision.id));
671 self.manifest
672 .branches
673 .retain(|branch| !inside.contains(&branch.id));
674 self.manifest.unmeasured.retain(|id| !inside.contains(id));
675 }
676
677 fn location_source(&mut self, range: TextRange) -> Option<(usize, usize, String)> {
678 let result = self.locations.range(range).map(|(start, _)| {
679 let (line, column) = self.locations.line_column(start);
680 (line, column, self.locations.text(range))
681 });
682 match result {
683 Ok((line, column, Ok(source))) => Some((line, column, source)),
684 Ok((_, _, Err(error))) | Err(error) => {
685 self.error.get_or_insert(error);
686 None
687 }
688 }
689 }
690
691 fn point(&mut self, range: TextRange, kind: PointKind, label: Option<String>) {
692 self.point_located(range, range, kind, label);
693 }
694
695 fn point_located(
699 &mut self,
700 range: TextRange,
701 location: TextRange,
702 kind: PointKind,
703 label: Option<String>,
704 ) {
705 let kind_name = match kind {
706 PointKind::Statement => "statement",
707 PointKind::Function => "function",
708 };
709 let id = stable_id(self.file, kind_name, range, label.as_deref().unwrap_or(""));
710 self.obligation_ranges.push((id.clone(), range));
711 if !self.point_ids.insert(id.clone()) {
712 return;
713 }
714 let Some((line, column, source)) = self.location_source(location) else {
715 return;
716 };
717 self.manifest.points.push(PointMeta {
718 id,
719 kind,
720 file: self.file.into(),
721 line,
722 column,
723 source,
724 label,
725 });
726 }
727
728 fn atomic_condition_ranges(expression: &ast::Expr, ranges: &mut Vec<TextRange>) {
729 match expression {
730 ast::Expr::ParenExpr(paren) => {
731 if let Some(inner) = paren.expr() {
732 Self::atomic_condition_ranges(&inner, ranges);
733 } else {
734 ranges.push(expression.syntax().text_range());
735 }
736 }
737 ast::Expr::BinExpr(binary)
738 if matches!(
739 binary.op_kind(),
740 Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))
741 ) =>
742 {
743 if let Some(left) = binary.lhs() {
744 Self::atomic_condition_ranges(&left, ranges);
745 }
746 if let Some(right) = binary.rhs() {
747 Self::atomic_condition_ranges(&right, ranges);
748 }
749 }
750 _ => ranges.push(expression.syntax().text_range()),
751 }
752 }
753
754 fn decision(&mut self, test: &ast::Expr, kind: &str) {
755 let range = test.syntax().text_range();
756 let id = stable_id(self.file, "decision", range, kind);
757 self.obligation_ranges.push((id.clone(), range));
758 if !self.decision_ids.insert(id.clone()) {
759 return;
760 }
761 let Some((line, column, source)) = self.location_source(range) else {
762 return;
763 };
764 let mut condition_ranges = Vec::new();
765 Self::atomic_condition_ranges(test, &mut condition_ranges);
766 let mut conditions = Vec::with_capacity(condition_ranges.len());
767 for condition in condition_ranges {
768 match self.locations.text(condition) {
769 Ok(source) => conditions.push(source),
770 Err(error) => {
771 self.error.get_or_insert(error);
772 return;
773 }
774 }
775 }
776 self.manifest.decisions.push(DecisionMeta {
777 id: id.clone(),
778 file: self.file.into(),
779 line,
780 column,
781 source: source.clone(),
782 conditions,
783 kind: kind.into(),
784 });
785 self.branch_with_id(
786 format!("{id}:outcome"),
787 range,
788 kind,
789 source,
790 [("true", "true"), ("false", "false")],
791 );
792 }
793
794 fn branch<const N: usize>(
795 &mut self,
796 range: TextRange,
797 kind: &str,
798 alternatives: [(&str, &str); N],
799 ) {
800 let id = stable_id(self.file, "branch", range, kind);
801 let Some((_, _, source)) = self.location_source(range) else {
802 return;
803 };
804 self.branch_with_id(id, range, kind, source, alternatives);
805 }
806
807 fn branch_with_id<const N: usize>(
808 &mut self,
809 id: String,
810 range: TextRange,
811 kind: &str,
812 source: String,
813 alternatives: [(&str, &str); N],
814 ) {
815 self.obligation_ranges.push((id.clone(), range));
816 if !self.branch_ids.insert(id.clone()) {
817 return;
818 }
819 let Some((line, column, _)) = self.location_source(range) else {
820 return;
821 };
822 self.manifest.branches.push(BranchMeta {
823 id: id.clone(),
824 kind: kind.into(),
825 file: self.file.into(),
826 line,
827 column,
828 source,
829 alternatives: alternatives
830 .into_iter()
831 .map(|(suffix, label)| BranchAlternativeMeta {
832 id: format!("{id}:{suffix}"),
833 label: label.into(),
834 })
835 .collect(),
836 });
837 }
838
839 fn decline_unreachable_obligations(&mut self, root: &ra_ap_syntax::SyntaxNode) {
844 let mut declined = self
848 .manifest
849 .unmeasured
850 .iter()
851 .cloned()
852 .collect::<BTreeSet<_>>();
853 for (id, range) in &self.obligation_ranges {
854 let node = match root.covering_element(*range) {
855 ra_ap_syntax::NodeOrToken::Node(node) => Some(node),
856 ra_ap_syntax::NodeOrToken::Token(token) => token.parent(),
857 };
858 if node.is_some_and(|node| cannot_carry_probe(&node)) {
859 declined.insert(id.clone());
860 }
861 }
862 self.manifest.unmeasured = declined.into_iter().collect();
863 }
864
865 fn site_limitation(&mut self, id: &'static str, range: TextRange, reason: String) {
871 if self.site_limitations.contains(id)
872 || in_test_code(&self.tests, range)
873 || in_test_code(&self.compiled_out, range)
874 {
875 return;
876 }
877 let Some((line, column, source)) = self.location_source(range) else {
878 return;
879 };
880 self.site_limitations.insert(id);
881 let source = source.lines().next().unwrap_or("").trim();
884 let source = if source.chars().count() > 120 {
885 format!("{}...", source.chars().take(117).collect::<String>())
886 } else {
887 source.to_owned()
888 };
889 self.manifest.limitations.push(json!({
890 "id": limitation_id(id, self.file),
891 "kind": "source-scope",
892 "file": self.file,
893 "line": line,
894 "column": column,
895 "source": source,
896 "blocking": false,
900 "reason": reason
901 }));
902 }
903
904 fn collect(
905 self,
906 file: &SourceFile,
907 assertions: &[TextRange],
908 matches: &[TextRange],
909 ) -> Result<CoverageManifest, RustInstrumenterError> {
910 self.collect_gated(file, assertions, matches)
911 .map(|(manifest, _)| manifest)
912 }
913
914 fn collect_gated(
916 mut self,
917 file: &SourceFile,
918 assertions: &[TextRange],
919 matches: &[TextRange],
920 ) -> Result<(CoverageManifest, Vec<Vec<String>>), RustInstrumenterError> {
921 let root = file.syntax();
922
923 for list in root.descendants().filter_map(ast::StmtList::cast) {
924 for statement in list.statements() {
925 match statement {
926 ast::Stmt::ExprStmt(statement) => {
927 self.point(statement.syntax().text_range(), PointKind::Statement, None);
928 }
929 ast::Stmt::LetStmt(statement) => {
930 self.point(statement.syntax().text_range(), PointKind::Statement, None);
931 }
932 ast::Stmt::Item(_) => {}
933 }
934 }
935 if let Some(tail) = list.tail_expr() {
936 self.point(tail.syntax().text_range(), PointKind::Statement, None);
937 }
938 }
939
940 for function in root.descendants().filter_map(ast::Fn::cast) {
941 if function.body().is_none() {
942 continue;
943 }
944 if function.const_token().is_some() {
945 self.site_limitation(
946 "rust-const-context-not-instrumented",
947 function.syntax().text_range(),
948 "Runtime probes cannot execute in const fn or compile-time evaluation".into(),
949 );
950 continue;
951 }
952 let label = function.name().map(|name| name.text().to_string());
953 self.point_located(
954 function.syntax().text_range(),
955 item_range(function.syntax()),
956 PointKind::Function,
957 label,
958 );
959 }
960
961 for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
962 self.point(
963 closure.syntax().text_range(),
964 PointKind::Function,
965 Some("<closure>".into()),
966 );
967 }
968
969 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
970 if let Some(condition) = expression.condition() {
971 self.decision(&condition, "if");
972 }
973 }
974 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
975 if let Some(condition) = expression.condition() {
976 self.decision(&condition, "while");
977 }
978 self.branch(
979 expression.syntax().text_range(),
980 "while-loop",
981 [("zero", "zero iterations"), ("entered", "entered")],
982 );
983 }
984 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
985 if let Some(condition) = guard.condition() {
986 self.decision(&condition, "match-guard");
987 }
988 }
989 for arguments in assertions {
990 if let Some(condition) = assertion_condition(root, *arguments) {
991 self.decision(&condition, "assert");
992 }
993 }
994 for expression in standalone_matches(root, matches, assertions) {
995 self.decision(&expression, "matches");
996 }
997 for expression in value_decisions(root, assertions) {
998 self.decision(&expression, "value");
999 }
1000
1001 for binary in root.descendants().filter_map(ast::BinExpr::cast) {
1002 let kind = match binary.op_kind() {
1003 Some(BinaryOp::LogicOp(LogicOp::And)) => "logical-and",
1004 Some(BinaryOp::LogicOp(LogicOp::Or)) => "logical-or",
1005 _ => continue,
1006 };
1007 let range = binary.rhs().map_or_else(
1008 || binary.syntax().text_range(),
1009 |right| right.syntax().text_range(),
1010 );
1011 self.branch(
1012 range,
1013 kind,
1014 [
1015 ("short-circuit", "short-circuited"),
1016 ("evaluated", "right operand evaluated"),
1017 ],
1018 );
1019 }
1020
1021 for expression in root.descendants().filter_map(ast::ForExpr::cast) {
1022 self.branch(
1023 expression.syntax().text_range(),
1024 "for-loop",
1025 [("zero", "zero iterations"), ("entered", "entered")],
1026 );
1027 }
1028 for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
1029 let Some(list) = expression.match_arm_list() else {
1030 continue;
1031 };
1032 let arms = list.arms().collect::<Vec<_>>();
1033 let last = arms.len().saturating_sub(1);
1034 for (index, arm) in arms.iter().enumerate() {
1035 let range = arm.syntax().text_range();
1036 if index == last {
1037 self.branch(range, "match-arm", [("selected", "selected")]);
1041 } else {
1042 self.branch(
1043 range,
1044 "match-arm",
1045 [("missed", "not selected"), ("selected", "selected")],
1046 );
1047 }
1048 }
1049 }
1050 for expression in root.descendants().filter_map(ast::TryExpr::cast) {
1051 self.branch(
1052 expression.syntax().text_range(),
1053 "try-operator",
1054 [("continued", "continued"), ("returned", "early return")],
1055 );
1056 }
1057
1058 let mut macro_names = BTreeSet::new();
1064 let mut first_macro = None;
1065 let mut macro_sites = 0;
1066 for call in root.descendants().filter_map(ast::MacroCall::cast) {
1067 macro_names.insert(
1068 call.path()
1069 .map(|path| path.syntax().text().to_string())
1070 .unwrap_or_else(|| "?".into()),
1071 );
1072 first_macro.get_or_insert_with(|| call.syntax().text_range());
1073 macro_sites += 1;
1074 }
1075 if let Some(range) = first_macro {
1076 let named = macro_names
1077 .iter()
1078 .take(4)
1079 .map(|name| format!("`{name}!`"))
1080 .collect::<Vec<_>>()
1081 .join(", ");
1082 let rest = macro_names.len().saturating_sub(4);
1083 self.site_limitation(
1084 "rust-macro-expansion-not-instrumented",
1085 range,
1086 format!(
1087 "{named}{} expand in the compiler: their arguments and expansions are outside the owned source denominator ({macro_sites} call site{} in this file)",
1088 if rest == 0 {
1089 String::new()
1090 } else {
1091 format!(" and {rest} other macro{}", if rest == 1 { "" } else { "s" })
1092 },
1093 if macro_sites == 1 { "" } else { "s" }
1094 ),
1095 );
1096 }
1097
1098 let bears_obligation = |node: &ra_ap_syntax::SyntaxNode| {
1103 ast::StmtList::cast(node.clone()).is_some_and(|list| {
1104 list.statements().next().is_some() || list.tail_expr().is_some()
1105 }) || ast::IfExpr::can_cast(node.kind())
1106 || ast::WhileExpr::can_cast(node.kind())
1107 || ast::MatchGuard::can_cast(node.kind())
1108 || ast::ForExpr::can_cast(node.kind())
1109 || ast::MatchArm::can_cast(node.kind())
1110 || ast::TryExpr::can_cast(node.kind())
1111 || ast::ClosureExpr::can_cast(node.kind())
1112 || ast::BinExpr::cast(node.clone()).is_some_and(|binary| {
1113 matches!(
1114 binary.op_kind(),
1115 Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))
1116 )
1117 })
1118 };
1119 if let Some(node) = root
1120 .descendants()
1121 .find(|node| bears_obligation(node) && in_const_context(node))
1122 {
1123 self.site_limitation(
1124 "rust-const-context-not-instrumented",
1125 node.text_range(),
1126 "Runtime probes cannot execute in const fn or compile-time evaluation; this and any later const context in the file stay declared".into(),
1127 );
1128 }
1129 if let Some(node) = root
1130 .descendants()
1131 .find(|node| bears_obligation(node) && in_global_allocator(node))
1132 {
1133 self.site_limitation(
1134 "rust-global-allocator-not-instrumented",
1135 node.text_range(),
1136 "Probing a GlobalAlloc implementation recurses into itself, because the runtime allocates".into(),
1137 );
1138 }
1139
1140 self.decline_unreachable_obligations(root);
1141 self.leave_out_test_code();
1142 self.gate_obligations();
1143
1144 if let Some(error) = self.error {
1145 return Err(error);
1146 }
1147 self.manifest
1148 .decisions
1149 .sort_by(|left, right| left.id.cmp(&right.id));
1150 self.manifest
1151 .points
1152 .sort_by(|left, right| left.id.cmp(&right.id));
1153 self.manifest
1154 .branches
1155 .sort_by(|left, right| left.id.cmp(&right.id));
1156 self.manifest.limitations.sort_by(|left, right| {
1157 left.get("id")
1158 .and_then(|value| value.as_str())
1159 .cmp(&right.get("id").and_then(|value| value.as_str()))
1160 });
1161 Ok((self.manifest, self.gated))
1162 }
1163}
1164
1165pub const FAILED_TRANSFORM_DUMP_ENV: &str = "SUPERCOV_RUST_DUMP_FAILED_INSTRUMENTATION";
1168
1169const EXPRESSION_MACROS: &[&str] = &[
1178 "assert",
1179 "debug_assert",
1180 "assert_eq",
1181 "assert_ne",
1182 "debug_assert_eq",
1183 "debug_assert_ne",
1184 "println",
1185 "print",
1186 "eprintln",
1187 "eprint",
1188 "format",
1189 "format_args",
1190 "write",
1191 "writeln",
1192 "panic",
1193 "unreachable",
1194 "todo",
1195 "unimplemented",
1196 "vec",
1197 "dbg",
1198 "matches",
1199];
1200
1201const ASSERTION_MACROS: &[&str] = &["assert", "debug_assert"];
1203
1204const ASSERTION_STATEMENT_MACROS: &[&str] = &[
1207 "assert",
1208 "assert_eq",
1209 "assert_ne",
1210 "debug_assert",
1211 "debug_assert_eq",
1212 "debug_assert_ne",
1213];
1214
1215struct ExpressionView {
1219 text: String,
1220 assertions: Vec<TextRange>,
1221 assertion_calls: Vec<TextRange>,
1224 matches: Vec<TextRange>,
1226}
1227
1228const EDITIONS: [Edition; 4] = [
1234 Edition::Edition2024,
1235 Edition::Edition2021,
1236 Edition::Edition2018,
1237 Edition::Edition2015,
1238];
1239
1240fn parse_any_edition(source: &str) -> Result<(SourceFile, Edition), Vec<String>> {
1241 let mut newest_errors = None;
1242 for edition in EDITIONS {
1243 let parsed = SourceFile::parse(source, edition);
1244 let errors = parsed.errors();
1245 if errors.is_empty() {
1246 return Ok((parsed.tree(), edition));
1247 }
1248 newest_errors.get_or_insert_with(|| {
1249 errors
1250 .into_iter()
1251 .map(|error| error.to_string())
1252 .collect::<Vec<_>>()
1253 });
1254 }
1255 Err(newest_errors.unwrap_or_default())
1256}
1257
1258fn expression_view(source: &str, edition: Edition) -> ExpressionView {
1259 let mut text = source.to_owned();
1260 let mut assertions = Vec::new();
1261 let mut assertion_calls = Vec::new();
1262 let mut matches = Vec::new();
1263 for _ in 0..16 {
1266 let tree = SourceFile::parse(&text, edition).tree();
1267 let Some(next) = rewrite_expression_macros(
1268 &text,
1269 &tree,
1270 edition,
1271 &mut assertions,
1272 &mut assertion_calls,
1273 &mut matches,
1274 ) else {
1275 break;
1276 };
1277 text = next;
1278 }
1279 ExpressionView {
1280 text,
1281 assertions,
1282 assertion_calls,
1283 matches,
1284 }
1285}
1286
1287fn is_statement_macro(call: &ast::MacroCall) -> bool {
1290 call.syntax().parent().is_some_and(|parent| {
1291 ast::MacroExpr::can_cast(parent.kind())
1292 && parent.parent().is_some_and(|grandparent| {
1293 ast::ExprStmt::can_cast(grandparent.kind())
1294 || ast::StmtList::can_cast(grandparent.kind())
1295 })
1296 })
1297}
1298
1299fn first_top_level_comma_end(arguments: &ast::TokenTree) -> Option<usize> {
1301 arguments
1302 .syntax()
1303 .children_with_tokens()
1304 .filter_map(|element| element.into_token())
1305 .find(|token| token.kind() == SyntaxKind::COMMA)
1306 .map(|token| usize::from(token.text_range().end()))
1307}
1308
1309fn rewrite_expression_macros(
1312 source: &str,
1313 tree: &SourceFile,
1314 edition: Edition,
1315 assertions: &mut Vec<TextRange>,
1316 assertion_calls: &mut Vec<TextRange>,
1317 matches: &mut Vec<TextRange>,
1318) -> Option<String> {
1319 let mut text = source.as_bytes().to_vec();
1320 let mut changed = false;
1321 for call in tree.syntax().descendants().filter_map(ast::MacroCall::cast) {
1322 let Some(name) = call
1323 .path()
1324 .and_then(|path| path.segment())
1325 .and_then(|segment| segment.name_ref())
1326 .map(|name| name.text().to_string())
1327 else {
1328 continue;
1329 };
1330 if !EXPRESSION_MACROS.contains(&name.as_str()) {
1331 continue;
1332 }
1333 let (Some(bang), Some(arguments)) = (call.excl_token(), call.token_tree()) else {
1334 continue;
1335 };
1336 let parenthesised = arguments.l_paren_token().is_some();
1337 if !parenthesised && arguments.l_brack_token().is_none() {
1338 continue;
1339 }
1340 let range = arguments.syntax().text_range();
1341 let (start, end) = (usize::from(range.start()), usize::from(range.end()));
1342 let mut rewritten = source.as_bytes()[start..end].to_vec();
1343 if !parenthesised {
1344 rewritten[0] = b'(';
1345 *rewritten
1346 .last_mut()
1347 .expect("a token tree has a closing delimiter") = b')';
1348 }
1349 if name == "matches" {
1350 let Some(comma_end) = first_top_level_comma_end(&arguments) else {
1353 continue;
1354 };
1355 for byte in &mut rewritten[comma_end - start..end - start - 1] {
1356 if *byte != b'\n' {
1357 *byte = b' ';
1358 }
1359 }
1360 }
1361 let probe = format!(
1363 "fn __supercov() {{ let _ = __f{}; }}",
1364 String::from_utf8_lossy(&rewritten)
1365 );
1366 if !SourceFile::parse(&probe, edition).errors().is_empty() {
1367 if !parenthesised && !is_statement_macro(&call) {
1371 let array = source.as_bytes()[start..end].to_vec();
1372 let array_probe = format!(
1373 "fn __supercov() {{ let _ = {}; }}",
1374 String::from_utf8_lossy(&array)
1375 );
1376 if SourceFile::parse(&array_probe, edition).errors().is_empty() {
1377 let prefix_start = usize::from(call.syntax().text_range().start());
1378 let prefix_start = call.attrs().last().map_or(prefix_start, |attribute| {
1379 usize::from(attribute.syntax().text_range().end())
1380 });
1381 for byte in &mut text[prefix_start..start] {
1382 if *byte != b'\n' {
1383 *byte = b' ';
1384 }
1385 }
1386 text[prefix_start] = b'&';
1391 changed = true;
1392 }
1393 }
1394 continue;
1395 }
1396 text[usize::from(bang.text_range().start())] = b'_';
1397 text[start..end].copy_from_slice(&rewritten);
1398 changed = true;
1399 if ASSERTION_MACROS.contains(&name.as_str()) {
1400 assertions.push(range);
1401 }
1402 if ASSERTION_STATEMENT_MACROS.contains(&name.as_str()) {
1403 assertion_calls.push(call.syntax().text_range());
1404 }
1405 if name == "matches" {
1406 matches.push(call.syntax().text_range());
1407 }
1408 }
1409 changed.then(|| String::from_utf8(text).expect("rewriting ASCII keeps the source UTF-8"))
1410}
1411
1412fn assertion_argument_count(root: &ra_ap_syntax::SyntaxNode, arguments: TextRange) -> usize {
1415 root.descendants()
1416 .find(|node| node.text_range() == arguments && ast::ArgList::can_cast(node.kind()))
1417 .and_then(ast::ArgList::cast)
1418 .map_or(0, |list| list.args().count())
1419}
1420
1421fn assertion_condition(root: &ra_ap_syntax::SyntaxNode, arguments: TextRange) -> Option<ast::Expr> {
1424 root.descendants()
1425 .find(|node| node.text_range() == arguments && ast::ArgList::can_cast(node.kind()))
1426 .and_then(ast::ArgList::cast)?
1427 .args()
1428 .next()
1429}
1430
1431struct ParsedSource {
1436 tree: SourceFile,
1437 assertions: Vec<TextRange>,
1438 assertion_calls: Vec<TextRange>,
1439 matches: Vec<TextRange>,
1440 edition: Edition,
1441}
1442
1443fn parse_for_instrumentation(source: &str) -> Result<ParsedSource, RustInstrumenterError> {
1444 if source.len() > u32::MAX as usize {
1445 return Err(RustInstrumenterError::SourceTooLarge);
1446 }
1447 let (tree, edition) = parse_any_edition(source).map_err(RustInstrumenterError::Parse)?;
1448 let view = expression_view(source, edition);
1449 let parsed_view = SourceFile::parse(&view.text, edition);
1450 if parsed_view.errors().is_empty() {
1451 Ok(ParsedSource {
1452 tree: parsed_view.tree(),
1453 assertions: view.assertions,
1454 assertion_calls: view.assertion_calls,
1455 matches: view.matches,
1456 edition,
1457 })
1458 } else {
1459 Ok(ParsedSource {
1460 tree,
1461 assertions: Vec::new(),
1462 assertion_calls: Vec::new(),
1463 matches: Vec::new(),
1464 edition,
1465 })
1466 }
1467}
1468
1469fn condition_expressions(
1472 root: &ra_ap_syntax::SyntaxNode,
1473 assertions: &[TextRange],
1474) -> Vec<ast::Expr> {
1475 let mut conditions = Vec::new();
1476 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
1477 conditions.extend(expression.condition());
1478 }
1479 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
1480 conditions.extend(expression.condition());
1481 }
1482 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
1483 conditions.extend(guard.condition());
1484 }
1485 for arguments in assertions {
1486 conditions.extend(assertion_condition(root, *arguments));
1487 }
1488 conditions
1489}
1490
1491fn is_logical(expression: &ast::Expr) -> bool {
1492 matches!(expression, ast::Expr::BinExpr(binary)
1493 if matches!(binary.op_kind(), Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))))
1494}
1495
1496fn value_decisions(root: &ra_ap_syntax::SyntaxNode, assertions: &[TextRange]) -> Vec<ast::Expr> {
1502 let conditions = condition_expressions(root, assertions)
1503 .iter()
1504 .map(|condition| unparenthesized(condition).syntax().text_range())
1505 .collect::<Vec<_>>();
1506 root.descendants()
1507 .filter_map(ast::Expr::cast)
1508 .filter(is_logical)
1509 .filter(|expression| {
1510 let mut parent = expression.syntax().parent();
1511 while let Some(node) = parent
1512 .clone()
1513 .filter(|node| ast::ParenExpr::can_cast(node.kind()))
1514 {
1515 parent = node.parent();
1516 }
1517 !parent
1518 .and_then(ast::Expr::cast)
1519 .is_some_and(|parent| is_logical(&parent))
1520 })
1521 .filter(|expression| !conditions.contains(&expression.syntax().text_range()))
1522 .collect()
1523}
1524
1525fn unparenthesized(expression: &ast::Expr) -> ast::Expr {
1526 match expression {
1527 ast::Expr::ParenExpr(paren) => paren
1528 .expr()
1529 .map_or_else(|| expression.clone(), |inner| unparenthesized(&inner)),
1530 _ => expression.clone(),
1531 }
1532}
1533
1534fn standalone_matches(
1538 root: &ra_ap_syntax::SyntaxNode,
1539 matches: &[TextRange],
1540 assertions: &[TextRange],
1541) -> Vec<ast::Expr> {
1542 let mut atoms = Vec::new();
1543 let mut conditions = condition_expressions(root, assertions);
1544 conditions.extend(value_decisions(root, assertions));
1545 for condition in &conditions {
1546 RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
1547 }
1548 matches
1549 .iter()
1550 .filter(|range| !atoms.contains(range))
1551 .filter_map(|range| {
1552 root.descendants()
1553 .find(|node| node.text_range() == *range && ast::CallExpr::can_cast(node.kind()))
1554 .and_then(ast::Expr::cast)
1555 })
1556 .collect()
1557}
1558
1559#[derive(Debug, Clone, Default, PartialEq)]
1565pub struct TargetCfg {
1566 names: BTreeSet<String>,
1567 pairs: BTreeSet<(String, String)>,
1568}
1569
1570impl TargetCfg {
1571 pub fn new(values: &[cargo_platform::Cfg]) -> Self {
1572 let mut cfg = Self::default();
1573 for value in values {
1574 match value {
1575 cargo_platform::Cfg::Name(name) => {
1576 cfg.names.insert(name.name.clone());
1577 }
1578 cargo_platform::Cfg::KeyPair(key, value) => {
1579 cfg.pairs.insert((key.name.clone(), value.clone()));
1580 }
1581 }
1582 }
1583 cfg
1584 }
1585
1586 fn decides(key: &str) -> bool {
1588 matches!(key, "unix" | "windows" | "panic") || key.starts_with("target_")
1589 }
1590
1591 fn evaluate(&self, expression: &cargo_platform::CfgExpr) -> Option<bool> {
1592 use cargo_platform::{Cfg, CfgExpr};
1593 match expression {
1594 CfgExpr::True => Some(true),
1595 CfgExpr::False => Some(false),
1596 CfgExpr::Not(inner) => self.evaluate(inner).map(|value| !value),
1597 CfgExpr::All(items) => {
1598 let values = items
1599 .iter()
1600 .map(|item| self.evaluate(item))
1601 .collect::<Vec<_>>();
1602 if values.contains(&Some(false)) {
1603 Some(false)
1604 } else {
1605 values.iter().all(Option::is_some).then_some(true)
1606 }
1607 }
1608 CfgExpr::Any(items) => {
1609 let values = items
1610 .iter()
1611 .map(|item| self.evaluate(item))
1612 .collect::<Vec<_>>();
1613 if values.contains(&Some(true)) {
1614 Some(true)
1615 } else {
1616 values.iter().all(Option::is_some).then_some(false)
1617 }
1618 }
1619 CfgExpr::Value(Cfg::Name(name)) => {
1620 Self::decides(&name.name).then(|| self.names.contains(&name.name))
1621 }
1622 CfgExpr::Value(Cfg::KeyPair(key, value)) => Self::decides(&key.name)
1623 .then(|| self.pairs.contains(&(key.name.clone(), value.clone()))),
1624 }
1625 }
1626
1627 fn compiles_out(&self, node: &ra_ap_syntax::SyntaxNode) -> bool {
1629 cfg_predicate(node).is_some_and(|expression| self.evaluate(&expression) == Some(false))
1630 }
1631}
1632
1633fn cfg_predicate(node: &ra_ap_syntax::SyntaxNode) -> Option<cargo_platform::CfgExpr> {
1637 let attributes = ast::AnyHasAttrs::cast(node.clone())?;
1638 let mut predicates = attributes
1639 .attrs()
1640 .filter_map(|attribute| attribute.meta())
1641 .filter_map(|meta| {
1642 let text = meta.syntax().text().to_string();
1644 text.trim()
1645 .strip_prefix("cfg")
1646 .map(str::trim_start)
1647 .and_then(|rest| rest.strip_prefix('('))
1648 .and_then(|inner| inner.strip_suffix(')'))
1649 .and_then(|inner| inner.parse::<cargo_platform::CfgExpr>().ok())
1650 })
1651 .collect::<Vec<_>>();
1652 match predicates.len() {
1653 0 => None,
1654 1 => predicates.pop(),
1655 _ => Some(cargo_platform::CfgExpr::All(predicates)),
1656 }
1657}
1658
1659fn unsettled_cfg_gates(
1662 root: &ra_ap_syntax::SyntaxNode,
1663 cfg: Option<&TargetCfg>,
1664) -> Vec<(TextRange, cargo_platform::CfgExpr)> {
1665 root.descendants()
1666 .filter_map(|node| {
1667 let predicate = cfg_predicate(&node)?;
1668 if cfg.is_some_and(|cfg| cfg.evaluate(&predicate).is_some()) {
1669 return None;
1670 }
1671 Some((node.text_range(), predicate))
1672 })
1673 .collect()
1674}
1675
1676fn item_attributes(node: &ra_ap_syntax::SyntaxNode) -> Vec<String> {
1679 let attributes = if let Some(item) = ast::Item::cast(node.clone()) {
1680 item.attrs().collect::<Vec<_>>()
1681 } else if let Some(item) = ast::AssocItem::cast(node.clone()) {
1682 item.attrs().collect::<Vec<_>>()
1683 } else {
1684 return Vec::new();
1685 };
1686 attributes
1687 .iter()
1688 .filter_map(|attribute| attribute.meta())
1689 .map(|meta| {
1690 meta.syntax()
1691 .text()
1692 .to_string()
1693 .chars()
1694 .filter(|character| !character.is_whitespace())
1695 .collect()
1696 })
1697 .collect()
1698}
1699
1700pub fn test_code_path(file: &str) -> bool {
1704 let mut parts = file.split(['/', '\\']).collect::<Vec<_>>();
1705 let name = parts.pop().unwrap_or_default();
1706 parts
1707 .iter()
1708 .any(|directory| matches!(*directory, "tests" | "examples" | "benches"))
1709 || name == "tests.rs"
1710 || name
1711 .strip_suffix("tests.rs")
1712 .and_then(|stem| stem.strip_suffix(['_', '-']))
1713 .is_some_and(|stem| {
1714 !stem.is_empty()
1715 && stem.chars().all(|character| {
1716 character.is_ascii_alphanumeric() || "_-".contains(character)
1717 })
1718 })
1719}
1720
1721pub(crate) fn test_item(node: &ra_ap_syntax::SyntaxNode) -> bool {
1724 item_attributes(node).iter().any(|text| {
1725 let path = text
1726 .chars()
1727 .take_while(|character| character.is_alphanumeric() || "_:".contains(*character))
1728 .collect::<String>();
1729 matches!(path.rsplit("::").next(), Some("test" | "bench"))
1730 || text == "cfg(test)"
1731 || text.starts_with("cfg(all(test,")
1732 || text.starts_with("cfg(all(test)")
1733 })
1734}
1735
1736fn compiled_out_ranges(root: &ra_ap_syntax::SyntaxNode, cfg: Option<&TargetCfg>) -> Vec<TextRange> {
1738 let Some(cfg) = cfg else {
1739 return Vec::new();
1740 };
1741 root.descendants()
1742 .filter(|node| cfg.compiles_out(node))
1743 .map(|node| node.text_range())
1744 .collect()
1745}
1746
1747fn test_code_ranges(root: &ra_ap_syntax::SyntaxNode, test_file: bool) -> Vec<TextRange> {
1749 if test_file {
1750 return vec![root.text_range()];
1751 }
1752 root.descendants()
1753 .filter(test_item)
1754 .map(|node| node.text_range())
1755 .collect()
1756}
1757
1758fn in_test_code(tests: &[TextRange], range: TextRange) -> bool {
1759 tests.iter().any(|test| test.contains_range(range))
1760}
1761
1762pub fn build_rust_manifest(
1763 file: &str,
1764 source: &str,
1765) -> Result<CoverageManifest, RustInstrumenterError> {
1766 let parsed = parse_for_instrumentation(source)?;
1767 let tests = test_code_ranges(parsed.tree.syntax(), test_code_path(file));
1768 RustObligationCollector::new(file, source)
1769 .leaving_out(tests, Vec::new())
1770 .collect(&parsed.tree, &parsed.assertions, &parsed.matches)
1771}
1772
1773fn before_its_line(root: &ra_ap_syntax::SyntaxNode, source: &str, offset: usize) -> usize {
1784 let line = source[..offset].rfind('\n').map_or(0, |at| at + 1);
1785 if line == 0 || !source[line..offset].chars().all(char::is_whitespace) {
1786 return offset;
1787 }
1788 let Ok(at) = u32::try_from(offset) else {
1789 return offset;
1790 };
1791 let mut before = root
1792 .token_at_offset(ra_ap_syntax::TextSize::from(at))
1793 .right_biased()
1794 .and_then(|token| token.prev_token());
1795 while let Some(token) = &before {
1796 if !token.kind().is_trivia() {
1797 return usize::from(token.text_range().end());
1798 }
1799 before = token.prev_token();
1800 }
1801 offset
1802}
1803
1804fn block_entry_offset(block: &ast::BlockExpr) -> Option<usize> {
1805 let list = block.stmt_list()?;
1806 list.attrs()
1807 .last()
1808 .map(|attribute| usize::from(attribute.syntax().text_range().end()))
1809 .or_else(|| {
1810 list.l_curly_token()
1811 .map(|token| usize::from(token.text_range().end()))
1812 })
1813}
1814
1815fn item_range(node: &ra_ap_syntax::SyntaxNode) -> TextRange {
1820 let range = node.text_range();
1821 node.children_with_tokens()
1822 .find(|element| match element {
1823 ra_ap_syntax::NodeOrToken::Token(token) => !token.kind().is_trivia(),
1824 ra_ap_syntax::NodeOrToken::Node(child) => !ast::Attr::can_cast(child.kind()),
1825 })
1826 .map_or(range, |element| {
1827 TextRange::new(element.text_range().start(), range.end())
1828 })
1829}
1830
1831fn range_after_attributes(node: &impl HasAttrs) -> TextRange {
1832 let range = node.syntax().text_range();
1833 node.attrs().last().map_or(range, |attribute| {
1834 TextRange::new(attribute.syntax().text_range().end(), range.end())
1835 })
1836}
1837
1838fn has_let(expression: &ast::Expr) -> bool {
1839 expression
1840 .syntax()
1841 .descendants()
1842 .any(|node| ast::LetExpr::can_cast(node.kind()))
1843}
1844
1845enum ChainHost<'a> {
1847 If(&'a ast::IfExpr),
1848 While(&'a ast::WhileExpr),
1849}
1850
1851fn allocate_chain_table_name(
1853 file: &str,
1854 condition: &ast::Expr,
1855 identifiers: &mut BTreeSet<String>,
1856) -> String {
1857 let id = stable_id(file, "chain", condition.syntax().text_range(), "operators");
1858 let suffix = id
1859 .rsplit(':')
1860 .next()
1861 .unwrap_or("chain")
1862 .to_ascii_uppercase();
1863 let base = format!("__SUPERCOV_CHAIN_{suffix}");
1864 let mut candidate = base.clone();
1865 let mut attempt = 0_usize;
1866 while !identifiers.insert(candidate.clone()) {
1867 attempt += 1;
1868 candidate = format!("{base}_{attempt}");
1869 }
1870 candidate
1871}
1872
1873fn allocate_identifier(
1875 file: &str,
1876 range: TextRange,
1877 kind: &str,
1878 identifiers: &mut BTreeSet<String>,
1879) -> String {
1880 let id = stable_id(file, kind, range, "");
1881 let suffix = id.rsplit(':').next().unwrap_or(kind);
1882 let base = format!("__supercov_{kind}_{suffix}");
1883 let mut candidate = base.clone();
1884 let mut attempt = 0_usize;
1885 while !identifiers.insert(candidate.clone()) {
1886 attempt += 1;
1887 candidate = format!("{base}_{attempt}");
1888 }
1889 candidate
1890}
1891
1892fn own_breaks(body: &ast::BlockExpr, label: Option<ast::Label>) -> Vec<TextRange> {
1896 let own_label = label
1897 .and_then(|label| label.lifetime())
1898 .map(|lifetime| lifetime.text().to_string());
1899 body.syntax()
1900 .descendants()
1901 .filter_map(ast::BreakExpr::cast)
1902 .filter(|expression| match expression.lifetime() {
1903 Some(lifetime) => own_label.as_deref() == Some(lifetime.text().to_string().as_str()),
1904 None => !expression
1905 .syntax()
1906 .ancestors()
1907 .skip(1)
1908 .take_while(|ancestor| ancestor != body.syntax())
1909 .any(|ancestor| {
1910 ast::LoopExpr::can_cast(ancestor.kind())
1911 || ast::WhileExpr::can_cast(ancestor.kind())
1912 || ast::ForExpr::can_cast(ancestor.kind())
1913 || ast::ClosureExpr::can_cast(ancestor.kind())
1914 }),
1915 })
1916 .map(|expression| expression.syntax().text_range())
1917 .collect()
1918}
1919
1920fn instrument_let_chain(
1936 insertions: &mut Vec<Insertion>,
1937 runtime_path: &str,
1938 file: &str,
1939 condition: &ast::Expr,
1940 host: ChainHost<'_>,
1941 identifiers: &mut BTreeSet<String>,
1942) {
1943 let (kind, host_range, body, label) = match &host {
1946 ChainHost::If(expression) => (
1947 "if",
1948 range_after_attributes(*expression),
1949 expression.then_branch(),
1950 None,
1951 ),
1952 ChainHost::While(expression) => (
1953 "while",
1954 range_after_attributes(*expression),
1955 expression.loop_body(),
1956 expression.label(),
1957 ),
1958 };
1959 let Some(body) = body else {
1960 return;
1961 };
1962 let Some(body_offset) = block_entry_offset(&body) else {
1963 return;
1964 };
1965 let range = condition.syntax().text_range();
1966 let id = stable_id(file, "decision", range, kind);
1967 let mut atoms = Vec::new();
1968 RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
1969 let lets = condition
1970 .syntax()
1971 .descendants()
1972 .filter_map(ast::LetExpr::cast)
1973 .map(|expression| expression.syntax().text_range())
1974 .collect::<Vec<_>>();
1975 let frame = allocate_frame_name(file, condition, kind, identifiers);
1976 let table = allocate_chain_table_name(file, condition, identifiers);
1977 let single_let = atoms.len() == 1;
1978 let broke = match &host {
1979 ChainHost::While(_) if single_let => {
1980 Some(allocate_identifier(file, range, "broke", identifiers))
1981 }
1982 _ => None,
1983 };
1984
1985 let mut operators = Vec::new();
1986 for binary in condition
1987 .syntax()
1988 .descendants()
1989 .filter_map(ast::BinExpr::cast)
1990 {
1991 if !matches!(binary.op_kind(), Some(BinaryOp::LogicOp(LogicOp::And))) {
1994 continue;
1995 }
1996 let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
1997 continue;
1998 };
1999 if !has_let(&left) {
2000 continue;
2001 }
2002 let branch = stable_id(file, "branch", right.syntax().text_range(), "logical-and");
2003 let right_range = right.syntax().text_range();
2004 let Some(first) = atoms
2005 .iter()
2006 .position(|atom| right_range.contains_range(*atom))
2007 else {
2008 continue;
2009 };
2010 operators.push(format!(
2011 "({first}, {}, {})",
2012 hit_ordinal(&format!("{branch}:short-circuit")),
2013 hit_ordinal(&format!("{branch}:evaluated"))
2014 ));
2015 }
2016
2017 let mark = format!("{runtime_path}::reached(&mut {frame}, 0);");
2018 let record_false = format!("{runtime_path}::decision_chain(&mut {frame}, false, {table});");
2019 let mut prefix = format!(
2020 "{{ const {table}: &[(usize, u32, u32)] = &[{}]; let mut {frame} = {runtime_path}::DecisionFrame::new({}, {}); ",
2021 operators.join(", "),
2022 decision_ordinal(&id),
2023 atoms.len()
2024 );
2025 if single_let {
2026 prefix.push_str(&mark);
2027 prefix.push(' ');
2028 }
2029 if let Some(broke) = &broke {
2030 prefix.push_str(&format!("let mut {broke} = false; "));
2031 }
2032 let suffix = match &host {
2033 ChainHost::If(expression) => match expression.else_branch() {
2034 Some(_) => " }".to_owned(),
2035 None => format!(" else {{ {record_false} }} }}"),
2036 },
2037 ChainHost::While(_) => match &broke {
2038 Some(broke) => format!(" if !{broke} {{ {mark} {record_false} }} }}"),
2039 None => format!(" {record_false} }}"),
2040 },
2041 };
2042 push_wrapper(insertions, host_range, host_range, 1, prefix, suffix);
2043 if !single_let {
2044 push_direct(
2045 insertions,
2046 usize::from(range.start()),
2047 format!("{runtime_path}::reached(&mut {frame}, 0) && "),
2048 );
2049 }
2050 for (index, atom) in atoms.iter().enumerate() {
2051 if lets.contains(atom) {
2052 if index > 0 {
2053 push_direct(
2054 insertions,
2055 usize::from(atom.start()),
2056 format!("{runtime_path}::reached(&mut {frame}, {index}) && "),
2057 );
2058 }
2059 } else {
2060 push_wrapper(
2061 insertions,
2062 *atom,
2063 *atom,
2064 1,
2065 format!("{runtime_path}::condition(("),
2066 format!("), &mut {frame}, {index})"),
2067 );
2068 }
2069 }
2070 let mut entry = String::new();
2071 if broke.is_some() {
2072 entry.push_str(&format!(" {mark}"));
2073 }
2074 entry.push_str(&format!(
2075 " {runtime_path}::decision_chain(&mut {frame}, true, {table});"
2076 ));
2077 push_direct(insertions, body_offset, entry);
2078 if let Some(broke) = &broke {
2079 for break_range in own_breaks(&body, label) {
2080 push_wrapper(
2081 insertions,
2082 break_range,
2083 break_range,
2084 0,
2085 format!("{{ {broke} = true; "),
2086 " }".into(),
2087 );
2088 }
2089 }
2090 if let ChainHost::If(expression) = &host {
2091 match expression.else_branch() {
2092 Some(ast::ElseBranch::Block(block)) => {
2093 if let Some(offset) = block_entry_offset(&block) {
2094 push_direct(insertions, offset, format!(" {record_false}"));
2095 }
2096 }
2097 Some(ast::ElseBranch::IfExpr(nested)) => {
2098 let nested_range = nested.syntax().text_range();
2099 push_wrapper(
2100 insertions,
2101 nested_range,
2102 nested_range,
2103 0,
2104 format!("{{ {record_false} "),
2105 " }".into(),
2106 );
2107 }
2108 None => {}
2109 }
2110 }
2111}
2112
2113fn enclosing_block_entry(node: &ra_ap_syntax::SyntaxNode) -> Option<usize> {
2117 node.ancestors()
2118 .skip(1)
2119 .find_map(ast::BlockExpr::cast)
2120 .and_then(|block| block_entry_offset(&block))
2121}
2122
2123fn plain_block(block: &ast::BlockExpr) -> bool {
2127 block
2128 .syntax()
2129 .first_token()
2130 .is_some_and(|token| token.kind() == SyntaxKind::L_CURLY)
2131}
2132
2133fn instrument_decision(
2134 insertions: &mut Vec<Insertion>,
2135 runtime_path: &str,
2136 file: &str,
2137 condition: &ast::Expr,
2138 kind: &str,
2139 frame_name: &str,
2140) -> bool {
2141 if cannot_carry_probe(condition.syntax())
2142 || condition
2143 .syntax()
2144 .descendants()
2145 .any(|node| ast::LetExpr::can_cast(node.kind()))
2146 {
2147 return false;
2148 }
2149 let range = condition.syntax().text_range();
2150 let id = stable_id(file, "decision", range, kind);
2151 let mut condition_ranges = Vec::new();
2152 RustObligationCollector::atomic_condition_ranges(condition, &mut condition_ranges);
2153 push_wrapper(
2154 insertions,
2155 range,
2156 range,
2157 0,
2158 format!(
2159 "({{ let mut {frame_name} = {runtime_path}::DecisionFrame::new({}, {}); {runtime_path}::decision((",
2160 decision_ordinal(&id),
2161 condition_ranges.len()
2162 ),
2163 format!("), &mut {frame_name}) }})"),
2164 );
2165 for (index, atomic_range) in condition_ranges.into_iter().enumerate() {
2169 push_wrapper(
2170 insertions,
2171 atomic_range,
2172 atomic_range,
2173 1,
2174 format!("{runtime_path}::condition(("),
2175 format!("), &mut {frame_name}, {index})"),
2176 );
2177 }
2178 true
2179}
2180
2181pub fn instrument_rust_source(
2191 file: &str,
2192 source: &str,
2193 runtime_path: &str,
2194) -> Result<RustInstrumentedSource, RustInstrumenterError> {
2195 instrument_rust_file(file, source, runtime_path, test_code_path(file), None)
2196}
2197
2198pub fn instrument_rust_file(
2203 file: &str,
2204 source: &str,
2205 runtime_path: &str,
2206 test_file: bool,
2207 cfg: Option<&TargetCfg>,
2208) -> Result<RustInstrumentedSource, RustInstrumenterError> {
2209 let mut layout = RustProbeLayout::default();
2210 let mut transformed =
2211 instrument_rust_file_numbered(file, source, runtime_path, test_file, cfg, &mut layout)?;
2212 transformed.probes = layout;
2213 Ok(transformed)
2214}
2215
2216pub fn instrument_rust_file_numbered(
2220 file: &str,
2221 source: &str,
2222 runtime_path: &str,
2223 test_file: bool,
2224 cfg: Option<&TargetCfg>,
2225 layout: &mut RustProbeLayout,
2226) -> Result<RustInstrumentedSource, RustInstrumenterError> {
2227 let _numbering = NumberingScope::enter(layout);
2228 if !valid_runtime_path(runtime_path) {
2229 return Err(RustInstrumenterError::InvalidRuntimePath);
2230 }
2231 let parsed = parse_for_instrumentation(source)?;
2237 let tests = test_code_ranges(parsed.tree.syntax(), test_file);
2238 let compiled_out = compiled_out_ranges(parsed.tree.syntax(), cfg);
2239 let gates = unsettled_cfg_gates(parsed.tree.syntax(), cfg)
2242 .into_iter()
2243 .filter(|(range, _)| !in_test_code(&tests, *range) && !in_test_code(&compiled_out, *range))
2244 .collect::<Vec<_>>();
2245 let (mut manifest, gated) = RustObligationCollector::new(file, source)
2246 .leaving_out(tests.clone(), compiled_out.clone())
2247 .gating(gates.iter().map(|(range, _)| *range).collect())
2248 .collect_gated(&parsed.tree, &parsed.assertions, &parsed.matches)?;
2249 for ((range, predicate), obligations) in gates.iter().zip(gated) {
2250 if obligations.is_empty() {
2251 continue;
2252 }
2253 let line_of = |offset: ra_ap_syntax::TextSize| {
2254 source[..usize::from(offset)].matches('\n').count() + 1
2255 };
2256 cfg_gate_ordinal(RustCfgGate {
2257 file: file.to_owned(),
2258 predicate: predicate.to_string(),
2259 line: line_of(range.start()),
2260 end_line: line_of(range.end()),
2261 obligations,
2262 });
2263 }
2264 let ParsedSource {
2265 tree,
2266 assertions,
2267 assertion_calls,
2268 matches,
2269 edition,
2270 } = parsed;
2271 let root = tree.syntax();
2272 let mut insertions = Vec::new();
2273 let mut identifiers = root
2274 .descendants_with_tokens()
2275 .filter_map(|element| element.into_token())
2276 .filter(|token| token.kind() == SyntaxKind::IDENT)
2277 .map(|token| token.text().to_string())
2278 .collect::<BTreeSet<_>>();
2279
2280 let mut skipped_attributed_statement = false;
2281 let attributed_probe = |insertions: &mut Vec<Insertion>,
2292 skipped: &mut bool,
2293 expression: Option<ast::Expr>,
2294 has_attrs: bool,
2295 statement: bool,
2299 trailing: bool,
2300 range: TextRange,
2301 id: String| {
2302 if !has_attrs {
2303 push_direct(
2304 insertions,
2305 before_its_line(root, source, usize::from(range.start())),
2306 format!("{runtime_path}::hit({});", hit_ordinal(&id)),
2307 );
2308 return;
2309 }
2310 let Some(expression) = expression else {
2311 *skipped = true;
2312 return;
2313 };
2314 if let ast::Expr::BlockExpr(block) = &expression
2315 && let Some(offset) = block_entry_offset(block)
2316 {
2317 push_direct(
2318 insertions,
2319 offset,
2320 format!("{runtime_path}::hit({});", hit_ordinal(&id)),
2321 );
2322 return;
2323 }
2324 if trailing && let ast::Expr::MacroExpr(_) = &expression {
2335 if expression.attrs().any(|attribute| {
2336 attribute.meta().is_some_and(|meta| {
2339 let text = meta.syntax().text().to_string();
2340 let name = text
2341 .chars()
2342 .take_while(|character| character.is_alphanumeric() || *character == '_')
2343 .collect::<String>();
2344 matches!(name.as_str(), "cfg" | "cfg_attr")
2345 })
2346 }) {
2347 *skipped = true;
2348 return;
2349 }
2350 let range = expression.syntax().text_range();
2351 push_wrapper(
2352 insertions,
2353 range,
2354 range,
2355 0,
2356 format!("{{ {runtime_path}::hit({}); ", hit_ordinal(&id)),
2357 " }".into(),
2358 );
2359 return;
2360 }
2361 if statement && let ast::Expr::MacroExpr(_) = &expression {
2368 let start = expression.attrs().last().map_or_else(
2369 || expression.syntax().text_range().start(),
2370 |attribute| attribute.syntax().text_range().end(),
2371 );
2372 let wrapped = TextRange::new(start, expression.syntax().text_range().end());
2373 push_wrapper(
2374 insertions,
2375 wrapped,
2376 wrapped,
2377 0,
2378 format!(" {{ {runtime_path}::hit({}); ", hit_ordinal(&id)),
2379 "; }".into(),
2380 );
2381 return;
2382 }
2383 let start = expression.attrs().last().map_or_else(
2389 || expression.syntax().text_range().start(),
2390 |attribute| attribute.syntax().text_range().end(),
2391 );
2392 let wrapped = TextRange::new(start, expression.syntax().text_range().end());
2393 push_wrapper(
2394 insertions,
2395 wrapped,
2396 wrapped,
2397 0,
2398 format!(" {{ {runtime_path}::hit({}); (", hit_ordinal(&id)),
2399 ") }".into(),
2400 );
2401 };
2402 for list in root.descendants().filter_map(ast::StmtList::cast) {
2403 let last_statement = list.statements().last();
2404 for statement in list.statements() {
2405 let (range, expression, has_attrs, statement_expression, trailing) = match &statement {
2406 ast::Stmt::ExprStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
2407 let expression = statement.expr();
2408 let has_attrs = expression
2411 .as_ref()
2412 .is_some_and(|expression| expression.attrs().next().is_some());
2413 let trailing = statement.semicolon_token().is_none()
2416 && list.tail_expr().is_none()
2417 && last_statement.as_ref() == Some(&ast::Stmt::ExprStmt(statement.clone()));
2418 (
2419 statement.syntax().text_range(),
2420 expression,
2421 has_attrs,
2422 true,
2423 trailing,
2424 )
2425 }
2426 ast::Stmt::LetStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
2427 let has_attrs = statement.attrs().next().is_some();
2428 let initializer = has_attrs.then(|| statement.initializer()).flatten();
2431 (
2432 statement.syntax().text_range(),
2433 initializer,
2434 has_attrs,
2435 false,
2436 false,
2437 )
2438 }
2439 _ => continue,
2440 };
2441 let id = stable_id(file, "statement", range, "");
2442 if !has_attrs
2449 && !trailing
2450 && assertion_calls
2451 .iter()
2452 .any(|call| range.contains_range(*call))
2453 {
2454 push_direct(
2455 &mut insertions,
2456 usize::from(range.end()),
2457 format!("{runtime_path}::assertion({});", assertion_ordinal(&id)),
2458 );
2459 }
2460 attributed_probe(
2461 &mut insertions,
2462 &mut skipped_attributed_statement,
2463 expression,
2464 has_attrs,
2465 statement_expression,
2466 trailing,
2467 range,
2468 id,
2469 );
2470 }
2471 if let Some(tail) = list
2472 .tail_expr()
2473 .filter(|tail| !cannot_carry_probe(tail.syntax()))
2474 {
2475 let range = tail.syntax().text_range();
2476 let id = stable_id(file, "statement", range, "");
2477 let has_attrs = tail.attrs().next().is_some();
2478 attributed_probe(
2479 &mut insertions,
2480 &mut skipped_attributed_statement,
2481 Some(tail),
2482 has_attrs,
2483 true,
2484 true,
2485 range,
2486 id,
2487 );
2488 }
2489 }
2490
2491 for function in root.descendants().filter_map(ast::Fn::cast) {
2492 if cannot_carry_probe(function.syntax()) {
2495 continue;
2496 }
2497 let Some(body) = function.body() else {
2498 continue;
2499 };
2500 let label = function.name().map(|name| name.text().to_string());
2501 let id = stable_id(
2502 file,
2503 "function",
2504 function.syntax().text_range(),
2505 label.as_deref().unwrap_or(""),
2506 );
2507 if let Some(offset) = block_entry_offset(&body) {
2508 push_direct(
2509 &mut insertions,
2510 offset,
2511 format!("{runtime_path}::hit({});", hit_ordinal(&id)),
2512 );
2513 }
2514 }
2515
2516 for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
2517 let Some(body) = closure.body() else {
2518 continue;
2519 };
2520 if cannot_carry_probe(body.syntax()) {
2521 continue;
2522 }
2523 let id = stable_id(file, "function", closure.syntax().text_range(), "<closure>");
2524 if let ast::Expr::BlockExpr(block) = &body {
2525 if let Some(offset) = block_entry_offset(block) {
2526 push_direct(
2527 &mut insertions,
2528 offset,
2529 format!("{runtime_path}::hit({});", hit_ordinal(&id)),
2530 );
2531 }
2532 } else {
2533 let range = body.syntax().text_range();
2534 push_wrapper(
2535 &mut insertions,
2536 range,
2537 closure.syntax().text_range(),
2538 0,
2539 format!("{{ {runtime_path}::hit({}); (", hit_ordinal(&id)),
2540 ") }".into(),
2541 );
2542 }
2543 }
2544
2545 for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
2552 if cannot_carry_probe(expression.syntax()) {
2553 continue;
2554 }
2555 let Some(list) = expression.match_arm_list() else {
2556 continue;
2557 };
2558 let arms = list.arms().collect::<Vec<_>>();
2559 if arms.is_empty() {
2560 continue;
2561 }
2562 let Some(table_offset) = enclosing_block_entry(expression.syntax()) else {
2563 continue;
2564 };
2565 let table = allocate_table_name(file, &expression, &mut identifiers);
2566 let entries = arms
2567 .iter()
2568 .map(|arm| {
2569 let id = stable_id(file, "branch", arm.syntax().text_range(), "match-arm");
2570 format!(
2571 "{}, {}",
2572 hit_ordinal(&format!("{id}:missed")),
2573 hit_ordinal(&format!("{id}:selected"))
2574 )
2575 })
2576 .collect::<Vec<_>>()
2577 .join(", ");
2578 push_direct(
2579 &mut insertions,
2580 table_offset,
2581 format!(" const {table}: &[u32] = &[{entries}];"),
2585 );
2586 for (index, arm) in arms.iter().enumerate() {
2587 let Some(body) = arm.expr() else {
2588 continue;
2589 };
2590 let call = format!("{runtime_path}::arms({table}, {index});");
2591 match &body {
2592 ast::Expr::BlockExpr(block) if plain_block(block) => {
2593 if let Some(offset) = block_entry_offset(block) {
2594 push_direct(&mut insertions, offset, format!(" {call}"));
2595 }
2596 }
2597 _ => push_wrapper(
2598 &mut insertions,
2599 body.syntax().text_range(),
2600 arm.syntax().text_range(),
2601 0,
2602 format!("{{ {call} ("),
2603 ") }".into(),
2604 ),
2605 }
2606 }
2607 }
2608
2609 for binary in root.descendants().filter_map(ast::BinExpr::cast) {
2613 let short_circuits_when = match binary.op_kind() {
2614 Some(BinaryOp::LogicOp(LogicOp::And)) => false,
2615 Some(BinaryOp::LogicOp(LogicOp::Or)) => true,
2616 _ => continue,
2617 };
2618 if cannot_carry_probe(binary.syntax()) {
2619 continue;
2620 }
2621 let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
2622 continue;
2623 };
2624 if has_let(&left) {
2628 continue;
2629 }
2630 let kind = if short_circuits_when {
2631 "logical-or"
2632 } else {
2633 "logical-and"
2634 };
2635 let id = stable_id(file, "branch", right.syntax().text_range(), kind);
2636 push_wrapper(
2637 &mut insertions,
2638 left.syntax().text_range(),
2639 binary.syntax().text_range(),
2640 2,
2641 format!("{runtime_path}::logical(("),
2642 format!(
2643 "), {short_circuits_when}, {}, {})",
2644 hit_ordinal(&format!("{id}:short-circuit")),
2645 hit_ordinal(&format!("{id}:evaluated"))
2646 ),
2647 );
2648 }
2649
2650 for expression in root.descendants().filter_map(ast::ForExpr::cast) {
2654 if cannot_carry_probe(expression.syntax()) {
2655 continue;
2656 }
2657 let Some(iterable) = expression.iterable() else {
2658 continue;
2659 };
2660 let id = stable_id(file, "branch", expression.syntax().text_range(), "for-loop");
2661 push_wrapper(
2665 &mut insertions,
2666 iterable.syntax().text_range(),
2667 iterable.syntax().text_range(),
2668 0,
2669 format!("{runtime_path}::for_loop(("),
2670 format!(
2671 "), {}, {})",
2672 hit_ordinal(&format!("{id}:zero")),
2673 hit_ordinal(&format!("{id}:entered"))
2674 ),
2675 );
2676 }
2677
2678 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
2682 if cannot_carry_probe(expression.syntax()) {
2683 continue;
2684 }
2685 let Some(offset) = expression.loop_body().as_ref().and_then(block_entry_offset) else {
2686 continue;
2687 };
2688 let id = stable_id(
2689 file,
2690 "branch",
2691 expression.syntax().text_range(),
2692 "while-loop",
2693 );
2694 let flag = allocate_flag_name(file, &expression, &mut identifiers);
2695 let range = range_after_attributes(&expression);
2696 push_wrapper(
2697 &mut insertions,
2698 range,
2699 range,
2700 0,
2701 format!("{{ let mut {flag} = true; "),
2702 format!(
2703 " {runtime_path}::zero_iterations({flag}, {}) }}",
2704 hit_ordinal(&format!("{id}:zero"))
2705 ),
2706 );
2707 push_direct(
2708 &mut insertions,
2709 offset,
2710 format!(
2711 " {runtime_path}::entered(&mut {flag}, {});",
2712 hit_ordinal(&format!("{id}:entered"))
2713 ),
2714 );
2715 }
2716
2717 for expression in root.descendants().filter_map(ast::TryExpr::cast) {
2721 if cannot_carry_probe(expression.syntax()) {
2722 continue;
2723 }
2724 let Some(operand) = expression.expr() else {
2725 continue;
2726 };
2727 let id = stable_id(
2728 file,
2729 "branch",
2730 expression.syntax().text_range(),
2731 "try-operator",
2732 );
2733 push_wrapper(
2736 &mut insertions,
2737 operand.syntax().text_range(),
2738 operand.syntax().text_range(),
2739 0,
2740 format!("{runtime_path}::TryProbe::probe(("),
2741 format!(
2742 "), {}, {})",
2743 hit_ordinal(&format!("{id}:continued")),
2744 hit_ordinal(&format!("{id}:returned"))
2745 ),
2746 );
2747 }
2748
2749 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
2750 let Some(condition) = expression.condition() else {
2751 continue;
2752 };
2753 if has_let(&condition) {
2754 if !cannot_carry_probe(condition.syntax()) {
2755 instrument_let_chain(
2756 &mut insertions,
2757 runtime_path,
2758 file,
2759 &condition,
2760 ChainHost::If(&expression),
2761 &mut identifiers,
2762 );
2763 }
2764 continue;
2765 }
2766 let frame_name = allocate_frame_name(file, &condition, "if", &mut identifiers);
2767 instrument_decision(
2768 &mut insertions,
2769 runtime_path,
2770 file,
2771 &condition,
2772 "if",
2773 &frame_name,
2774 );
2775 }
2776 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
2777 let Some(condition) = expression.condition() else {
2778 continue;
2779 };
2780 if has_let(&condition) {
2781 if !cannot_carry_probe(condition.syntax()) {
2782 instrument_let_chain(
2783 &mut insertions,
2784 runtime_path,
2785 file,
2786 &condition,
2787 ChainHost::While(&expression),
2788 &mut identifiers,
2789 );
2790 }
2791 continue;
2792 }
2793 let frame_name = allocate_frame_name(file, &condition, "while", &mut identifiers);
2794 instrument_decision(
2795 &mut insertions,
2796 runtime_path,
2797 file,
2798 &condition,
2799 "while",
2800 &frame_name,
2801 );
2802 }
2803 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
2804 if let Some(condition) = guard.condition() {
2805 let frame_name = allocate_frame_name(file, &condition, "match-guard", &mut identifiers);
2806 instrument_decision(
2807 &mut insertions,
2808 runtime_path,
2809 file,
2810 &condition,
2811 "match-guard",
2812 &frame_name,
2813 );
2814 }
2815 }
2816 for arguments in &assertions {
2823 let Some(condition) = assertion_condition(root, *arguments) else {
2824 continue;
2825 };
2826 let frame_name = allocate_frame_name(file, &condition, "assert", &mut identifiers);
2827 if !instrument_decision(
2828 &mut insertions,
2829 runtime_path,
2830 file,
2831 &condition,
2832 "assert",
2833 &frame_name,
2834 ) {
2835 continue;
2836 }
2837 if assertion_argument_count(root, *arguments) == 1 {
2838 let range = condition.syntax().text_range();
2839 let original = &source[usize::from(range.start())..usize::from(range.end())];
2840 push_direct(
2841 &mut insertions,
2842 usize::from(range.end()),
2843 format!(", \"assertion failed: {{}}\", stringify!({original})"),
2844 );
2845 }
2846 }
2847 for expression in standalone_matches(root, &matches, &assertions) {
2850 let frame_name = allocate_frame_name(file, &expression, "matches", &mut identifiers);
2851 instrument_decision(
2852 &mut insertions,
2853 runtime_path,
2854 file,
2855 &expression,
2856 "matches",
2857 &frame_name,
2858 );
2859 }
2860 for expression in value_decisions(root, &assertions) {
2861 let frame_name = allocate_frame_name(file, &expression, "value", &mut identifiers);
2862 instrument_decision(
2863 &mut insertions,
2864 runtime_path,
2865 file,
2866 &expression,
2867 "value",
2868 &frame_name,
2869 );
2870 }
2871
2872 if !compiled_out.is_empty() {
2879 let within = |start: usize, end: usize| {
2880 compiled_out
2881 .iter()
2882 .any(|range| usize::from(range.start()) <= start && end <= usize::from(range.end()))
2883 };
2884 let strictly_inside = |offset: usize| {
2885 compiled_out.iter().any(|range| {
2886 usize::from(range.start()) < offset && offset < usize::from(range.end())
2887 })
2888 };
2889 let mut kept = Vec::with_capacity(insertions.len());
2890 let mut pending = insertions.into_iter().peekable();
2891 while let Some(insertion) = pending.next() {
2892 match insertion.kind {
2893 InsertionKind::Start => {
2896 match pending.next_if(|next| next.kind == InsertionKind::End) {
2897 Some(end) if within(insertion.offset, end.offset) => {}
2898 Some(end) => {
2899 kept.push(insertion);
2900 kept.push(end);
2901 }
2902 None => kept.push(insertion),
2903 }
2904 }
2905 InsertionKind::End => kept.push(insertion),
2906 InsertionKind::Direct => {
2907 if !strictly_inside(insertion.offset) {
2908 kept.push(insertion);
2909 }
2910 }
2911 }
2912 }
2913 insertions = kept;
2914 }
2915 if !tests.is_empty() {
2918 let inside = |offset: usize| {
2919 tests.iter().any(|test| {
2920 usize::from(test.start()) <= offset && offset <= usize::from(test.end())
2921 })
2922 };
2923 let markers = manifest
2924 .assertion_sites
2925 .iter()
2926 .map(|site| {
2927 (
2928 format!(
2929 "{runtime_path}::assertion({});",
2930 assertion_ordinal(&site.id)
2931 ),
2932 site.id.clone(),
2933 )
2934 })
2935 .collect::<std::collections::BTreeMap<_, _>>();
2936 let mut marked = BTreeSet::new();
2937 insertions.retain(|insertion| {
2938 if !inside(insertion.offset) {
2939 return true;
2940 }
2941 match markers.get(&insertion.text) {
2942 Some(id) => {
2943 marked.insert(id.clone());
2944 true
2945 }
2946 None => false,
2947 }
2948 });
2949 manifest
2950 .assertion_sites
2951 .retain(|site| marked.contains(&site.id));
2952 }
2953
2954 if skipped_attributed_statement && !test_file {
2955 add_manifest_limitation(
2956 &mut manifest,
2957 file,
2958 "rust-attributed-statement-probes-not-injected",
2959 "A `let` without an initializer that carries outer attributes has no expression to hold a probe",
2960 );
2961 }
2962 manifest.limitations.sort_by(|left, right| {
2963 left.get("id")
2964 .and_then(|value| value.as_str())
2965 .cmp(&right.get("id").and_then(|value| value.as_str()))
2966 });
2967
2968 let code = apply_insertions(source, insertions)?;
2969 let transformed = SourceFile::parse(&code, edition);
2970 let errors = transformed
2971 .errors()
2972 .into_iter()
2973 .map(|error| error.to_string())
2974 .collect::<Vec<_>>();
2975 if !errors.is_empty() {
2976 if let Some(directory) = std::env::var_os(FAILED_TRANSFORM_DUMP_ENV) {
2980 let name = file.replace(['/', '\\'], "__");
2981 let _ = std::fs::create_dir_all(&directory);
2982 let _ = std::fs::write(std::path::Path::new(&directory).join(name), &code);
2983 }
2984 return Err(RustInstrumenterError::Parse(errors));
2985 }
2986 Ok(RustInstrumentedSource {
2987 code,
2988 manifest,
2989 probes: RustProbeLayout::default(),
2990 })
2991}
2992
2993#[cfg(test)]
2994mod tests {
2995 use std::{
2996 fs,
2997 process::Command,
2998 time::{SystemTime, UNIX_EPOCH},
2999 };
3000
3001 use super::*;
3002
3003 fn limitation_kind_of(limitation: &serde_json::Value) -> Option<&str> {
3005 limitation.get("id")?.as_str()?.split('#').next()
3006 }
3007
3008 const NOOP_RUNTIME: &str = r#"
3009#[doc(hidden)]
3010mod __supercov_runtime_v1 {
3011 pub struct DecisionFrame;
3012 impl DecisionFrame {
3013 pub fn new(_: u32, _: usize) -> Self { Self }
3014 }
3015 pub fn hit(_: u32) {}
3016 pub fn arms(_: &[u32], _: usize) {}
3017 pub fn logical(left: bool, _: bool, _: u32, _: u32) -> bool { left }
3018 pub fn for_loop<I: IntoIterator>(iterable: I, _: u32, _: u32) -> I::IntoIter {
3019 iterable.into_iter()
3020 }
3021 pub fn entered(_: &mut bool, _: u32) {}
3022 pub fn zero_iterations(_: bool, _: u32) {}
3023 pub trait TryProbe: Sized {
3024 fn probe(self, _: u32, _: u32) -> Self { self }
3025 }
3026 impl<T> TryProbe for T {}
3027 pub fn condition<V: std::ops::Not<Output = bool>>(value: V, _: &mut DecisionFrame, _: usize) -> bool { !!value }
3028 pub fn decision(value: bool, _: &mut DecisionFrame) -> bool { value }
3029 pub fn reached(_: &mut DecisionFrame, _: usize) -> bool { true }
3030 pub fn decision_chain(_: &mut DecisionFrame, _: bool, _: &[(usize, u32, u32)]) {}
3031 pub fn assertion(_: u32) {}
3032}
3033"#;
3034
3035 fn compile_and_run(source: &str, name: &str) -> std::process::Output {
3036 compile_and_run_edition(source, name, "2024")
3037 }
3038
3039 fn compile_and_run_edition(source: &str, name: &str, edition: &str) -> std::process::Output {
3040 let nonce = SystemTime::now()
3041 .duration_since(UNIX_EPOCH)
3042 .unwrap()
3043 .as_nanos();
3044 let directory = std::env::temp_dir().join(format!(
3045 "supercov-rust-transform-{}-{nonce}-{name}",
3046 std::process::id()
3047 ));
3048 fs::create_dir(&directory).unwrap();
3049 let input = directory.join("main.rs");
3050 let binary = directory.join("program");
3051 fs::write(&input, source).unwrap();
3052 let compile = Command::new("rustc")
3053 .arg(format!("--edition={edition}"))
3054 .arg(&input)
3055 .arg("-o")
3056 .arg(&binary)
3057 .output()
3058 .unwrap();
3059 assert!(
3060 compile.status.success(),
3061 "rustc failed:\n{}\nsource:\n{source}",
3062 String::from_utf8_lossy(&compile.stderr)
3063 );
3064 let output = Command::new(&binary).output().unwrap();
3065 fs::remove_dir_all(directory).unwrap();
3066 output
3067 }
3068
3069 #[test]
3070 fn a_match_and_a_loop_take_no_line_of_their_own() {
3071 let source = r#"fn kind(key: &str) -> u8 {
3076 match key.len() {
3077 0 => 0,
3078 n if n < 4 => {
3079 1
3080 }
3081 _ => 2,
3082 }
3083}
3084
3085fn scan(key: &str, limit: usize) -> usize {
3086 let mut seen = 0;
3087 while seen < limit && seen < key.len() {
3088 if key.as_bytes()[seen] == b'-' || seen > 8 {
3089 break;
3090 } else {
3091 seen += 1;
3092 }
3093 }
3094 for _ in 0..limit {
3095 seen += 0;
3096 }
3097 seen
3098}
3099"#;
3100 let transformed =
3101 instrument_rust_source("src/scan.rs", source, "crate::__supercov_runtime_v1").unwrap();
3102 assert_eq!(
3103 transformed.code.lines().count(),
3104 source.lines().count(),
3105 "{}",
3106 transformed.code
3107 );
3108 for (written, rewritten) in source.lines().zip(transformed.code.lines()) {
3110 let end = written.trim().trim_end_matches(['{', ';', ',']).trim();
3111 let last = end.rsplit(' ').next().unwrap_or_default();
3112 assert!(rewritten.contains(last), "{written:?} became {rewritten:?}");
3113 }
3114 }
3115
3116 #[test]
3117 fn a_panic_is_reported_where_its_author_wrote_it() {
3118 let source = r#"fn check(key: &str) -> usize {
3124 if key.len() < 4 {
3125 panic!("key too short");
3126 }
3127 key.len()
3128}
3129
3130fn later(values: &[usize]) -> usize {
3131 let first = values[0];
3132 let total: usize = values.iter().sum();
3133 assert!(total > first, "nothing but the first");
3134 total
3135}
3136
3137fn main() {
3138 let mode = std::env::args().nth(1).unwrap_or_default();
3139 if mode == "later" {
3140 later(&[3]);
3141 }
3142 let closure = |key: &str| {
3143 check(key)
3144 };
3145 closure("ab");
3146}
3147"#;
3148 let transformed =
3149 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3150 assert_eq!(
3151 transformed.code.lines().count(),
3152 source.lines().count(),
3153 "{}",
3154 transformed.code
3155 );
3156 for (number, (written, rewritten)) in
3158 source.lines().zip(transformed.code.lines()).enumerate()
3159 {
3160 let code = written.trim_start();
3161 let word = code.split(['(', ' ']).next().unwrap_or(code);
3163 if matches!(word, "panic!" | "assert!" | "let") {
3164 assert_eq!(
3165 rewritten.find(word),
3166 written.find(word),
3167 "line {}: {rewritten}",
3168 number + 1
3169 );
3170 }
3171 }
3172 let location = |output: &std::process::Output| {
3173 String::from_utf8_lossy(&output.stderr)
3174 .lines()
3175 .find(|line| line.contains("panicked at"))
3176 .map(|line| line[line.find("main.rs").unwrap()..].to_owned())
3177 .expect("a panic")
3178 };
3179 let original = compile_and_run(source, "panic-original");
3180 let instrumented = compile_and_run(
3181 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3182 "panic-instrumented",
3183 );
3184 assert_eq!(location(&original), "main.rs:3:9:");
3185 assert_eq!(location(&instrumented), location(&original));
3186 }
3187
3188 #[test]
3189 fn a_target_cfg_decides_only_what_the_target_settles() {
3190 let cfg = TargetCfg::new(
3191 &[
3192 "unix",
3193 "target_os=\"linux\"",
3194 "target_family=\"unix\"",
3195 "target_pointer_width=\"64\"",
3196 "debug_assertions",
3197 ]
3198 .map(|value| value.parse::<cargo_platform::Cfg>().unwrap()),
3199 );
3200 let decide = |expression: &str| cfg.evaluate(&expression.parse().unwrap());
3201 assert_eq!(decide("unix"), Some(true));
3202 assert_eq!(decide("windows"), Some(false));
3203 assert_eq!(decide("target_os = \"macos\""), Some(false));
3204 assert_eq!(decide("not(target_os = \"linux\")"), Some(false));
3205 assert_eq!(
3206 decide("all(unix, target_pointer_width = \"64\")"),
3207 Some(true)
3208 );
3209 assert_eq!(decide("any(windows, target_os = \"wasi\")"), Some(false));
3210 assert_eq!(decide("feature = \"extra\""), None);
3212 assert_eq!(decide("not(feature = \"extra\")"), None);
3213 assert_eq!(decide("debug_assertions"), None);
3214 assert_eq!(decide("all(windows, feature = \"extra\")"), Some(false));
3215 assert_eq!(decide("any(unix, feature = \"extra\")"), Some(true));
3216 assert_eq!(decide("any(windows, feature = \"extra\")"), None);
3217
3218 let source = "pub fn a() -> u8 { b() }\n#[cfg(windows)]\nfn b() -> u8 { 1 }\n#[cfg(unix)]\nfn b() -> u8 { 2 }\n#[cfg(feature = \"x\")]\nfn c() -> u8 { 3 }\n";
3219 let instrumented = instrument_rust_file(
3220 "src/lib.rs",
3221 source,
3222 "crate::__supercov_runtime_v1",
3223 false,
3224 Some(&cfg),
3225 )
3226 .unwrap();
3227 let lines = instrumented
3228 .manifest
3229 .points
3230 .iter()
3231 .map(|point| point.line)
3232 .collect::<BTreeSet<_>>();
3233 assert!(
3234 !lines.contains(&3),
3235 "the windows item is left out: {lines:?}"
3236 );
3237 assert!(lines.contains(&5) && lines.contains(&7), "{lines:?}");
3238 let windows = &instrumented.code[instrumented.code.find("#[cfg(windows)]").unwrap()
3239 ..instrumented.code.find("#[cfg(unix)]").unwrap()];
3240 assert!(!windows.contains("__supercov_runtime_v1"), "{windows}");
3241 }
3242
3243 #[test]
3244 fn code_behind_a_cfg_only_the_build_settles_is_gated_by_its_predicate() {
3245 let cfg = TargetCfg::new(
3246 &["unix", "target_os=\"linux\""]
3247 .map(|value| value.parse::<cargo_platform::Cfg>().unwrap()),
3248 );
3249 let source = "\
3250pub fn a() -> u8 {
3251 #[cfg(windows)]
3252 let v = 1;
3253 #[cfg(unix)]
3254 let v = 2;
3255 #[cfg(debug_assertions)]
3256 let w = v + 1;
3257 #[cfg(not(debug_assertions))]
3258 let w = v;
3259 w
3260}
3261#[cfg(all(unix, feature = \"x y\"))]
3262fn c() -> u8 {
3263 3
3264}
3265#[cfg(feature = \"z\")]
3266struct NoCode;
3267#[cfg(test)]
3268mod tests {
3269 #[test]
3270 fn t() {}
3271}
3272";
3273 let instrumented = instrument_rust_file(
3274 "src/lib.rs",
3275 source,
3276 "crate::__supercov_runtime_v1",
3277 false,
3278 Some(&cfg),
3279 )
3280 .unwrap();
3281 let lines = instrumented
3282 .manifest
3283 .points
3284 .iter()
3285 .map(|point| (point.line, point.id.clone()))
3286 .collect::<Vec<_>>();
3287 assert!(lines.iter().all(|(line, _)| *line != 3), "{lines:?}");
3289 let gates = &instrumented.probes.cfgs;
3290 let described = gates
3291 .iter()
3292 .map(|gate| (gate.predicate.as_str(), gate.line, gate.end_line))
3293 .collect::<Vec<_>>();
3294 assert_eq!(
3296 described,
3297 [
3298 ("debug_assertions", 6, 7),
3299 ("not(debug_assertions)", 8, 9),
3300 ("all(unix, feature = \"x y\")", 12, 15),
3301 ],
3302 "{gates:?}"
3303 );
3304 let in_c = lines
3305 .iter()
3306 .filter(|(line, _)| (13..=15).contains(line))
3307 .map(|(_, id)| id.clone())
3308 .collect::<BTreeSet<_>>();
3309 assert!(!in_c.is_empty());
3310 assert_eq!(
3311 gates[2]
3312 .obligations
3313 .iter()
3314 .cloned()
3315 .collect::<BTreeSet<_>>(),
3316 in_c
3317 );
3318 let unsettled = instrument_rust_file(
3320 "src/lib.rs",
3321 source,
3322 "crate::__supercov_runtime_v1",
3323 false,
3324 None,
3325 )
3326 .unwrap();
3327 assert!(
3328 unsettled
3329 .probes
3330 .cfgs
3331 .iter()
3332 .any(|gate| gate.predicate == "windows" && gate.line == 2),
3333 "{:?}",
3334 unsettled.probes.cfgs
3335 );
3336 }
3337
3338 #[test]
3339 fn test_code_is_known_by_where_it_sits() {
3340 for file in [
3341 "tests/it.rs",
3342 "tests/common/mod.rs",
3343 "crates/a/tests/it.rs",
3344 "examples/demo.rs",
3345 "benches/speed.rs",
3346 "src/tests.rs",
3347 "src/parser_tests.rs",
3348 "src/parser-tests.rs",
3349 ] {
3350 assert!(test_code_path(file), "{file}");
3351 }
3352 for file in [
3353 "src/lib.rs",
3354 "src/attests.rs",
3355 "src/_tests.rs",
3356 "crates/foo-tests/src/lib.rs",
3357 "src/testing.rs",
3358 ] {
3359 assert!(!test_code_path(file), "{file}");
3360 }
3361 }
3362
3363 #[test]
3364 fn test_items_keep_only_their_assertion_markers() {
3365 let source = r#"pub fn add(a: i32, b: i32) -> i32 {
3366 if a > 0 && b > 0 { a + b } else { 0 }
3367}
3368
3369#[cfg(test)]
3370mod tests {
3371 use super::*;
3372 #[test]
3373 fn adds() {
3374 let total = add(1, 2);
3375 assert_eq!(total, 3);
3376 }
3377}
3378
3379#[cfg(all(test, feature = "slow"))]
3380fn slow_helper() -> bool { true || false }
3381
3382#[tokio::test]
3383async fn async_adds() {
3384 assert!(add(2, 2) == 4);
3385}
3386"#;
3387 let transformed =
3388 instrument_rust_source("src/lib.rs", source, "crate::__supercov_runtime_v1").unwrap();
3389 let manifest = &transformed.manifest;
3390 assert!(
3391 manifest.points.iter().all(|point| point.line <= 3),
3392 "{:?}",
3393 manifest.points
3394 );
3395 assert!(manifest.decisions.iter().all(|decision| decision.line == 2));
3396 assert!(manifest.branches.iter().all(|branch| branch.line == 2));
3397 assert_eq!(manifest.decisions.len(), 1);
3398 let sites = manifest
3399 .assertion_sites
3400 .iter()
3401 .map(|site| site.source.as_str())
3402 .collect::<BTreeSet<_>>();
3403 assert_eq!(
3404 sites,
3405 BTreeSet::from(["assert!(add(2, 2) == 4);", "assert_eq!(total, 3);"])
3406 );
3407 let tests = &transformed.code[transformed.code.find("#[cfg(test)]").unwrap()..];
3408 assert_eq!(tests.matches("::assertion(").count(), 2, "{tests}");
3409 assert!(!tests.contains("::hit("), "{tests}");
3410 assert!(!tests.contains("::logical("), "{tests}");
3411
3412 let file = instrument_rust_source(
3414 "tests/it.rs",
3415 "#[test]\nfn it() {\n let x = 1;\n assert_eq!(x, 1);\n}\n",
3416 "crate::__supercov_runtime_v1",
3417 )
3418 .unwrap();
3419 assert!(file.manifest.points.is_empty());
3420 assert_eq!(file.manifest.assertion_sites.len(), 1);
3421 assert!(!file.code.contains("::hit("));
3422 }
3423
3424 #[test]
3425 fn discovers_rust_obligations_with_exact_ranges_and_stable_ids() {
3426 let source = r#"fn classify<T>(values: &[T], first: bool, second: bool, third: bool) -> Option<&T> {
3427 let picked = if first && (second || third) {
3428 values.first()?
3429 } else {
3430 None
3431 };
3432 for value in values {
3433 if first || second {
3434 return Some(value);
3435 }
3436 }
3437 match picked {
3438 Some(value) if second && third => Some(value),
3439 _ => None,
3440 }
3441}
3442
3443fn closure(value: i32) -> bool {
3444 (|candidate| candidate > 0)(value)
3445}
3446"#;
3447 let first = build_rust_manifest("src/lib.rs", source).unwrap();
3448 let second = build_rust_manifest("src/lib.rs", source).unwrap();
3449 assert_eq!(first, second);
3450 assert!(first.points.iter().any(|point| {
3451 point.kind == PointKind::Function && point.label.as_deref() == Some("classify")
3452 }));
3453 assert!(first.points.iter().any(|point| {
3454 point.kind == PointKind::Function && point.label.as_deref() == Some("<closure>")
3455 }));
3456 let first_if = first
3457 .decisions
3458 .iter()
3459 .find(|decision| decision.line == 2)
3460 .unwrap();
3461 assert_eq!(first_if.conditions, ["first", "second", "third"]);
3462 assert_eq!(first_if.column, 20);
3463 assert!(
3464 first
3465 .branches
3466 .iter()
3467 .any(|branch| branch.kind == "for-loop")
3468 );
3469 let mut arms = first
3470 .branches
3471 .iter()
3472 .filter(|branch| branch.kind == "match-arm")
3473 .collect::<Vec<_>>();
3474 arms.sort_by_key(|branch| branch.line);
3475 assert_eq!(arms.len(), 2);
3476 assert_eq!(
3477 arms[0]
3478 .alternatives
3479 .iter()
3480 .map(|alternative| alternative.label.as_str())
3481 .collect::<Vec<_>>(),
3482 ["not selected", "selected"]
3483 );
3484 assert_eq!(
3487 arms[1]
3488 .alternatives
3489 .iter()
3490 .map(|alternative| alternative.label.as_str())
3491 .collect::<Vec<_>>(),
3492 ["selected"]
3493 );
3494 assert!(
3495 first
3496 .branches
3497 .iter()
3498 .any(|branch| branch.kind == "try-operator")
3499 );
3500 assert!(first.decisions.iter().all(|decision| {
3501 decision.id.starts_with("rs:decision:") && decision.conditions.len() >= 2
3502 }));
3503 assert!(first.limitations.is_empty());
3504 }
3505
3506 #[test]
3507 fn declares_macro_and_const_boundaries_instead_of_hiding_them() {
3508 let source = r#"const fn doubled(value: usize) -> usize { value * 2 }
3509
3510macro_rules! noop {
3511 () => {};
3512}
3513
3514fn checked(value: bool) -> bool {
3515 assert!(value);
3516 noop!();
3517 const { doubled(2) == 4 }
3518}
3519"#;
3520 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
3521 assert!(manifest.decisions.iter().any(|decision| {
3524 decision.line == 8 && decision.source == "value" && decision.conditions == ["value"]
3525 }));
3526 let ids = manifest
3527 .limitations
3528 .iter()
3529 .filter_map(limitation_kind_of)
3530 .collect::<BTreeSet<_>>();
3531 assert_eq!(
3532 ids,
3533 BTreeSet::from([
3534 "rust-const-context-not-instrumented",
3535 "rust-macro-expansion-not-instrumented"
3536 ])
3537 );
3538 assert!(!manifest.points.iter().any(|point| {
3539 point.kind == PointKind::Function && point.label.as_deref() == Some("doubled")
3540 }));
3541 }
3542
3543 #[test]
3544 fn a_vec_macro_that_starts_a_larger_expression_keeps_its_probe_outside() {
3545 let source = "pub fn grown(n: usize) -> Vec<u8> {\n if n > 0 {\n vec![0; n].into()\n } else {\n vec![1; 2] == vec![1, 1] && n == 0;\n Vec::new()\n }\n}\n";
3549 let transformed =
3550 instrument_rust_source("src/lib.rs", source, "crate::__supercov_runtime_v1").unwrap();
3551 assert!(
3552 !transformed.code.contains("vec!crate"),
3553 "{}",
3554 transformed.code
3555 );
3556 assert!(
3557 transformed.code.contains("vec![0; n].into()"),
3558 "{}",
3559 transformed.code
3560 );
3561 }
3562
3563 #[test]
3564 fn transforms_points_and_nested_decisions_without_changing_behavior() {
3565 let source = r#"use std::sync::atomic::{AtomicUsize, Ordering};
3566
3567static CALLS: AtomicUsize = AtomicUsize::new(0);
3568
3569fn observed(name: &str, value: bool) -> bool {
3570 let order = CALLS.fetch_add(1, Ordering::SeqCst);
3571 println!("{order}:{name}:{value}");
3572 value
3573}
3574
3575fn classify(first: bool, second: bool, third: bool) -> i32 {
3576 if observed("a", first) && (observed("b", second) || observed("c", third)) {
3577 7
3578 } else {
3579 3
3580 }
3581}
3582
3583fn main() {
3584 let closure = |value: i32| value + 1;
3585 println!("result={}", closure(classify(true, false, true)));
3586}
3587"#;
3588 let transformed =
3589 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3590 assert!(transformed.code.contains("::condition("));
3591 assert!(transformed.code.contains("::decision("));
3592 assert!(transformed.code.contains("::hit("));
3593 let original = compile_and_run(source, "original");
3594 let instrumented = compile_and_run(
3595 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3596 "instrumented",
3597 );
3598 assert_eq!(instrumented.status, original.status);
3599 assert_eq!(instrumented.stdout, original.stdout);
3600 assert_eq!(instrumented.stderr, original.stderr);
3601 }
3602
3603 #[test]
3604 fn let_chains_take_derived_condition_probes_and_const_contexts_stay_declared() {
3605 let source = r#"const fn enabled(value: bool) -> bool {
3606 if value { true } else { false }
3607}
3608
3609fn classify(value: Option<bool>, fallback: bool) -> bool {
3610 if let Some(inner) = value && inner && fallback { true } else { false }
3611}
3612"#;
3613 let transformed =
3614 instrument_rust_source("src/lib.rs", source, "crate::__supercov_runtime_v1").unwrap();
3615 let ids = transformed
3616 .manifest
3617 .limitations
3618 .iter()
3619 .filter_map(limitation_kind_of)
3620 .collect::<BTreeSet<_>>();
3621 assert!(ids.contains("rust-const-context-not-instrumented"));
3622 assert!(!ids.contains("rust-let-chain-probes-not-injected"));
3623 assert!(
3626 transformed
3627 .code
3628 .contains("::reached(&mut __supercov_decision_")
3629 );
3630 assert!(
3631 transformed
3632 .code
3633 .contains("::condition((inner), &mut __supercov_decision_")
3634 );
3635 assert!(
3636 transformed
3637 .code
3638 .contains("::decision_chain(&mut __supercov_decision_")
3639 );
3640 assert!(transformed.code.contains("&& let Some(inner) = value &&"));
3641 assert!(!transformed.code.contains("condition((let"));
3642 }
3643
3644 #[test]
3645 fn std_macro_arguments_take_probes_and_assertions_are_decisions() {
3646 let source = r#"use std::fmt::Write as _;
3647
3648fn classify(values: &[i32], strict: bool) -> String {
3649 let mut out = String::new();
3650 assert!(values.len() < 10 && (strict || !values.is_empty()), "bad input {:?}", values);
3651 debug_assert!(values.iter().all(|v| *v > -100));
3652 let doubled = vec![values.iter().map(|v| v * 2).sum::<i32>(), if strict { 1 } else { 2 }];
3653 let repeated = vec![if strict { 1 } else { 0 }; values.len()];
3654 let small = matches!(values.first(), Some(v) if *v < 3);
3655 if matches!(values.len(), 1 | 2) && small {
3656 println!("small");
3657 }
3658 println!("{}", repeated.len() + small as usize);
3659 write!(out, "{}", doubled.iter().map(|d| if *d > 4 { "big" } else { "small" }).collect::<Vec<_>>().join(",")).unwrap();
3660 println!("{} {}", format!("{:?}", doubled), if values.first().copied().unwrap_or(0) > 0 && strict { "positive" } else { "other" });
3661 assert_eq!(doubled.len(), if strict { 2 } else { 2 }, "length for strict={strict}");
3662 out
3663}
3664
3665fn main() {
3666 println!("{}", classify(&[1, 2], true));
3667 println!("{}", classify(&[3], false));
3668 println!("{}", classify(&[], true));
3669 let total: i32 = dbg!(vec![1, 2, 3]).into_iter().sum();
3670 println!("{total}");
3671}
3672"#;
3673 let transformed =
3674 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3675 assert!(transformed.code.contains(
3678 r#", "assertion failed: {}", stringify!(values.iter().all(|v| *v > -100)))"#
3679 ));
3680 assert!(
3682 transformed
3683 .code
3684 .contains(r#"), "bad input {:?}", values);"#)
3685 );
3686 let assertion = transformed
3690 .manifest
3691 .decisions
3692 .iter()
3693 .find(|decision| decision.line == 5)
3694 .expect("assert! decision");
3695 assert_eq!(
3696 assertion.conditions,
3697 ["values.len() < 10", "strict", "!values.is_empty()"]
3698 );
3699 assert!(
3700 transformed
3701 .manifest
3702 .decisions
3703 .iter()
3704 .any(|decision| decision.line == 7)
3705 );
3706 assert!(
3707 transformed
3708 .manifest
3709 .decisions
3710 .iter()
3711 .any(|decision| decision.line == 9)
3712 );
3713 assert!(
3714 transformed
3715 .code
3716 .contains("assert!(({ let mut __supercov_decision_")
3717 );
3718 assert!(
3719 transformed
3720 .code
3721 .contains(", if ({ let mut __supercov_decision_")
3722 );
3723 assert!(
3725 transformed
3726 .code
3727 .contains("vec![if ({ let mut __supercov_decision_")
3728 );
3729 assert!(
3732 transformed
3733 .code
3734 .contains("let small = ({ let mut __supercov_decision_")
3735 );
3736 assert_eq!(
3737 transformed
3738 .manifest
3739 .decisions
3740 .iter()
3741 .filter(|decision| decision.line == 10)
3742 .count(),
3743 1
3744 );
3745 assert_eq!(
3746 transformed
3747 .manifest
3748 .decisions
3749 .iter()
3750 .filter(|decision| decision.line == 9)
3751 .count(),
3752 1
3753 );
3754 assert!(
3755 transformed
3756 .code
3757 .contains("vec![values.iter().map(|v| { crate::__supercov_runtime_v1::hit(")
3758 );
3759 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
3760 limitation.get("id").and_then(|id| id.as_str())
3761 == Some("rust-macro-expansion-not-instrumented")
3762 }));
3763 let original = compile_and_run(source, "original-macros");
3764 let instrumented = compile_and_run(
3765 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3766 "instrumented-macros",
3767 );
3768 assert_eq!(instrumented.status, original.status);
3769 assert_eq!(instrumented.stdout, original.stdout);
3770 let after_location = |stderr: &[u8]| {
3773 String::from_utf8_lossy(stderr)
3774 .lines()
3775 .map(|line| {
3776 line.split_once("] ")
3777 .map_or(line, |(_, rest)| rest)
3778 .to_owned()
3779 })
3780 .collect::<Vec<_>>()
3781 };
3782 assert_eq!(
3783 after_location(&instrumented.stderr),
3784 after_location(&original.stderr)
3785 );
3786 }
3787
3788 #[test]
3789 fn assertion_panic_messages_survive_instrumentation() {
3790 let source = r#"fn grow(len: usize, new_capacity: usize) {
3793 assert!(new_capacity >= len);
3794}
3795
3796fn check(value: i32) {
3797 assert!(value > 0 && value < 10, "value {value} out of range");
3798}
3799
3800// tokio: `assert!` only negates its operand, so a `&bool` is accepted.
3801fn all_seen(seen: &[bool]) {
3802 for was_seen in seen {
3803 assert!(was_seen);
3804 debug_assert!(was_seen, "seen");
3805 }
3806}
3807
3808fn main() {
3809 std::panic::set_hook(Box::new(|_| {}));
3810 all_seen(&[true, true]);
3811 match std::panic::catch_unwind(|| all_seen(&[true, false])) {
3812 Ok(()) => println!("ok"),
3813 Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
3814 }
3815 for (len, capacity) in [(3, 5), (8, 5)] {
3816 match std::panic::catch_unwind(|| grow(len, capacity)) {
3817 Ok(()) => println!("ok"),
3818 Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
3819 }
3820 }
3821 match std::panic::catch_unwind(|| check(12)) {
3822 Ok(()) => println!("ok"),
3823 Err(payload) => println!("{}", payload.downcast_ref::<String>().cloned().unwrap_or_default()),
3824 }
3825}
3826"#;
3827 let transformed =
3828 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3829 let original = compile_and_run(source, "original-assert-message");
3830 let instrumented = compile_and_run(
3831 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3832 "instrumented-assert-message",
3833 );
3834 assert_eq!(instrumented.status, original.status);
3835 assert_eq!(instrumented.stdout, original.stdout);
3836 assert!(
3837 String::from_utf8_lossy(&instrumented.stdout)
3838 .contains("assertion failed: new_capacity >= len")
3839 );
3840 assert!(
3841 String::from_utf8_lossy(&instrumented.stdout).contains("assertion failed: was_seen")
3842 );
3843 }
3844
3845 #[test]
3846 fn files_that_predate_a_reserved_word_still_instrument() {
3847 let source = r#"struct Rng(u64);
3849impl Rng {
3850 fn gen(&mut self) -> u64 {
3851 self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1);
3852 self.0 >> 33
3853 }
3854}
3855
3856fn main() {
3857 let mut rng = Rng(7);
3858 let mut odd = 0;
3859 for _ in 0..10 {
3860 if rng.gen() % 2 == 1 {
3861 odd += 1;
3862 }
3863 }
3864 println!("{odd}");
3865}
3866"#;
3867 let transformed =
3868 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3869 assert!(
3870 transformed
3871 .manifest
3872 .decisions
3873 .iter()
3874 .any(|decision| decision.line == 13)
3875 );
3876 let original = compile_and_run_edition(source, "original-gen", "2021");
3877 let instrumented = compile_and_run_edition(
3878 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3879 "instrumented-gen",
3880 "2021",
3881 );
3882 assert_eq!(instrumented.status, original.status);
3883 assert_eq!(instrumented.stdout, original.stdout);
3884 }
3885
3886 #[test]
3887 fn lone_let_conditions_compile_before_edition_2024_and_record_breaks() {
3888 let source = r#"fn first_even(values: &[i32]) -> Option<i32> {
3892 let mut it = values.iter();
3893 'scan: while let Some(value) = it.next() {
3894 if *value < 0 {
3895 break;
3896 }
3897 for _ in 0..1 {
3898 if *value == 99 {
3899 break 'scan;
3900 }
3901 if *value == 98 {
3902 break;
3903 }
3904 }
3905 if *value % 2 == 0 {
3906 return Some(*value);
3907 }
3908 }
3909 None
3910}
3911
3912fn describe(value: Option<i32>) -> &'static str {
3913 if let Some(inner) = value {
3914 if inner > 0 { "positive" } else { "non-positive" }
3915 } else if let None = value {
3916 "none"
3917 } else {
3918 "unreachable"
3919 }
3920}
3921
3922fn count(values: &[Option<i32>]) -> usize {
3923 let mut total = 0;
3924 for value in values {
3925 if let Some(_) = value {
3926 total += 1;
3927 }
3928 }
3929 total
3930}
3931
3932fn main() {
3933 println!("{:?} {:?} {:?} {:?}", first_even(&[1, 3, 4]), first_even(&[1, -1, 4]), first_even(&[99, 4]), first_even(&[98, 3, 6]));
3934 println!("{} {} {}", describe(Some(2)), describe(Some(-2)), describe(None));
3935 println!("{}", count(&[Some(1), None, Some(3)]));
3936}
3937"#;
3938 let transformed =
3939 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3940 assert!(!transformed.code.contains("&& let"));
3941 assert!(transformed.code.contains("__supercov_broke_"));
3942 assert_eq!(transformed.code.matches("= true; break").count(), 2);
3943 for edition in ["2021", "2024"] {
3944 let original =
3945 compile_and_run_edition(source, &format!("original-lone-let-{edition}"), edition);
3946 let instrumented = compile_and_run_edition(
3947 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3948 &format!("instrumented-lone-let-{edition}"),
3949 edition,
3950 );
3951 assert_eq!(instrumented.status, original.status);
3952 assert_eq!(instrumented.stdout, original.stdout);
3953 assert_eq!(instrumented.stderr, original.stderr);
3954 }
3955 }
3956
3957 #[test]
3958 fn let_chains_keep_their_behavior() {
3959 let source = r#"fn describe(value: Option<i32>, flag: bool) -> &'static str {
3960 if let Some(inner) = value && inner > 0 && flag {
3961 "positive"
3962 } else if let Some(inner) = value && (inner < 0 || flag) {
3963 "negative-or-flagged"
3964 } else {
3965 "other"
3966 }
3967}
3968
3969fn count_pairs(values: &[(Option<i32>, i32)]) -> i32 {
3970 let mut total = 0;
3971 let mut it = values.iter();
3972 while let Some((first, second)) = it.next() && let Some(inner) = first && *second > 0 {
3973 total += inner * second;
3974 if total > 100 {
3975 break;
3976 }
3977 }
3978 total
3979}
3980
3981fn tail(value: Option<&str>) -> usize {
3982 let pick = |v: Option<&str>| if let Some(text) = v && !text.is_empty() { text.len() } else { 0 };
3983 if let Some(text) = value && text.starts_with('x') {
3984 println!("x-prefixed");
3985 }
3986 pick(value)
3987}
3988
3989fn main() {
3990 for value in [Some(3), Some(-3), Some(0), None] {
3991 for flag in [true, false] {
3992 println!("{value:?} {flag} {}", describe(value, flag));
3993 }
3994 }
3995 println!("{}", count_pairs(&[(Some(2), 3), (Some(4), 5), (None, 1), (Some(9), 9)]));
3996 println!("{}", count_pairs(&[(Some(50), 3), (Some(4), 5)]));
3997 println!("{} {} {}", tail(Some("xyz")), tail(Some("")), tail(None));
3998}
3999"#;
4000 let transformed =
4001 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
4002 assert_eq!(
4003 transformed.code.matches("const __SUPERCOV_CHAIN_").count(),
4004 5
4005 );
4006 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
4007 limitation.get("id").and_then(|id| id.as_str())
4008 == Some("rust-let-chain-probes-not-injected")
4009 }));
4010 let original = compile_and_run(source, "original-chains");
4011 let instrumented = compile_and_run(
4012 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4013 "instrumented-chains",
4014 );
4015 assert_eq!(instrumented.status, original.status);
4016 assert_eq!(instrumented.stdout, original.stdout);
4017 assert_eq!(instrumented.stderr, original.stderr);
4018 }
4019
4020 #[test]
4021 fn instrumented_const_and_static_initialisers_still_compile() {
4022 let source = r#"const DIRECT: usize = if cfg!(unix) { 100 } else { 1_000 };
4028static WIDTH: usize = if cfg!(unix) { 2 } else { 4 };
4029
4030enum Mode {
4031 Narrow = if cfg!(unix) { 1 } else { 2 },
4032}
4033
4034struct Buffer([u8; if cfg!(unix) { 4 } else { 8 }]);
4035
4036impl Buffer {
4037 const SPAN: usize = if cfg!(unix) { 5 } else { 9 };
4038}
4039
4040fn scaled(flag: bool) -> usize {
4041 const LOCAL: usize = if cfg!(unix) { 3 } else { 6 };
4042 if flag { LOCAL + Buffer::SPAN } else { DIRECT + WIDTH }
4043}
4044
4045fn main() {
4046 let buffer = Buffer([0; if cfg!(unix) { 4 } else { 8 }]);
4047 println!(
4048 "{} {} {} {}",
4049 scaled(true),
4050 scaled(false),
4051 Mode::Narrow as usize,
4052 buffer.0.len()
4053 );
4054}
4055"#;
4056 let transformed =
4057 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
4058 assert!(transformed.code.contains("::decision("));
4061 let ids = transformed
4062 .manifest
4063 .limitations
4064 .iter()
4065 .filter_map(limitation_kind_of)
4066 .collect::<BTreeSet<_>>();
4067 assert!(ids.contains("rust-const-context-not-instrumented"));
4068
4069 let original = compile_and_run(source, "const-original");
4070 let instrumented = compile_and_run(
4071 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4072 "const-instrumented",
4073 );
4074 assert_eq!(instrumented.status, original.status);
4075 assert_eq!(instrumented.stdout, original.stdout);
4076 assert_eq!(instrumented.stderr, original.stderr);
4077 }
4078
4079 #[test]
4080 fn a_probed_global_allocator_would_recurse_into_itself() {
4081 let source = r#"use std::alloc::{GlobalAlloc, Layout, System};
4087
4088struct Odd;
4089
4090unsafe impl GlobalAlloc for Odd {
4091 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
4092 if layout.align() == 1 && layout.size() > 0 {
4093 System.alloc(layout)
4094 } else {
4095 System.alloc(layout)
4096 }
4097 }
4098
4099 unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
4100 System.dealloc(pointer, layout);
4101 }
4102}
4103
4104#[global_allocator]
4105static ODD: Odd = Odd;
4106
4107fn classify(flag: bool) -> usize {
4108 if flag { 1 } else { 2 }
4109}
4110
4111fn main() {
4112 let held = std::vec![7u8; 32];
4113 println!("{} {}", classify(!held.is_empty()), held.len());
4114}
4115"#;
4116 let transformed =
4117 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
4118 let allocator = transformed
4120 .code
4121 .split("unsafe impl GlobalAlloc for Odd")
4122 .nth(1)
4123 .and_then(|rest| rest.split("#[global_allocator]").next())
4124 .expect("the instrumented source still contains the allocator impl");
4125 assert!(
4126 !allocator.contains("__supercov_runtime_v1"),
4127 "probe injected into a GlobalAlloc impl:\n{allocator}"
4128 );
4129 assert!(transformed.code.contains("::decision("));
4131 let ids = transformed
4132 .manifest
4133 .limitations
4134 .iter()
4135 .filter_map(limitation_kind_of)
4136 .collect::<BTreeSet<_>>();
4137 assert!(ids.contains("rust-global-allocator-not-instrumented"));
4138
4139 let original = compile_and_run(source, "alloc-original");
4140 let instrumented = compile_and_run(
4141 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4142 "alloc-instrumented",
4143 );
4144 assert_eq!(instrumented.status, original.status);
4145 assert_eq!(instrumented.stdout, original.stdout);
4146 assert_eq!(instrumented.stderr, original.stderr);
4147 }
4148
4149 #[test]
4150 fn match_arms_record_selection_without_changing_behavior() {
4151 let source = r#"#[derive(Debug)]
4152enum Shape { Dot, Line(i32), Box { w: i32, h: i32 } }
4153
4154fn area(shape: &Shape) -> i32 {
4155 match shape {
4156 Shape::Dot => 0,
4157 Shape::Line(length) if *length < 0 => -length,
4158 Shape::Line(length) => *length,
4159 Shape::Box { w, h } => {
4160 let area = w * h;
4161 area
4162 }
4163 }
4164}
4165
4166fn describe(value: i32) -> &'static str {
4167 let inner = |v: i32| match v { 0 => "none", 1 => "one", _ => "many" };
4168 match value {
4169 0 => inner(value),
4170 n if n < 0 => unsafe { std::hint::unreachable_unchecked() },
4171 n => match n % 2 {
4172 0 => "even",
4173 _ => inner(n),
4174 },
4175 }
4176}
4177
4178fn main() {
4179 for shape in [Shape::Dot, Shape::Line(-3), Shape::Line(4), Shape::Box { w: 2, h: 5 }] {
4180 println!("{shape:?}={}", area(&shape));
4181 }
4182 for value in [0, 1, 3, 8] {
4183 println!("{value}:{}", describe(value));
4184 }
4185}
4186"#;
4187 let transformed =
4188 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
4189 assert!(transformed.code.contains("::arms(__SUPERCOV_ARMS_"));
4190 assert_eq!(
4191 transformed.code.matches("const __SUPERCOV_ARMS_").count(),
4192 4
4193 );
4194 let arms = transformed
4195 .manifest
4196 .branches
4197 .iter()
4198 .filter(|branch| branch.kind == "match-arm")
4199 .count();
4200 assert_eq!(arms, 4 + 3 + 3 + 2);
4201 for branch in transformed
4204 .manifest
4205 .branches
4206 .iter()
4207 .filter(|branch| branch.kind == "match-arm")
4208 {
4209 for alternative in &branch.alternatives {
4210 let ordinal = transformed
4211 .probes
4212 .hits
4213 .iter()
4214 .position(|id| id == &alternative.id)
4215 .unwrap_or_else(|| panic!("{} has no probe", alternative.id));
4216 assert!(
4217 transformed.code.contains("&[u32] = &["),
4218 "{} is not in any table",
4219 alternative.id
4220 );
4221 assert!(transformed.code.contains(&format!("{ordinal}")));
4222 }
4223 }
4224 let original = compile_and_run(source, "original-arms");
4225 let instrumented = compile_and_run(
4226 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4227 "instrumented-arms",
4228 );
4229 assert_eq!(instrumented.status, original.status);
4230 assert_eq!(instrumented.stdout, original.stdout);
4231 assert_eq!(instrumented.stderr, original.stderr);
4232 }
4233
4234 #[test]
4235 fn loops_logic_and_try_record_their_branches_without_changing_behavior() {
4236 let source = r#"use std::ops::ControlFlow;
4237
4238fn total(values: &[i32]) -> i32 {
4239 let mut sum = 0;
4240 for value in values {
4241 sum += value;
4242 }
4243 'outer: for row in 0..3 {
4244 for column in 0..3 {
4245 if column > row {
4246 continue 'outer;
4247 }
4248 sum += row * column;
4249 }
4250 }
4251 sum
4252}
4253
4254fn first_even(values: &[i32]) -> Option<i32> {
4255 let mut index = 0;
4256 'scan: while index < values.len() {
4257 if values[index] % 2 == 0 {
4258 break 'scan;
4259 }
4260 index += 1;
4261 }
4262 let mut it = values.iter().skip(index);
4263 while let Some(value) = it.next() {
4264 return Some(*value);
4265 }
4266 None
4267}
4268
4269fn parse_twice(text: &str) -> Result<i32, String> {
4270 let value: i32 = text.trim().parse().map_err(|_| "bad".to_string())?;
4271 let doubled = Some(value).map(|v| v * 2).ok_or("none")?;
4272 Ok(doubled)
4273}
4274
4275fn halve(value: i32) -> Option<i32> {
4276 let even = (value % 2 == 0).then_some(value)?;
4277 Some(even / 2)
4278}
4279
4280fn flow(values: &[i32]) -> ControlFlow<i32, i32> {
4281 let mut sum = 0;
4282 for value in values {
4283 let step: ControlFlow<i32, i32> = if *value < 0 { ControlFlow::Break(*value) } else { ControlFlow::Continue(*value) };
4284 sum += step?;
4285 }
4286 ControlFlow::Continue(sum)
4287}
4288
4289fn gate(a: bool, b: bool, c: bool) -> bool {
4290 let both = a && b;
4291 let either = a || b || c;
4292 both || (either && !c) || (c && a && (b || !b))
4293}
4294
4295fn main() {
4296 println!("{} {}", total(&[]), total(&[1, 2, 3]));
4297 println!("{:?} {:?} {:?}", first_even(&[]), first_even(&[1, 3]), first_even(&[1, 4, 6]));
4298 println!("{:?} {:?}", parse_twice(" 21 "), parse_twice("x"));
4299 println!("{:?} {:?}", halve(8), halve(7));
4300 println!("{:?} {:?}", flow(&[1, 2]), flow(&[1, -5, 2]));
4301 for a in [false, true] {
4302 for b in [false, true] {
4303 for c in [false, true] {
4304 print!("{}", gate(a, b, c) as u8);
4305 }
4306 }
4307 }
4308 println!();
4309}
4310"#;
4311 let transformed =
4312 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
4313 for marker in [
4314 "::logical((",
4315 "::for_loop((",
4316 "::entered(&mut __supercov_loop_",
4317 "::zero_iterations(__supercov_loop_",
4318 "::TryProbe::probe((",
4319 ] {
4320 assert!(transformed.code.contains(marker), "{marker} missing");
4321 }
4322 let kinds = |kind: &str| {
4323 transformed
4324 .manifest
4325 .branches
4326 .iter()
4327 .filter(|branch| branch.kind == kind)
4328 .count()
4329 };
4330 assert_eq!(kinds("for-loop"), 3 + 1 + 3);
4331 assert_eq!(kinds("while-loop"), 2);
4332 assert_eq!(kinds("try-operator"), 4);
4333 assert_eq!(kinds("logical-and"), 4);
4334 assert_eq!(kinds("logical-or"), 5);
4335 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
4336 limitation.get("id").and_then(|id| id.as_str())
4337 == Some("rust-structural-branch-probes-not-yet-injected")
4338 }));
4339 let original = compile_and_run(source, "original-structural");
4340 let instrumented = compile_and_run(
4341 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4342 "instrumented-structural",
4343 );
4344 assert_eq!(instrumented.status, original.status);
4345 assert_eq!(instrumented.stdout, original.stdout);
4346 assert_eq!(instrumented.stderr, original.stderr);
4347 }
4348
4349 #[test]
4350 fn cfg_gated_sibling_blocks_keep_their_tail_position() {
4351 let source = r#"pub fn is_available() -> bool {
4357 #[cfg(target_endian = "little")]
4358 {
4359 true
4360 }
4361 #[cfg(not(target_endian = "little"))]
4362 {
4363 false
4364 }
4365}
4366
4367fn main() {
4368 println!("{}", is_available());
4369}
4370"#;
4371 let transformed =
4372 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
4373 let original = compile_and_run(source, "cfg-original");
4374 let instrumented = compile_and_run(
4375 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4376 "cfg-instrumented",
4377 );
4378 assert_eq!(instrumented.status, original.status);
4379 assert_eq!(instrumented.stdout, original.stdout);
4380 assert!(
4382 transformed
4383 .code
4384 .contains("{crate::__supercov_runtime_v1::hit("),
4385 "{}",
4386 transformed.code
4387 );
4388
4389 let attributed_let = r#"fn main() {
4393 #[cfg(target_endian = "little")]
4394 let value = 1;
4395 #[cfg(not(target_endian = "little"))]
4396 let value = 2;
4397 #[cfg(target_endian = "little")]
4398 let borrowed: &String = &String::from("little");
4399 #[cfg(not(target_endian = "little"))]
4400 let borrowed: &String = &String::from("big");
4401 #[cfg(target_endian = "little")]
4402 print!("le ");
4403 #[cfg(not(target_endian = "little"))]
4404 print!("be ");
4405 #[allow(unused_assignments)]
4406 let mut later;
4407 later = value + 1;
4408 println!("{value} {borrowed} {later}");
4409}
4410"#;
4411 let transformed = instrument_rust_source(
4412 "src/main.rs",
4413 attributed_let,
4414 "crate::__supercov_runtime_v1",
4415 )
4416 .unwrap();
4417 let ids = transformed
4418 .manifest
4419 .limitations
4420 .iter()
4421 .filter_map(limitation_kind_of)
4422 .collect::<BTreeSet<_>>();
4423 assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
4425 assert!(
4426 transformed
4427 .code
4428 .contains("let value = { crate::__supercov_runtime_v1::hit(")
4429 );
4430 assert!(
4431 transformed
4432 .code
4433 .contains("let borrowed: &String = { crate::__supercov_runtime_v1::hit(")
4434 );
4435 assert!(
4436 transformed
4437 .code
4438 .contains("] { crate::__supercov_runtime_v1::hit(")
4439 );
4440 let original = compile_and_run(attributed_let, "cfg-let-original");
4441 let instrumented = compile_and_run(
4442 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4443 "cfg-let-instrumented",
4444 );
4445 assert_eq!(instrumented.status, original.status);
4446 assert_eq!(instrumented.stdout, original.stdout);
4447 assert_eq!(instrumented.stderr, original.stderr);
4448
4449 let trailing_macro = r#"macro_rules! pick { ($e:expr) => { $e } }
4454fn value() -> i32 {
4455 let base = 20;
4456 #[rustfmt::skip]
4457 pick! { base + 1 }
4458}
4459fn effect() {
4460 #[rustfmt::skip]
4461 println! { "effect" }
4462}
4463fn gated() {
4464 #[cfg(target_endian = "little")]
4465 println! { "little" }
4466}
4467fn main() {
4468 effect();
4469 gated();
4470 println!("{}", value());
4471}
4472"#;
4473 let transformed = instrument_rust_source(
4474 "src/main.rs",
4475 trailing_macro,
4476 "crate::__supercov_runtime_v1",
4477 )
4478 .unwrap();
4479 assert!(
4480 transformed
4481 .code
4482 .contains("{ crate::__supercov_runtime_v1::hit(")
4483 );
4484 assert!(
4485 transformed
4486 .code
4487 .contains("); #[rustfmt::skip]\n pick! { base + 1 } }")
4488 );
4489 assert!(
4490 transformed
4491 .code
4492 .contains("); #[rustfmt::skip]\n println! { \"effect\" } }")
4493 );
4494 assert!(
4495 transformed.code.contains(
4496 "\n #[cfg(target_endian = \"little\")]\n println! { \"little\" }\n"
4497 )
4498 );
4499 let ids = transformed
4500 .manifest
4501 .limitations
4502 .iter()
4503 .filter_map(limitation_kind_of)
4504 .collect::<BTreeSet<_>>();
4505 assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
4506 let original = compile_and_run(trailing_macro, "trailing-macro-original");
4507 let instrumented = compile_and_run(
4508 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4509 "trailing-macro-instrumented",
4510 );
4511 assert_eq!(
4512 instrumented.status,
4513 original.status,
4514 "{}",
4515 String::from_utf8_lossy(&instrumented.stderr)
4516 );
4517 assert_eq!(instrumented.stdout, original.stdout);
4518 assert_eq!(instrumented.stderr, original.stderr);
4519 }
4520
4521 #[test]
4522 fn an_attributed_macro_statement_keeps_its_macro_in_statement_position() {
4523 let source = r#"macro_rules! trace {
4528 ($($arg:tt)*) => {
4529 #[cfg(target_endian = "little")]
4530 {
4531 println!($($arg)+);
4532 }
4533 }
4534}
4535fn manual() {
4536 #[cfg(any(target_endian = "little", target_endian = "big"))]
4537 trace!("manual");
4538 let _ = 1;
4539}
4540fn main() {
4541 manual();
4542}
4543"#;
4544 let transformed =
4545 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
4546 assert!(
4549 transformed.code.contains(r#"trace!("manual"); }"#),
4550 "{}",
4551 transformed.code
4552 );
4553 let original = compile_and_run(source, "attributed-macro-statement-original");
4554 let instrumented = compile_and_run(
4555 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4556 "attributed-macro-statement-instrumented",
4557 );
4558 assert_eq!(
4559 instrumented.status,
4560 original.status,
4561 "{}",
4562 String::from_utf8_lossy(&instrumented.stderr)
4563 );
4564 assert_eq!(instrumented.stdout, original.stdout);
4565 assert_eq!(instrumented.stderr, original.stderr);
4566 }
4567
4568 #[test]
4569 fn an_attributed_lets_macro_initialiser_keeps_its_value() {
4570 let source = r#"macro_rules! first {
4576 ($e:expr) => { $e }
4577}
4578fn value(flag: bool) -> i32 {
4579 #[cfg(any(target_endian = "little", target_endian = "big"))]
4580 let chosen = first!(if flag { 7 } else { 3 });
4581 chosen + 1
4582}
4583fn main() {
4584 println!("{}", value(true));
4585}
4586"#;
4587 let transformed =
4588 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
4589 assert!(
4592 !transformed
4593 .code
4594 .contains("first!(if flag { 7 } else { 3 }); }"),
4595 "{}",
4596 transformed.code
4597 );
4598 let original = compile_and_run(source, "attributed-let-macro-original");
4599 let instrumented = compile_and_run(
4600 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4601 "attributed-let-macro-instrumented",
4602 );
4603 assert_eq!(
4604 instrumented.status,
4605 original.status,
4606 "{}",
4607 String::from_utf8_lossy(&instrumented.stderr)
4608 );
4609 assert_eq!(instrumented.stdout, original.stdout);
4610 assert_eq!(instrumented.stdout, b"8\n");
4611 }
4612
4613 #[test]
4614 fn obligations_no_probe_can_reach_are_declined() {
4615 let source = r#"pub struct Tagged(usize);
4621impl Tagged {
4622 pub const fn new(len: usize, on_heap: bool) -> Self {
4623 Self(if on_heap { len << 1 } else { len })
4624 }
4625 pub fn plain(len: usize) -> Self {
4626 Self(if len > 0 { len } else { 0 })
4627 }
4628}
4629"#;
4630 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
4631 let declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
4632 assert!(!declined.is_empty(), "the const fn was not declined");
4633
4634 let line_of = |id: &str| {
4636 manifest
4637 .points
4638 .iter()
4639 .find(|point| point.id == id)
4640 .map(|point| point.line)
4641 .or_else(|| {
4642 manifest
4643 .decisions
4644 .iter()
4645 .find(|decision| decision.id == id)
4646 .map(|decision| decision.line)
4647 })
4648 .or_else(|| {
4649 manifest
4650 .branches
4651 .iter()
4652 .find(|branch| branch.id == id)
4653 .map(|branch| branch.line)
4654 })
4655 };
4656 for id in &declined {
4657 let line = line_of(id).unwrap_or_else(|| panic!("no obligation {id}"));
4658 assert!(
4659 (3..=5).contains(&line),
4660 "declined an obligation outside the const fn, at line {line}"
4661 );
4662 }
4663 let measured = manifest
4664 .points
4665 .iter()
4666 .map(|point| (point.id.clone(), point.line))
4667 .filter(|(id, _)| !declined.contains(id))
4668 .collect::<Vec<_>>();
4669 assert!(
4670 measured.iter().any(|(_, line)| (6..=8).contains(line)),
4671 "the plain fn must stay measured: {measured:?}"
4672 );
4673 }
4674
4675 #[test]
4676 fn a_documented_function_is_reported_where_it_starts() {
4677 let source = r#"/// Documented.
4680/// Twice.
4681#[inline]
4682pub fn documented(value: i32) -> i32 {
4683 value + 1
4684}
4685"#;
4686 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
4687 let point = manifest
4688 .points
4689 .iter()
4690 .find(|point| {
4691 point.kind == PointKind::Function && point.label.as_deref() == Some("documented")
4692 })
4693 .unwrap();
4694 assert_eq!(point.line, 4, "{point:?}");
4695 assert!(
4696 point.source.starts_with("pub fn documented"),
4697 "{}",
4698 point.source
4699 );
4700 }
4701
4702 #[test]
4703 fn rejects_non_crate_local_runtime_paths() {
4704 assert_eq!(
4705 instrument_rust_source("src/lib.rs", "fn okay() {}", "supercov::runtime"),
4706 Err(RustInstrumenterError::InvalidRuntimePath)
4707 );
4708 }
4709
4710 #[test]
4711 fn rejects_invalid_rust_without_partial_obligations() {
4712 assert!(matches!(
4713 build_rust_manifest("src/lib.rs", "fn broken( {\n"),
4714 Err(RustInstrumenterError::Parse(_))
4715 ));
4716 }
4717}