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}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50enum InsertionKind {
51 End,
52 Direct,
53 Start,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57struct Insertion {
58 offset: usize,
59 kind: InsertionKind,
60 scope_len: usize,
61 rank: usize,
62 text: String,
63}
64
65fn valid_runtime_path(path: &str) -> bool {
66 let mut parts = path.split("::");
67 if !matches!(parts.next(), Some("crate")) {
68 return false;
69 }
70 let parts = parts.collect::<Vec<_>>();
71 !parts.is_empty()
72 && parts.into_iter().all(|part| {
73 !part.is_empty()
74 && part.bytes().enumerate().all(|(index, byte)| {
75 byte == b'_'
76 || byte.is_ascii_alphabetic()
77 || (index > 0 && byte.is_ascii_digit())
78 })
79 })
80}
81
82fn in_const_context(node: &ra_ap_syntax::SyntaxNode) -> bool {
100 let start = node.text_range().start();
101 node.ancestors().any(|ancestor| {
102 ast::Fn::cast(ancestor.clone()).is_some_and(|function| function.const_token().is_some())
103 || ast::BlockExpr::cast(ancestor.clone())
104 .is_some_and(|block| block.const_token().is_some())
105 || ast::Const::can_cast(ancestor.kind())
106 || ast::Static::can_cast(ancestor.kind())
107 || ast::ConstArg::can_cast(ancestor.kind())
108 || ast::ArrayExpr::cast(ancestor).is_some_and(|array| {
109 array
110 .semicolon_token()
111 .is_some_and(|semicolon| start >= semicolon.text_range().end())
112 })
113 })
114}
115
116fn in_global_allocator(node: &ra_ap_syntax::SyntaxNode) -> bool {
131 node.ancestors().any(|ancestor| {
132 ast::Impl::cast(ancestor).is_some_and(|block| {
133 block.trait_().is_some_and(|implemented| {
134 implemented
135 .syntax()
136 .descendants_with_tokens()
137 .filter_map(|element| element.into_token())
138 .any(|token| token.kind() == SyntaxKind::IDENT && token.text() == "GlobalAlloc")
139 })
140 })
141 })
142}
143
144fn in_global_allocator_impl(node: &ra_ap_syntax::SyntaxNode) -> bool {
146 ast::Impl::cast(node.clone()).is_some_and(|block| {
147 block.trait_().is_some_and(|implemented| {
148 implemented
149 .syntax()
150 .descendants_with_tokens()
151 .filter_map(|element| element.into_token())
152 .any(|token| token.kind() == SyntaxKind::IDENT && token.text() == "GlobalAlloc")
153 })
154 })
155}
156
157fn cannot_carry_probe(node: &ra_ap_syntax::SyntaxNode) -> bool {
159 in_const_context(node) || in_global_allocator(node)
160}
161
162fn range_offsets(range: TextRange) -> (usize, usize) {
163 (usize::from(range.start()), usize::from(range.end()))
164}
165
166fn push_wrapper(
167 insertions: &mut Vec<Insertion>,
168 range: TextRange,
169 scope: TextRange,
170 rank: usize,
171 prefix: String,
172 suffix: String,
173) {
174 let (start, end) = range_offsets(range);
175 let (scope_start, scope_end) = range_offsets(scope);
176 let scope_len = scope_end - scope_start;
177 insertions.push(Insertion {
178 offset: start,
179 kind: InsertionKind::Start,
180 scope_len,
181 rank,
182 text: prefix,
183 });
184 insertions.push(Insertion {
185 offset: end,
186 kind: InsertionKind::End,
187 scope_len,
188 rank,
189 text: suffix,
190 });
191}
192
193fn push_direct(insertions: &mut Vec<Insertion>, offset: usize, text: String) {
194 insertions.push(Insertion {
195 offset,
196 kind: InsertionKind::Direct,
197 scope_len: 0,
198 rank: 0,
199 text,
200 });
201}
202
203fn apply_insertions(
204 source: &str,
205 mut insertions: Vec<Insertion>,
206) -> Result<String, RustInstrumenterError> {
207 if insertions
208 .iter()
209 .any(|edit| edit.offset > source.len() || !source.is_char_boundary(edit.offset))
210 {
211 return Err(RustInstrumenterError::InvalidRange);
212 }
213 insertions.sort_by(|left, right| {
214 left.offset.cmp(&right.offset).then_with(|| {
215 let kind_order = |kind: InsertionKind| match kind {
216 InsertionKind::End => 0,
217 InsertionKind::Direct => 1,
218 InsertionKind::Start => 2,
219 };
220 kind_order(left.kind)
221 .cmp(&kind_order(right.kind))
222 .then_with(|| match left.kind {
223 InsertionKind::End => left
224 .scope_len
225 .cmp(&right.scope_len)
226 .then_with(|| right.rank.cmp(&left.rank)),
227 InsertionKind::Direct => std::cmp::Ordering::Equal,
228 InsertionKind::Start => right
229 .scope_len
230 .cmp(&left.scope_len)
231 .then_with(|| left.rank.cmp(&right.rank)),
232 })
233 })
234 });
235
236 let mut output = source.to_owned();
237 let mut index = insertions.len();
238 while index > 0 {
239 let offset = insertions[index - 1].offset;
240 let start = insertions[..index].partition_point(|insertion| insertion.offset < offset);
241 let text = insertions[start..index]
242 .iter()
243 .map(|insertion| insertion.text.as_str())
244 .collect::<String>();
245 output.insert_str(offset, &text);
246 index = start;
247 }
248 Ok(output)
249}
250
251fn limitation_id(kind: &str, file: &str) -> String {
255 format!("{kind}#{file}")
256}
257
258fn add_manifest_limitation(manifest: &mut CoverageManifest, file: &str, id: &str, reason: &str) {
259 let id = limitation_id(id, file);
260 if manifest.limitations.iter().any(|limitation| {
261 limitation.get("id").and_then(|value| value.as_str()) == Some(id.as_str())
262 }) {
263 return;
264 }
265 manifest.limitations.push(json!({
266 "id": id,
267 "kind": "source-scope",
268 "file": file,
269 "line": 1,
270 "column": 0,
271 "source": "",
272 "reason": reason,
273 "blocking": false
276 }));
277}
278
279fn allocate_frame_name(
280 file: &str,
281 condition: &ast::Expr,
282 kind: &str,
283 identifiers: &mut BTreeSet<String>,
284) -> String {
285 let id = stable_id(file, "decision", condition.syntax().text_range(), kind);
286 let suffix = id.rsplit(':').next().unwrap_or("decision");
287 let base = format!("__supercov_decision_{suffix}");
288 let mut candidate = base.clone();
289 let mut attempt = 0_usize;
290 while !identifiers.insert(candidate.clone()) {
291 attempt += 1;
292 candidate = format!("{base}_{attempt}");
293 }
294 candidate
295}
296
297fn allocate_table_name(
300 file: &str,
301 expression: &ast::MatchExpr,
302 identifiers: &mut BTreeSet<String>,
303) -> String {
304 let id = stable_id(file, "match", expression.syntax().text_range(), "arms");
305 let suffix = id
306 .rsplit(':')
307 .next()
308 .unwrap_or("match")
309 .to_ascii_uppercase();
310 let base = format!("__SUPERCOV_ARMS_{suffix}");
311 let mut candidate = base.clone();
312 let mut attempt = 0_usize;
313 while !identifiers.insert(candidate.clone()) {
314 attempt += 1;
315 candidate = format!("{base}_{attempt}");
316 }
317 candidate
318}
319
320fn allocate_flag_name(
322 file: &str,
323 expression: &ast::WhileExpr,
324 identifiers: &mut BTreeSet<String>,
325) -> String {
326 let id = stable_id(file, "loop", expression.syntax().text_range(), "flag");
327 let suffix = id.rsplit(':').next().unwrap_or("loop");
328 let base = format!("__supercov_loop_{suffix}");
329 let mut candidate = base.clone();
330 let mut attempt = 0_usize;
331 while !identifiers.insert(candidate.clone()) {
332 attempt += 1;
333 candidate = format!("{base}_{attempt}");
334 }
335 candidate
336}
337
338impl std::error::Error for RustInstrumenterError {}
339
340struct SourceLocations<'a> {
341 source: &'a str,
342 line_starts: Vec<usize>,
343}
344
345impl<'a> SourceLocations<'a> {
346 fn new(source: &'a str) -> Self {
347 let mut line_starts = vec![0];
348 line_starts.extend(
349 source
350 .bytes()
351 .enumerate()
352 .filter_map(|(index, byte)| (byte == b'\n').then_some(index + 1)),
353 );
354 Self {
355 source,
356 line_starts,
357 }
358 }
359
360 fn range(&self, range: TextRange) -> Result<(usize, usize), RustInstrumenterError> {
361 let start = usize::from(range.start());
362 let end = usize::from(range.end());
363 if start > end
364 || end > self.source.len()
365 || !self.source.is_char_boundary(start)
366 || !self.source.is_char_boundary(end)
367 {
368 return Err(RustInstrumenterError::InvalidRange);
369 }
370 Ok((start, end))
371 }
372
373 fn line_column(&self, offset: usize) -> (usize, usize) {
374 let line_index = self.line_starts.partition_point(|start| *start <= offset) - 1;
375 (line_index + 1, offset - self.line_starts[line_index])
376 }
377
378 fn text(&self, range: TextRange) -> Result<String, RustInstrumenterError> {
379 let (start, end) = self.range(range)?;
380 Ok(self.source[start..end].trim().to_owned())
381 }
382}
383
384fn stable_id(file: &str, kind: &str, range: TextRange, suffix: &str) -> String {
385 let mut hash = Sha256::new();
386 let start = usize::from(range.start()).to_string();
387 let end = usize::from(range.end()).to_string();
388 for value in [file, kind, &start, &end, suffix] {
389 hash.update(value.as_bytes());
390 hash.update([0]);
391 }
392 let digest = hash.finalize();
393 let mut encoded = String::with_capacity(24);
394 for byte in &digest[..12] {
395 use std::fmt::Write as _;
396 write!(&mut encoded, "{byte:02x}").expect("writing to a string cannot fail");
397 }
398 format!("rs:{kind}:{encoded}")
399}
400
401struct RustObligationCollector<'a> {
402 file: &'a str,
403 locations: SourceLocations<'a>,
404 manifest: CoverageManifest,
405 point_ids: BTreeSet<String>,
406 decision_ids: BTreeSet<String>,
407 branch_ids: BTreeSet<String>,
408 site_limitations: BTreeSet<&'static str>,
410 obligation_ranges: Vec<(String, TextRange)>,
413 error: Option<RustInstrumenterError>,
414}
415
416impl<'a> RustObligationCollector<'a> {
417 fn new(file: &'a str, source: &'a str) -> Self {
418 Self {
419 file,
420 locations: SourceLocations::new(source),
421 manifest: CoverageManifest {
422 unmeasured: Vec::new(),
423 decisions: Vec::new(),
424 points: Vec::new(),
425 branches: Vec::new(),
426 limitations: Vec::new(),
427 scope: None,
428 },
429 point_ids: BTreeSet::new(),
430 decision_ids: BTreeSet::new(),
431 branch_ids: BTreeSet::new(),
432 site_limitations: BTreeSet::new(),
433 obligation_ranges: Vec::new(),
434 error: None,
435 }
436 }
437
438 fn location_source(&mut self, range: TextRange) -> Option<(usize, usize, String)> {
439 let result = self.locations.range(range).map(|(start, _)| {
440 let (line, column) = self.locations.line_column(start);
441 (line, column, self.locations.text(range))
442 });
443 match result {
444 Ok((line, column, Ok(source))) => Some((line, column, source)),
445 Ok((_, _, Err(error))) | Err(error) => {
446 self.error.get_or_insert(error);
447 None
448 }
449 }
450 }
451
452 fn point(&mut self, range: TextRange, kind: PointKind, label: Option<String>) {
453 let kind_name = match kind {
454 PointKind::Statement => "statement",
455 PointKind::Function => "function",
456 };
457 let id = stable_id(self.file, kind_name, range, label.as_deref().unwrap_or(""));
458 self.obligation_ranges.push((id.clone(), range));
459 if !self.point_ids.insert(id.clone()) {
460 return;
461 }
462 let Some((line, column, source)) = self.location_source(range) else {
463 return;
464 };
465 self.manifest.points.push(PointMeta {
466 id,
467 kind,
468 file: self.file.into(),
469 line,
470 column,
471 source,
472 label,
473 });
474 }
475
476 fn atomic_condition_ranges(expression: &ast::Expr, ranges: &mut Vec<TextRange>) {
477 match expression {
478 ast::Expr::ParenExpr(paren) => {
479 if let Some(inner) = paren.expr() {
480 Self::atomic_condition_ranges(&inner, ranges);
481 } else {
482 ranges.push(expression.syntax().text_range());
483 }
484 }
485 ast::Expr::BinExpr(binary)
486 if matches!(
487 binary.op_kind(),
488 Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))
489 ) =>
490 {
491 if let Some(left) = binary.lhs() {
492 Self::atomic_condition_ranges(&left, ranges);
493 }
494 if let Some(right) = binary.rhs() {
495 Self::atomic_condition_ranges(&right, ranges);
496 }
497 }
498 _ => ranges.push(expression.syntax().text_range()),
499 }
500 }
501
502 fn decision(&mut self, test: &ast::Expr, kind: &str) {
503 let range = test.syntax().text_range();
504 let id = stable_id(self.file, "decision", range, kind);
505 self.obligation_ranges.push((id.clone(), range));
506 if !self.decision_ids.insert(id.clone()) {
507 return;
508 }
509 let Some((line, column, source)) = self.location_source(range) else {
510 return;
511 };
512 let mut condition_ranges = Vec::new();
513 Self::atomic_condition_ranges(test, &mut condition_ranges);
514 let mut conditions = Vec::with_capacity(condition_ranges.len());
515 for condition in condition_ranges {
516 match self.locations.text(condition) {
517 Ok(source) => conditions.push(source),
518 Err(error) => {
519 self.error.get_or_insert(error);
520 return;
521 }
522 }
523 }
524 self.manifest.decisions.push(DecisionMeta {
525 id: id.clone(),
526 file: self.file.into(),
527 line,
528 column,
529 source: source.clone(),
530 conditions,
531 kind: kind.into(),
532 });
533 self.branch_with_id(
534 format!("{id}:outcome"),
535 range,
536 kind,
537 source,
538 [("true", "true"), ("false", "false")],
539 );
540 }
541
542 fn branch<const N: usize>(
543 &mut self,
544 range: TextRange,
545 kind: &str,
546 alternatives: [(&str, &str); N],
547 ) {
548 let id = stable_id(self.file, "branch", range, kind);
549 let Some((_, _, source)) = self.location_source(range) else {
550 return;
551 };
552 self.branch_with_id(id, range, kind, source, alternatives);
553 }
554
555 fn branch_with_id<const N: usize>(
556 &mut self,
557 id: String,
558 range: TextRange,
559 kind: &str,
560 source: String,
561 alternatives: [(&str, &str); N],
562 ) {
563 self.obligation_ranges.push((id.clone(), range));
564 if !self.branch_ids.insert(id.clone()) {
565 return;
566 }
567 let Some((line, column, _)) = self.location_source(range) else {
568 return;
569 };
570 self.manifest.branches.push(BranchMeta {
571 id: id.clone(),
572 kind: kind.into(),
573 file: self.file.into(),
574 line,
575 column,
576 source,
577 alternatives: alternatives
578 .into_iter()
579 .map(|(suffix, label)| BranchAlternativeMeta {
580 id: format!("{id}:{suffix}"),
581 label: label.into(),
582 })
583 .collect(),
584 });
585 }
586
587 fn decline_unreachable_obligations(&mut self, root: &ra_ap_syntax::SyntaxNode) {
592 let unreachable = root
595 .descendants()
596 .filter(|node| {
597 (ast::Fn::cast(node.clone())
598 .is_some_and(|function| function.const_token().is_some())
599 || ast::BlockExpr::cast(node.clone())
600 .is_some_and(|block| block.const_token().is_some())
601 || ast::Const::can_cast(node.kind())
602 || ast::Static::can_cast(node.kind()))
603 || ast::Impl::can_cast(node.kind()) && in_global_allocator_impl(node)
604 })
605 .map(|node| node.text_range())
606 .collect::<Vec<_>>();
607 if unreachable.is_empty() {
608 return;
609 }
610 let mut declined = self
611 .manifest
612 .unmeasured
613 .iter()
614 .cloned()
615 .collect::<BTreeSet<_>>();
616 for (id, range) in &self.obligation_ranges {
617 if unreachable
618 .iter()
619 .any(|region| region.contains_range(*range))
620 {
621 declined.insert(id.clone());
622 }
623 }
624 self.manifest.unmeasured = declined.into_iter().collect();
625 }
626
627 fn site_limitation(&mut self, id: &'static str, range: TextRange, reason: String) {
633 if self.site_limitations.contains(id) {
634 return;
635 }
636 let Some((line, column, source)) = self.location_source(range) else {
637 return;
638 };
639 self.site_limitations.insert(id);
640 let source = source.lines().next().unwrap_or("").trim();
643 let source = if source.chars().count() > 120 {
644 format!("{}...", source.chars().take(117).collect::<String>())
645 } else {
646 source.to_owned()
647 };
648 self.manifest.limitations.push(json!({
649 "id": limitation_id(id, self.file),
650 "kind": "source-scope",
651 "file": self.file,
652 "line": line,
653 "column": column,
654 "source": source,
655 "blocking": false,
659 "reason": reason
660 }));
661 }
662
663 fn collect(
664 mut self,
665 file: &SourceFile,
666 assertions: &[TextRange],
667 matches: &[TextRange],
668 ) -> Result<CoverageManifest, RustInstrumenterError> {
669 let root = file.syntax();
670
671 for list in root.descendants().filter_map(ast::StmtList::cast) {
672 for statement in list.statements() {
673 match statement {
674 ast::Stmt::ExprStmt(statement) => {
675 self.point(statement.syntax().text_range(), PointKind::Statement, None);
676 }
677 ast::Stmt::LetStmt(statement) => {
678 self.point(statement.syntax().text_range(), PointKind::Statement, None);
679 }
680 ast::Stmt::Item(_) => {}
681 }
682 }
683 if let Some(tail) = list.tail_expr() {
684 self.point(tail.syntax().text_range(), PointKind::Statement, None);
685 }
686 }
687
688 for function in root.descendants().filter_map(ast::Fn::cast) {
689 if function.body().is_none() {
690 continue;
691 }
692 if function.const_token().is_some() {
693 self.site_limitation(
694 "rust-const-context-not-instrumented",
695 function.syntax().text_range(),
696 "Runtime probes cannot execute in const fn or compile-time evaluation".into(),
697 );
698 continue;
699 }
700 let label = function.name().map(|name| name.text().to_string());
701 self.point(function.syntax().text_range(), PointKind::Function, label);
702 }
703
704 for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
705 self.point(
706 closure.syntax().text_range(),
707 PointKind::Function,
708 Some("<closure>".into()),
709 );
710 }
711
712 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
713 if let Some(condition) = expression.condition() {
714 self.decision(&condition, "if");
715 }
716 }
717 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
718 if let Some(condition) = expression.condition() {
719 self.decision(&condition, "while");
720 }
721 self.branch(
722 expression.syntax().text_range(),
723 "while-loop",
724 [("zero", "zero iterations"), ("entered", "entered")],
725 );
726 }
727 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
728 if let Some(condition) = guard.condition() {
729 self.decision(&condition, "match-guard");
730 }
731 }
732 for arguments in assertions {
733 if let Some(condition) = assertion_condition(root, *arguments) {
734 self.decision(&condition, "assert");
735 }
736 }
737 for expression in standalone_matches(root, matches, assertions) {
738 self.decision(&expression, "matches");
739 }
740
741 for binary in root.descendants().filter_map(ast::BinExpr::cast) {
742 let kind = match binary.op_kind() {
743 Some(BinaryOp::LogicOp(LogicOp::And)) => "logical-and",
744 Some(BinaryOp::LogicOp(LogicOp::Or)) => "logical-or",
745 _ => continue,
746 };
747 let range = binary.rhs().map_or_else(
748 || binary.syntax().text_range(),
749 |right| right.syntax().text_range(),
750 );
751 self.branch(
752 range,
753 kind,
754 [
755 ("short-circuit", "short-circuited"),
756 ("evaluated", "right operand evaluated"),
757 ],
758 );
759 }
760
761 for expression in root.descendants().filter_map(ast::ForExpr::cast) {
762 self.branch(
763 expression.syntax().text_range(),
764 "for-loop",
765 [("zero", "zero iterations"), ("entered", "entered")],
766 );
767 }
768 for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
769 let Some(list) = expression.match_arm_list() else {
770 continue;
771 };
772 let arms = list.arms().collect::<Vec<_>>();
773 let last = arms.len().saturating_sub(1);
774 for (index, arm) in arms.iter().enumerate() {
775 let range = arm.syntax().text_range();
776 if index == last {
777 self.branch(range, "match-arm", [("selected", "selected")]);
781 } else {
782 self.branch(
783 range,
784 "match-arm",
785 [("missed", "not selected"), ("selected", "selected")],
786 );
787 }
788 }
789 }
790 for expression in root.descendants().filter_map(ast::TryExpr::cast) {
791 self.branch(
792 expression.syntax().text_range(),
793 "try-operator",
794 [("continued", "continued"), ("returned", "early return")],
795 );
796 }
797
798 let mut macro_names = BTreeSet::new();
804 let mut first_macro = None;
805 let mut macro_sites = 0;
806 for call in root.descendants().filter_map(ast::MacroCall::cast) {
807 macro_names.insert(
808 call.path()
809 .map(|path| path.syntax().text().to_string())
810 .unwrap_or_else(|| "?".into()),
811 );
812 first_macro.get_or_insert_with(|| call.syntax().text_range());
813 macro_sites += 1;
814 }
815 if let Some(range) = first_macro {
816 let named = macro_names
817 .iter()
818 .take(4)
819 .map(|name| format!("`{name}!`"))
820 .collect::<Vec<_>>()
821 .join(", ");
822 let rest = macro_names.len().saturating_sub(4);
823 self.site_limitation(
824 "rust-macro-expansion-not-instrumented",
825 range,
826 format!(
827 "{named}{} expand in the compiler: their arguments and expansions are outside the owned source denominator ({macro_sites} call site{} in this file)",
828 if rest == 0 {
829 String::new()
830 } else {
831 format!(" and {rest} other macro{}", if rest == 1 { "" } else { "s" })
832 },
833 if macro_sites == 1 { "" } else { "s" }
834 ),
835 );
836 }
837
838 let bears_obligation = |node: &ra_ap_syntax::SyntaxNode| {
843 ast::StmtList::cast(node.clone()).is_some_and(|list| {
844 list.statements().next().is_some() || list.tail_expr().is_some()
845 }) || ast::IfExpr::can_cast(node.kind())
846 || ast::WhileExpr::can_cast(node.kind())
847 || ast::MatchGuard::can_cast(node.kind())
848 || ast::ForExpr::can_cast(node.kind())
849 || ast::MatchArm::can_cast(node.kind())
850 || ast::TryExpr::can_cast(node.kind())
851 || ast::ClosureExpr::can_cast(node.kind())
852 || ast::BinExpr::cast(node.clone()).is_some_and(|binary| {
853 matches!(
854 binary.op_kind(),
855 Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))
856 )
857 })
858 };
859 if let Some(node) = root
860 .descendants()
861 .find(|node| bears_obligation(node) && in_const_context(node))
862 {
863 self.site_limitation(
864 "rust-const-context-not-instrumented",
865 node.text_range(),
866 "Runtime probes cannot execute in const fn or compile-time evaluation; this and any later const context in the file stay declared".into(),
867 );
868 }
869 if let Some(node) = root
870 .descendants()
871 .find(|node| bears_obligation(node) && in_global_allocator(node))
872 {
873 self.site_limitation(
874 "rust-global-allocator-not-instrumented",
875 node.text_range(),
876 "Probing a GlobalAlloc implementation recurses into itself, because the runtime allocates".into(),
877 );
878 }
879
880 self.decline_unreachable_obligations(root);
881
882 if let Some(error) = self.error {
883 return Err(error);
884 }
885 self.manifest
886 .decisions
887 .sort_by(|left, right| left.id.cmp(&right.id));
888 self.manifest
889 .points
890 .sort_by(|left, right| left.id.cmp(&right.id));
891 self.manifest
892 .branches
893 .sort_by(|left, right| left.id.cmp(&right.id));
894 self.manifest.limitations.sort_by(|left, right| {
895 left.get("id")
896 .and_then(|value| value.as_str())
897 .cmp(&right.get("id").and_then(|value| value.as_str()))
898 });
899 Ok(self.manifest)
900 }
901}
902
903pub const FAILED_TRANSFORM_DUMP_ENV: &str = "SUPERCOV_RUST_DUMP_FAILED_INSTRUMENTATION";
906
907const EXPRESSION_MACROS: &[&str] = &[
916 "assert",
917 "debug_assert",
918 "assert_eq",
919 "assert_ne",
920 "debug_assert_eq",
921 "debug_assert_ne",
922 "println",
923 "print",
924 "eprintln",
925 "eprint",
926 "format",
927 "format_args",
928 "write",
929 "writeln",
930 "panic",
931 "unreachable",
932 "todo",
933 "unimplemented",
934 "vec",
935 "dbg",
936 "matches",
937];
938
939const ASSERTION_MACROS: &[&str] = &["assert", "debug_assert"];
941
942const ASSERTION_STATEMENT_MACROS: &[&str] = &[
945 "assert",
946 "assert_eq",
947 "assert_ne",
948 "debug_assert",
949 "debug_assert_eq",
950 "debug_assert_ne",
951];
952
953struct ExpressionView {
957 text: String,
958 assertions: Vec<TextRange>,
959 assertion_calls: Vec<TextRange>,
962 matches: Vec<TextRange>,
964}
965
966const EDITIONS: [Edition; 4] = [
972 Edition::Edition2024,
973 Edition::Edition2021,
974 Edition::Edition2018,
975 Edition::Edition2015,
976];
977
978fn parse_any_edition(source: &str) -> Result<(SourceFile, Edition), Vec<String>> {
979 let mut newest_errors = None;
980 for edition in EDITIONS {
981 let parsed = SourceFile::parse(source, edition);
982 let errors = parsed.errors();
983 if errors.is_empty() {
984 return Ok((parsed.tree(), edition));
985 }
986 newest_errors.get_or_insert_with(|| {
987 errors
988 .into_iter()
989 .map(|error| error.to_string())
990 .collect::<Vec<_>>()
991 });
992 }
993 Err(newest_errors.unwrap_or_default())
994}
995
996fn expression_view(source: &str, edition: Edition) -> ExpressionView {
997 let mut text = source.to_owned();
998 let mut assertions = Vec::new();
999 let mut assertion_calls = Vec::new();
1000 let mut matches = Vec::new();
1001 for _ in 0..16 {
1004 let tree = SourceFile::parse(&text, edition).tree();
1005 let Some(next) = rewrite_expression_macros(
1006 &text,
1007 &tree,
1008 edition,
1009 &mut assertions,
1010 &mut assertion_calls,
1011 &mut matches,
1012 ) else {
1013 break;
1014 };
1015 text = next;
1016 }
1017 ExpressionView {
1018 text,
1019 assertions,
1020 assertion_calls,
1021 matches,
1022 }
1023}
1024
1025fn is_statement_macro(call: &ast::MacroCall) -> bool {
1028 call.syntax().parent().is_some_and(|parent| {
1029 ast::MacroExpr::can_cast(parent.kind())
1030 && parent.parent().is_some_and(|grandparent| {
1031 ast::ExprStmt::can_cast(grandparent.kind())
1032 || ast::StmtList::can_cast(grandparent.kind())
1033 })
1034 })
1035}
1036
1037fn first_top_level_comma_end(arguments: &ast::TokenTree) -> Option<usize> {
1039 arguments
1040 .syntax()
1041 .children_with_tokens()
1042 .filter_map(|element| element.into_token())
1043 .find(|token| token.kind() == SyntaxKind::COMMA)
1044 .map(|token| usize::from(token.text_range().end()))
1045}
1046
1047fn rewrite_expression_macros(
1050 source: &str,
1051 tree: &SourceFile,
1052 edition: Edition,
1053 assertions: &mut Vec<TextRange>,
1054 assertion_calls: &mut Vec<TextRange>,
1055 matches: &mut Vec<TextRange>,
1056) -> Option<String> {
1057 let mut text = source.as_bytes().to_vec();
1058 let mut changed = false;
1059 for call in tree.syntax().descendants().filter_map(ast::MacroCall::cast) {
1060 let Some(name) = call
1061 .path()
1062 .and_then(|path| path.segment())
1063 .and_then(|segment| segment.name_ref())
1064 .map(|name| name.text().to_string())
1065 else {
1066 continue;
1067 };
1068 if !EXPRESSION_MACROS.contains(&name.as_str()) {
1069 continue;
1070 }
1071 let (Some(bang), Some(arguments)) = (call.excl_token(), call.token_tree()) else {
1072 continue;
1073 };
1074 let parenthesised = arguments.l_paren_token().is_some();
1075 if !parenthesised && arguments.l_brack_token().is_none() {
1076 continue;
1077 }
1078 let range = arguments.syntax().text_range();
1079 let (start, end) = (usize::from(range.start()), usize::from(range.end()));
1080 let mut rewritten = source.as_bytes()[start..end].to_vec();
1081 if !parenthesised {
1082 rewritten[0] = b'(';
1083 *rewritten
1084 .last_mut()
1085 .expect("a token tree has a closing delimiter") = b')';
1086 }
1087 if name == "matches" {
1088 let Some(comma_end) = first_top_level_comma_end(&arguments) else {
1091 continue;
1092 };
1093 for byte in &mut rewritten[comma_end - start..end - start - 1] {
1094 if *byte != b'\n' {
1095 *byte = b' ';
1096 }
1097 }
1098 }
1099 let probe = format!(
1101 "fn __supercov() {{ let _ = __f{}; }}",
1102 String::from_utf8_lossy(&rewritten)
1103 );
1104 if !SourceFile::parse(&probe, edition).errors().is_empty() {
1105 if !parenthesised && !is_statement_macro(&call) {
1109 let array = source.as_bytes()[start..end].to_vec();
1110 let array_probe = format!(
1111 "fn __supercov() {{ let _ = {}; }}",
1112 String::from_utf8_lossy(&array)
1113 );
1114 if SourceFile::parse(&array_probe, edition).errors().is_empty() {
1115 let prefix_start = usize::from(call.syntax().text_range().start());
1116 let prefix_start = call.attrs().last().map_or(prefix_start, |attribute| {
1117 usize::from(attribute.syntax().text_range().end())
1118 });
1119 for byte in &mut text[prefix_start..start] {
1120 if *byte != b'\n' {
1121 *byte = b' ';
1122 }
1123 }
1124 changed = true;
1125 }
1126 }
1127 continue;
1128 }
1129 text[usize::from(bang.text_range().start())] = b'_';
1130 text[start..end].copy_from_slice(&rewritten);
1131 changed = true;
1132 if ASSERTION_MACROS.contains(&name.as_str()) {
1133 assertions.push(range);
1134 }
1135 if ASSERTION_STATEMENT_MACROS.contains(&name.as_str()) {
1136 assertion_calls.push(call.syntax().text_range());
1137 }
1138 if name == "matches" {
1139 matches.push(call.syntax().text_range());
1140 }
1141 }
1142 changed.then(|| String::from_utf8(text).expect("rewriting ASCII keeps the source UTF-8"))
1143}
1144
1145fn assertion_argument_count(root: &ra_ap_syntax::SyntaxNode, arguments: TextRange) -> usize {
1148 root.descendants()
1149 .find(|node| node.text_range() == arguments && ast::ArgList::can_cast(node.kind()))
1150 .and_then(ast::ArgList::cast)
1151 .map_or(0, |list| list.args().count())
1152}
1153
1154fn assertion_condition(root: &ra_ap_syntax::SyntaxNode, arguments: TextRange) -> Option<ast::Expr> {
1157 root.descendants()
1158 .find(|node| node.text_range() == arguments && ast::ArgList::can_cast(node.kind()))
1159 .and_then(ast::ArgList::cast)?
1160 .args()
1161 .next()
1162}
1163
1164struct ParsedSource {
1169 tree: SourceFile,
1170 assertions: Vec<TextRange>,
1171 assertion_calls: Vec<TextRange>,
1172 matches: Vec<TextRange>,
1173 edition: Edition,
1174}
1175
1176fn parse_for_instrumentation(source: &str) -> Result<ParsedSource, RustInstrumenterError> {
1177 if source.len() > u32::MAX as usize {
1178 return Err(RustInstrumenterError::SourceTooLarge);
1179 }
1180 let (tree, edition) = parse_any_edition(source).map_err(RustInstrumenterError::Parse)?;
1181 let view = expression_view(source, edition);
1182 let parsed_view = SourceFile::parse(&view.text, edition);
1183 if parsed_view.errors().is_empty() {
1184 Ok(ParsedSource {
1185 tree: parsed_view.tree(),
1186 assertions: view.assertions,
1187 assertion_calls: view.assertion_calls,
1188 matches: view.matches,
1189 edition,
1190 })
1191 } else {
1192 Ok(ParsedSource {
1193 tree,
1194 assertions: Vec::new(),
1195 assertion_calls: Vec::new(),
1196 matches: Vec::new(),
1197 edition,
1198 })
1199 }
1200}
1201
1202fn standalone_matches(
1206 root: &ra_ap_syntax::SyntaxNode,
1207 matches: &[TextRange],
1208 assertions: &[TextRange],
1209) -> Vec<ast::Expr> {
1210 let mut atoms = Vec::new();
1211 let mut conditions = Vec::new();
1212 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
1213 conditions.extend(expression.condition());
1214 }
1215 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
1216 conditions.extend(expression.condition());
1217 }
1218 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
1219 conditions.extend(guard.condition());
1220 }
1221 for arguments in assertions {
1222 conditions.extend(assertion_condition(root, *arguments));
1223 }
1224 for condition in &conditions {
1225 RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
1226 }
1227 matches
1228 .iter()
1229 .filter(|range| !atoms.contains(range))
1230 .filter_map(|range| {
1231 root.descendants()
1232 .find(|node| node.text_range() == *range && ast::CallExpr::can_cast(node.kind()))
1233 .and_then(ast::Expr::cast)
1234 })
1235 .collect()
1236}
1237
1238pub fn build_rust_manifest(
1239 file: &str,
1240 source: &str,
1241) -> Result<CoverageManifest, RustInstrumenterError> {
1242 let parsed = parse_for_instrumentation(source)?;
1243 RustObligationCollector::new(file, source).collect(
1244 &parsed.tree,
1245 &parsed.assertions,
1246 &parsed.matches,
1247 )
1248}
1249
1250fn block_entry_offset(block: &ast::BlockExpr) -> Option<usize> {
1251 let list = block.stmt_list()?;
1252 list.attrs()
1253 .last()
1254 .map(|attribute| usize::from(attribute.syntax().text_range().end()))
1255 .or_else(|| {
1256 list.l_curly_token()
1257 .map(|token| usize::from(token.text_range().end()))
1258 })
1259}
1260
1261fn range_after_attributes(node: &impl HasAttrs) -> TextRange {
1264 let range = node.syntax().text_range();
1265 node.attrs().last().map_or(range, |attribute| {
1266 TextRange::new(attribute.syntax().text_range().end(), range.end())
1267 })
1268}
1269
1270fn has_let(expression: &ast::Expr) -> bool {
1271 expression
1272 .syntax()
1273 .descendants()
1274 .any(|node| ast::LetExpr::can_cast(node.kind()))
1275}
1276
1277enum ChainHost<'a> {
1279 If(&'a ast::IfExpr),
1280 While(&'a ast::WhileExpr),
1281}
1282
1283fn allocate_chain_table_name(
1285 file: &str,
1286 condition: &ast::Expr,
1287 identifiers: &mut BTreeSet<String>,
1288) -> String {
1289 let id = stable_id(file, "chain", condition.syntax().text_range(), "operators");
1290 let suffix = id
1291 .rsplit(':')
1292 .next()
1293 .unwrap_or("chain")
1294 .to_ascii_uppercase();
1295 let base = format!("__SUPERCOV_CHAIN_{suffix}");
1296 let mut candidate = base.clone();
1297 let mut attempt = 0_usize;
1298 while !identifiers.insert(candidate.clone()) {
1299 attempt += 1;
1300 candidate = format!("{base}_{attempt}");
1301 }
1302 candidate
1303}
1304
1305fn allocate_identifier(
1307 file: &str,
1308 range: TextRange,
1309 kind: &str,
1310 identifiers: &mut BTreeSet<String>,
1311) -> String {
1312 let id = stable_id(file, kind, range, "");
1313 let suffix = id.rsplit(':').next().unwrap_or(kind);
1314 let base = format!("__supercov_{kind}_{suffix}");
1315 let mut candidate = base.clone();
1316 let mut attempt = 0_usize;
1317 while !identifiers.insert(candidate.clone()) {
1318 attempt += 1;
1319 candidate = format!("{base}_{attempt}");
1320 }
1321 candidate
1322}
1323
1324fn own_breaks(body: &ast::BlockExpr, label: Option<ast::Label>) -> Vec<TextRange> {
1328 let own_label = label
1329 .and_then(|label| label.lifetime())
1330 .map(|lifetime| lifetime.text().to_string());
1331 body.syntax()
1332 .descendants()
1333 .filter_map(ast::BreakExpr::cast)
1334 .filter(|expression| match expression.lifetime() {
1335 Some(lifetime) => own_label.as_deref() == Some(lifetime.text().to_string().as_str()),
1336 None => !expression
1337 .syntax()
1338 .ancestors()
1339 .skip(1)
1340 .take_while(|ancestor| ancestor != body.syntax())
1341 .any(|ancestor| {
1342 ast::LoopExpr::can_cast(ancestor.kind())
1343 || ast::WhileExpr::can_cast(ancestor.kind())
1344 || ast::ForExpr::can_cast(ancestor.kind())
1345 || ast::ClosureExpr::can_cast(ancestor.kind())
1346 }),
1347 })
1348 .map(|expression| expression.syntax().text_range())
1349 .collect()
1350}
1351
1352fn instrument_let_chain(
1368 insertions: &mut Vec<Insertion>,
1369 runtime_path: &str,
1370 file: &str,
1371 condition: &ast::Expr,
1372 host: ChainHost<'_>,
1373 identifiers: &mut BTreeSet<String>,
1374) {
1375 let (kind, host_range, body, label) = match &host {
1378 ChainHost::If(expression) => (
1379 "if",
1380 range_after_attributes(*expression),
1381 expression.then_branch(),
1382 None,
1383 ),
1384 ChainHost::While(expression) => (
1385 "while",
1386 range_after_attributes(*expression),
1387 expression.loop_body(),
1388 expression.label(),
1389 ),
1390 };
1391 let Some(body) = body else {
1392 return;
1393 };
1394 let Some(body_offset) = block_entry_offset(&body) else {
1395 return;
1396 };
1397 let range = condition.syntax().text_range();
1398 let id = stable_id(file, "decision", range, kind);
1399 let mut atoms = Vec::new();
1400 RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
1401 let lets = condition
1402 .syntax()
1403 .descendants()
1404 .filter_map(ast::LetExpr::cast)
1405 .map(|expression| expression.syntax().text_range())
1406 .collect::<Vec<_>>();
1407 let frame = allocate_frame_name(file, condition, kind, identifiers);
1408 let table = allocate_chain_table_name(file, condition, identifiers);
1409 let single_let = atoms.len() == 1;
1410 let broke = match &host {
1411 ChainHost::While(_) if single_let => {
1412 Some(allocate_identifier(file, range, "broke", identifiers))
1413 }
1414 _ => None,
1415 };
1416
1417 let mut operators = Vec::new();
1418 for binary in condition
1419 .syntax()
1420 .descendants()
1421 .filter_map(ast::BinExpr::cast)
1422 {
1423 if !matches!(binary.op_kind(), Some(BinaryOp::LogicOp(LogicOp::And))) {
1426 continue;
1427 }
1428 let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
1429 continue;
1430 };
1431 if !has_let(&left) {
1432 continue;
1433 }
1434 let branch = stable_id(file, "branch", right.syntax().text_range(), "logical-and");
1435 let right_range = right.syntax().text_range();
1436 let Some(first) = atoms
1437 .iter()
1438 .position(|atom| right_range.contains_range(*atom))
1439 else {
1440 continue;
1441 };
1442 operators.push(format!(
1443 "({first}, {:?}, {:?})",
1444 format!("{branch}:short-circuit"),
1445 format!("{branch}:evaluated")
1446 ));
1447 }
1448
1449 let mark = format!("{runtime_path}::reached(&mut {frame}, 0);");
1450 let record_false = format!("{runtime_path}::decision_chain(&mut {frame}, false, {table});");
1451 let mut prefix = format!(
1452 "{{ const {table}: &[(usize, &str, &str)] = &[{}]; let mut {frame} = {runtime_path}::DecisionFrame::new({id:?}, {}); ",
1453 operators.join(", "),
1454 atoms.len()
1455 );
1456 if single_let {
1457 prefix.push_str(&mark);
1458 prefix.push(' ');
1459 }
1460 if let Some(broke) = &broke {
1461 prefix.push_str(&format!("let mut {broke} = false; "));
1462 }
1463 let suffix = match &host {
1464 ChainHost::If(expression) => match expression.else_branch() {
1465 Some(_) => " }".to_owned(),
1466 None => format!(" else {{ {record_false} }} }}"),
1467 },
1468 ChainHost::While(_) => match &broke {
1469 Some(broke) => format!(" if !{broke} {{ {mark} {record_false} }} }}"),
1470 None => format!(" {record_false} }}"),
1471 },
1472 };
1473 push_wrapper(insertions, host_range, host_range, 1, prefix, suffix);
1474 if !single_let {
1475 push_direct(
1476 insertions,
1477 usize::from(range.start()),
1478 format!("{runtime_path}::reached(&mut {frame}, 0) && "),
1479 );
1480 }
1481 for (index, atom) in atoms.iter().enumerate() {
1482 if lets.contains(atom) {
1483 if index > 0 {
1484 push_direct(
1485 insertions,
1486 usize::from(atom.start()),
1487 format!("{runtime_path}::reached(&mut {frame}, {index}) && "),
1488 );
1489 }
1490 } else {
1491 push_wrapper(
1492 insertions,
1493 *atom,
1494 *atom,
1495 1,
1496 format!("{runtime_path}::condition(("),
1497 format!("), &mut {frame}, {index})"),
1498 );
1499 }
1500 }
1501 let mut entry = String::new();
1502 if broke.is_some() {
1503 entry.push_str(&format!("\n{mark}"));
1504 }
1505 entry.push_str(&format!(
1506 "\n{runtime_path}::decision_chain(&mut {frame}, true, {table});"
1507 ));
1508 push_direct(insertions, body_offset, entry);
1509 if let Some(broke) = &broke {
1510 for break_range in own_breaks(&body, label) {
1511 push_wrapper(
1512 insertions,
1513 break_range,
1514 break_range,
1515 0,
1516 format!("{{ {broke} = true; "),
1517 " }".into(),
1518 );
1519 }
1520 }
1521 if let ChainHost::If(expression) = &host {
1522 match expression.else_branch() {
1523 Some(ast::ElseBranch::Block(block)) => {
1524 if let Some(offset) = block_entry_offset(&block) {
1525 push_direct(insertions, offset, format!("\n{record_false}"));
1526 }
1527 }
1528 Some(ast::ElseBranch::IfExpr(nested)) => {
1529 let nested_range = nested.syntax().text_range();
1530 push_wrapper(
1531 insertions,
1532 nested_range,
1533 nested_range,
1534 0,
1535 format!("{{ {record_false} "),
1536 " }".into(),
1537 );
1538 }
1539 None => {}
1540 }
1541 }
1542}
1543
1544fn enclosing_block_entry(node: &ra_ap_syntax::SyntaxNode) -> Option<usize> {
1548 node.ancestors()
1549 .skip(1)
1550 .find_map(ast::BlockExpr::cast)
1551 .and_then(|block| block_entry_offset(&block))
1552}
1553
1554fn plain_block(block: &ast::BlockExpr) -> bool {
1558 block
1559 .syntax()
1560 .first_token()
1561 .is_some_and(|token| token.kind() == SyntaxKind::L_CURLY)
1562}
1563
1564fn instrument_decision(
1565 insertions: &mut Vec<Insertion>,
1566 runtime_path: &str,
1567 file: &str,
1568 condition: &ast::Expr,
1569 kind: &str,
1570 frame_name: &str,
1571) -> bool {
1572 if cannot_carry_probe(condition.syntax())
1573 || condition
1574 .syntax()
1575 .descendants()
1576 .any(|node| ast::LetExpr::can_cast(node.kind()))
1577 {
1578 return false;
1579 }
1580 let range = condition.syntax().text_range();
1581 let id = stable_id(file, "decision", range, kind);
1582 let mut condition_ranges = Vec::new();
1583 RustObligationCollector::atomic_condition_ranges(condition, &mut condition_ranges);
1584 push_wrapper(
1585 insertions,
1586 range,
1587 range,
1588 0,
1589 format!(
1590 "({{ let mut {frame_name} = {runtime_path}::DecisionFrame::new({id:?}, {}); {runtime_path}::decision((",
1591 condition_ranges.len()
1592 ),
1593 format!("), &mut {frame_name}) }})"),
1594 );
1595 for (index, atomic_range) in condition_ranges.into_iter().enumerate() {
1599 push_wrapper(
1600 insertions,
1601 atomic_range,
1602 atomic_range,
1603 1,
1604 format!("{runtime_path}::condition(("),
1605 format!("), &mut {frame_name}, {index})"),
1606 );
1607 }
1608 true
1609}
1610
1611pub fn instrument_rust_source(
1621 file: &str,
1622 source: &str,
1623 runtime_path: &str,
1624) -> Result<RustInstrumentedSource, RustInstrumenterError> {
1625 if !valid_runtime_path(runtime_path) {
1626 return Err(RustInstrumenterError::InvalidRuntimePath);
1627 }
1628 let parsed = parse_for_instrumentation(source)?;
1634 let mut manifest = RustObligationCollector::new(file, source).collect(
1635 &parsed.tree,
1636 &parsed.assertions,
1637 &parsed.matches,
1638 )?;
1639 let ParsedSource {
1640 tree,
1641 assertions,
1642 assertion_calls,
1643 matches,
1644 edition,
1645 } = parsed;
1646 let root = tree.syntax();
1647 let mut insertions = Vec::new();
1648 let mut identifiers = root
1649 .descendants_with_tokens()
1650 .filter_map(|element| element.into_token())
1651 .filter(|token| token.kind() == SyntaxKind::IDENT)
1652 .map(|token| token.text().to_string())
1653 .collect::<BTreeSet<_>>();
1654
1655 let mut skipped_attributed_statement = false;
1656 let attributed_probe = |insertions: &mut Vec<Insertion>,
1667 skipped: &mut bool,
1668 expression: Option<ast::Expr>,
1669 has_attrs: bool,
1670 statement: bool,
1674 trailing: bool,
1675 range: TextRange,
1676 id: String| {
1677 if !has_attrs {
1678 push_direct(
1679 insertions,
1680 usize::from(range.start()),
1681 format!("{runtime_path}::hit({id:?});"),
1682 );
1683 return;
1684 }
1685 let Some(expression) = expression else {
1686 *skipped = true;
1687 return;
1688 };
1689 if let ast::Expr::BlockExpr(block) = &expression
1690 && let Some(offset) = block_entry_offset(block)
1691 {
1692 push_direct(
1693 insertions,
1694 offset,
1695 format!("\n{runtime_path}::hit({id:?});"),
1696 );
1697 return;
1698 }
1699 if trailing && let ast::Expr::MacroExpr(_) = &expression {
1710 if expression.attrs().any(|attribute| {
1711 attribute.meta().is_some_and(|meta| {
1714 let text = meta.syntax().text().to_string();
1715 let name = text
1716 .chars()
1717 .take_while(|character| character.is_alphanumeric() || *character == '_')
1718 .collect::<String>();
1719 matches!(name.as_str(), "cfg" | "cfg_attr")
1720 })
1721 }) {
1722 *skipped = true;
1723 return;
1724 }
1725 let range = expression.syntax().text_range();
1726 push_wrapper(
1727 insertions,
1728 range,
1729 range,
1730 0,
1731 format!("{{ {runtime_path}::hit({id:?}); "),
1732 " }".into(),
1733 );
1734 return;
1735 }
1736 if statement && let ast::Expr::MacroExpr(_) = &expression {
1743 let start = expression.attrs().last().map_or_else(
1744 || expression.syntax().text_range().start(),
1745 |attribute| attribute.syntax().text_range().end(),
1746 );
1747 let wrapped = TextRange::new(start, expression.syntax().text_range().end());
1748 push_wrapper(
1749 insertions,
1750 wrapped,
1751 wrapped,
1752 0,
1753 format!(" {{ {runtime_path}::hit({id:?}); "),
1754 "; }".into(),
1755 );
1756 return;
1757 }
1758 let start = expression.attrs().last().map_or_else(
1764 || expression.syntax().text_range().start(),
1765 |attribute| attribute.syntax().text_range().end(),
1766 );
1767 let wrapped = TextRange::new(start, expression.syntax().text_range().end());
1768 push_wrapper(
1769 insertions,
1770 wrapped,
1771 wrapped,
1772 0,
1773 format!(" {{ {runtime_path}::hit({id:?}); ("),
1774 ") }".into(),
1775 );
1776 };
1777 for list in root.descendants().filter_map(ast::StmtList::cast) {
1778 let last_statement = list.statements().last();
1779 for statement in list.statements() {
1780 let (range, expression, has_attrs, statement_expression, trailing) = match &statement {
1781 ast::Stmt::ExprStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
1782 let expression = statement.expr();
1783 let has_attrs = expression
1786 .as_ref()
1787 .is_some_and(|expression| expression.attrs().next().is_some());
1788 let trailing = statement.semicolon_token().is_none()
1791 && list.tail_expr().is_none()
1792 && last_statement.as_ref() == Some(&ast::Stmt::ExprStmt(statement.clone()));
1793 (
1794 statement.syntax().text_range(),
1795 expression,
1796 has_attrs,
1797 true,
1798 trailing,
1799 )
1800 }
1801 ast::Stmt::LetStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
1802 let has_attrs = statement.attrs().next().is_some();
1803 let initializer = has_attrs.then(|| statement.initializer()).flatten();
1806 (
1807 statement.syntax().text_range(),
1808 initializer,
1809 has_attrs,
1810 false,
1811 false,
1812 )
1813 }
1814 _ => continue,
1815 };
1816 let id = stable_id(file, "statement", range, "");
1817 if !has_attrs
1824 && !trailing
1825 && assertion_calls
1826 .iter()
1827 .any(|call| range.contains_range(*call))
1828 {
1829 push_direct(
1830 &mut insertions,
1831 usize::from(range.end()),
1832 format!("{runtime_path}::assertion({id:?});"),
1833 );
1834 }
1835 attributed_probe(
1836 &mut insertions,
1837 &mut skipped_attributed_statement,
1838 expression,
1839 has_attrs,
1840 statement_expression,
1841 trailing,
1842 range,
1843 id,
1844 );
1845 }
1846 if let Some(tail) = list
1847 .tail_expr()
1848 .filter(|tail| !cannot_carry_probe(tail.syntax()))
1849 {
1850 let range = tail.syntax().text_range();
1851 let id = stable_id(file, "statement", range, "");
1852 let has_attrs = tail.attrs().next().is_some();
1853 attributed_probe(
1854 &mut insertions,
1855 &mut skipped_attributed_statement,
1856 Some(tail),
1857 has_attrs,
1858 true,
1859 true,
1860 range,
1861 id,
1862 );
1863 }
1864 }
1865
1866 for function in root.descendants().filter_map(ast::Fn::cast) {
1867 if cannot_carry_probe(function.syntax()) {
1870 continue;
1871 }
1872 let Some(body) = function.body() else {
1873 continue;
1874 };
1875 let label = function.name().map(|name| name.text().to_string());
1876 let id = stable_id(
1877 file,
1878 "function",
1879 function.syntax().text_range(),
1880 label.as_deref().unwrap_or(""),
1881 );
1882 if let Some(offset) = block_entry_offset(&body) {
1883 push_direct(
1884 &mut insertions,
1885 offset,
1886 format!("\n{runtime_path}::hit({id:?});"),
1887 );
1888 }
1889 }
1890
1891 for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
1892 let Some(body) = closure.body() else {
1893 continue;
1894 };
1895 if cannot_carry_probe(body.syntax()) {
1896 continue;
1897 }
1898 let id = stable_id(file, "function", closure.syntax().text_range(), "<closure>");
1899 if let ast::Expr::BlockExpr(block) = &body {
1900 if let Some(offset) = block_entry_offset(block) {
1901 push_direct(
1902 &mut insertions,
1903 offset,
1904 format!("\n{runtime_path}::hit({id:?});"),
1905 );
1906 }
1907 } else {
1908 let range = body.syntax().text_range();
1909 push_wrapper(
1910 &mut insertions,
1911 range,
1912 closure.syntax().text_range(),
1913 0,
1914 format!("{{ {runtime_path}::hit({id:?}); ("),
1915 ") }".into(),
1916 );
1917 }
1918 }
1919
1920 for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
1927 if cannot_carry_probe(expression.syntax()) {
1928 continue;
1929 }
1930 let Some(list) = expression.match_arm_list() else {
1931 continue;
1932 };
1933 let arms = list.arms().collect::<Vec<_>>();
1934 if arms.is_empty() {
1935 continue;
1936 }
1937 let Some(table_offset) = enclosing_block_entry(expression.syntax()) else {
1938 continue;
1939 };
1940 let table = allocate_table_name(file, &expression, &mut identifiers);
1941 let entries = arms
1942 .iter()
1943 .map(|arm| {
1944 let id = stable_id(file, "branch", arm.syntax().text_range(), "match-arm");
1945 format!(
1946 "{:?}, {:?}",
1947 format!("{id}:missed"),
1948 format!("{id}:selected")
1949 )
1950 })
1951 .collect::<Vec<_>>()
1952 .join(", ");
1953 push_direct(
1954 &mut insertions,
1955 table_offset,
1956 format!("\nconst {table}: &[&str] = &[{entries}];"),
1957 );
1958 for (index, arm) in arms.iter().enumerate() {
1959 let Some(body) = arm.expr() else {
1960 continue;
1961 };
1962 let call = format!("{runtime_path}::arms({table}, {index});");
1963 match &body {
1964 ast::Expr::BlockExpr(block) if plain_block(block) => {
1965 if let Some(offset) = block_entry_offset(block) {
1966 push_direct(&mut insertions, offset, format!("\n{call}"));
1967 }
1968 }
1969 _ => push_wrapper(
1970 &mut insertions,
1971 body.syntax().text_range(),
1972 arm.syntax().text_range(),
1973 0,
1974 format!("{{ {call} ("),
1975 ") }".into(),
1976 ),
1977 }
1978 }
1979 }
1980
1981 for binary in root.descendants().filter_map(ast::BinExpr::cast) {
1985 let short_circuits_when = match binary.op_kind() {
1986 Some(BinaryOp::LogicOp(LogicOp::And)) => false,
1987 Some(BinaryOp::LogicOp(LogicOp::Or)) => true,
1988 _ => continue,
1989 };
1990 if cannot_carry_probe(binary.syntax()) {
1991 continue;
1992 }
1993 let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
1994 continue;
1995 };
1996 if has_let(&left) {
2000 continue;
2001 }
2002 let kind = if short_circuits_when {
2003 "logical-or"
2004 } else {
2005 "logical-and"
2006 };
2007 let id = stable_id(file, "branch", right.syntax().text_range(), kind);
2008 push_wrapper(
2009 &mut insertions,
2010 left.syntax().text_range(),
2011 binary.syntax().text_range(),
2012 2,
2013 format!("{runtime_path}::logical(("),
2014 format!(
2015 "), {short_circuits_when}, {:?}, {:?})",
2016 format!("{id}:short-circuit"),
2017 format!("{id}:evaluated")
2018 ),
2019 );
2020 }
2021
2022 for expression in root.descendants().filter_map(ast::ForExpr::cast) {
2026 if cannot_carry_probe(expression.syntax()) {
2027 continue;
2028 }
2029 let Some(iterable) = expression.iterable() else {
2030 continue;
2031 };
2032 let id = stable_id(file, "branch", expression.syntax().text_range(), "for-loop");
2033 push_wrapper(
2037 &mut insertions,
2038 iterable.syntax().text_range(),
2039 iterable.syntax().text_range(),
2040 0,
2041 format!("{runtime_path}::for_loop(("),
2042 format!(
2043 "), {:?}, {:?})",
2044 format!("{id}:zero"),
2045 format!("{id}:entered")
2046 ),
2047 );
2048 }
2049
2050 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
2054 if cannot_carry_probe(expression.syntax()) {
2055 continue;
2056 }
2057 let Some(offset) = expression.loop_body().as_ref().and_then(block_entry_offset) else {
2058 continue;
2059 };
2060 let id = stable_id(
2061 file,
2062 "branch",
2063 expression.syntax().text_range(),
2064 "while-loop",
2065 );
2066 let flag = allocate_flag_name(file, &expression, &mut identifiers);
2067 let range = range_after_attributes(&expression);
2068 push_wrapper(
2069 &mut insertions,
2070 range,
2071 range,
2072 0,
2073 format!("{{ let mut {flag} = true; "),
2074 format!(
2075 " {runtime_path}::zero_iterations({flag}, {:?}) }}",
2076 format!("{id}:zero")
2077 ),
2078 );
2079 push_direct(
2080 &mut insertions,
2081 offset,
2082 format!(
2083 "\n{runtime_path}::entered(&mut {flag}, {:?});",
2084 format!("{id}:entered")
2085 ),
2086 );
2087 }
2088
2089 for expression in root.descendants().filter_map(ast::TryExpr::cast) {
2093 if cannot_carry_probe(expression.syntax()) {
2094 continue;
2095 }
2096 let Some(operand) = expression.expr() else {
2097 continue;
2098 };
2099 let id = stable_id(
2100 file,
2101 "branch",
2102 expression.syntax().text_range(),
2103 "try-operator",
2104 );
2105 push_wrapper(
2108 &mut insertions,
2109 operand.syntax().text_range(),
2110 operand.syntax().text_range(),
2111 0,
2112 format!("{runtime_path}::TryProbe::probe(("),
2113 format!(
2114 "), {:?}, {:?})",
2115 format!("{id}:continued"),
2116 format!("{id}:returned")
2117 ),
2118 );
2119 }
2120
2121 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
2122 let Some(condition) = expression.condition() else {
2123 continue;
2124 };
2125 if has_let(&condition) {
2126 if !cannot_carry_probe(condition.syntax()) {
2127 instrument_let_chain(
2128 &mut insertions,
2129 runtime_path,
2130 file,
2131 &condition,
2132 ChainHost::If(&expression),
2133 &mut identifiers,
2134 );
2135 }
2136 continue;
2137 }
2138 let frame_name = allocate_frame_name(file, &condition, "if", &mut identifiers);
2139 instrument_decision(
2140 &mut insertions,
2141 runtime_path,
2142 file,
2143 &condition,
2144 "if",
2145 &frame_name,
2146 );
2147 }
2148 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
2149 let Some(condition) = expression.condition() else {
2150 continue;
2151 };
2152 if has_let(&condition) {
2153 if !cannot_carry_probe(condition.syntax()) {
2154 instrument_let_chain(
2155 &mut insertions,
2156 runtime_path,
2157 file,
2158 &condition,
2159 ChainHost::While(&expression),
2160 &mut identifiers,
2161 );
2162 }
2163 continue;
2164 }
2165 let frame_name = allocate_frame_name(file, &condition, "while", &mut identifiers);
2166 instrument_decision(
2167 &mut insertions,
2168 runtime_path,
2169 file,
2170 &condition,
2171 "while",
2172 &frame_name,
2173 );
2174 }
2175 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
2176 if let Some(condition) = guard.condition() {
2177 let frame_name = allocate_frame_name(file, &condition, "match-guard", &mut identifiers);
2178 instrument_decision(
2179 &mut insertions,
2180 runtime_path,
2181 file,
2182 &condition,
2183 "match-guard",
2184 &frame_name,
2185 );
2186 }
2187 }
2188 for arguments in &assertions {
2195 let Some(condition) = assertion_condition(root, *arguments) else {
2196 continue;
2197 };
2198 let frame_name = allocate_frame_name(file, &condition, "assert", &mut identifiers);
2199 if !instrument_decision(
2200 &mut insertions,
2201 runtime_path,
2202 file,
2203 &condition,
2204 "assert",
2205 &frame_name,
2206 ) {
2207 continue;
2208 }
2209 if assertion_argument_count(root, *arguments) == 1 {
2210 let range = condition.syntax().text_range();
2211 let original = &source[usize::from(range.start())..usize::from(range.end())];
2212 push_direct(
2213 &mut insertions,
2214 usize::from(range.end()),
2215 format!(", \"assertion failed: {{}}\", stringify!({original})"),
2216 );
2217 }
2218 }
2219 for expression in standalone_matches(root, &matches, &assertions) {
2222 let frame_name = allocate_frame_name(file, &expression, "matches", &mut identifiers);
2223 instrument_decision(
2224 &mut insertions,
2225 runtime_path,
2226 file,
2227 &expression,
2228 "matches",
2229 &frame_name,
2230 );
2231 }
2232
2233 if skipped_attributed_statement {
2234 add_manifest_limitation(
2235 &mut manifest,
2236 file,
2237 "rust-attributed-statement-probes-not-injected",
2238 "A `let` without an initializer that carries outer attributes has no expression to hold a probe",
2239 );
2240 }
2241 manifest.limitations.sort_by(|left, right| {
2242 left.get("id")
2243 .and_then(|value| value.as_str())
2244 .cmp(&right.get("id").and_then(|value| value.as_str()))
2245 });
2246
2247 let code = apply_insertions(source, insertions)?;
2248 let transformed = SourceFile::parse(&code, edition);
2249 let errors = transformed
2250 .errors()
2251 .into_iter()
2252 .map(|error| error.to_string())
2253 .collect::<Vec<_>>();
2254 if !errors.is_empty() {
2255 if let Some(directory) = std::env::var_os(FAILED_TRANSFORM_DUMP_ENV) {
2259 let name = file.replace(['/', '\\'], "__");
2260 let _ = std::fs::create_dir_all(&directory);
2261 let _ = std::fs::write(std::path::Path::new(&directory).join(name), &code);
2262 }
2263 return Err(RustInstrumenterError::Parse(errors));
2264 }
2265 Ok(RustInstrumentedSource { code, manifest })
2266}
2267
2268#[cfg(test)]
2269mod tests {
2270 use std::{
2271 fs,
2272 process::Command,
2273 time::{SystemTime, UNIX_EPOCH},
2274 };
2275
2276 use super::*;
2277
2278 fn limitation_kind_of(limitation: &serde_json::Value) -> Option<&str> {
2280 limitation.get("id")?.as_str()?.split('#').next()
2281 }
2282
2283 const NOOP_RUNTIME: &str = r#"
2284#[doc(hidden)]
2285mod __supercov_runtime_v1 {
2286 pub struct DecisionFrame;
2287 impl DecisionFrame {
2288 pub fn new(_: &'static str, _: usize) -> Self { Self }
2289 }
2290 pub fn hit(_: &'static str) {}
2291 pub fn arms(_: &[&'static str], _: usize) {}
2292 pub fn logical(left: bool, _: bool, _: &'static str, _: &'static str) -> bool { left }
2293 pub fn for_loop<I: IntoIterator>(iterable: I, _: &'static str, _: &'static str) -> I::IntoIter {
2294 iterable.into_iter()
2295 }
2296 pub fn entered(_: &mut bool, _: &'static str) {}
2297 pub fn zero_iterations(_: bool, _: &'static str) {}
2298 pub trait TryProbe: Sized {
2299 fn probe(self, _: &'static str, _: &'static str) -> Self { self }
2300 }
2301 impl<T> TryProbe for T {}
2302 pub fn condition<V: std::ops::Not<Output = bool>>(value: V, _: &mut DecisionFrame, _: usize) -> bool { !!value }
2303 pub fn decision(value: bool, _: &mut DecisionFrame) -> bool { value }
2304 pub fn reached(_: &mut DecisionFrame, _: usize) -> bool { true }
2305 pub fn decision_chain(_: &mut DecisionFrame, _: bool, _: &[(usize, &'static str, &'static str)]) {}
2306 pub fn assertion(_: &'static str) {}
2307}
2308"#;
2309
2310 fn compile_and_run(source: &str, name: &str) -> std::process::Output {
2311 compile_and_run_edition(source, name, "2024")
2312 }
2313
2314 fn compile_and_run_edition(source: &str, name: &str, edition: &str) -> std::process::Output {
2315 let nonce = SystemTime::now()
2316 .duration_since(UNIX_EPOCH)
2317 .unwrap()
2318 .as_nanos();
2319 let directory = std::env::temp_dir().join(format!(
2320 "supercov-rust-transform-{}-{nonce}-{name}",
2321 std::process::id()
2322 ));
2323 fs::create_dir(&directory).unwrap();
2324 let input = directory.join("main.rs");
2325 let binary = directory.join("program");
2326 fs::write(&input, source).unwrap();
2327 let compile = Command::new("rustc")
2328 .arg(format!("--edition={edition}"))
2329 .arg(&input)
2330 .arg("-o")
2331 .arg(&binary)
2332 .output()
2333 .unwrap();
2334 assert!(
2335 compile.status.success(),
2336 "rustc failed:\n{}\nsource:\n{source}",
2337 String::from_utf8_lossy(&compile.stderr)
2338 );
2339 let output = Command::new(&binary).output().unwrap();
2340 fs::remove_dir_all(directory).unwrap();
2341 output
2342 }
2343
2344 #[test]
2345 fn discovers_rust_obligations_with_exact_ranges_and_stable_ids() {
2346 let source = r#"fn classify<T>(values: &[T], first: bool, second: bool, third: bool) -> Option<&T> {
2347 let picked = if first && (second || third) {
2348 values.first()?
2349 } else {
2350 None
2351 };
2352 for value in values {
2353 if first || second {
2354 return Some(value);
2355 }
2356 }
2357 match picked {
2358 Some(value) if second && third => Some(value),
2359 _ => None,
2360 }
2361}
2362
2363fn closure(value: i32) -> bool {
2364 (|candidate| candidate > 0)(value)
2365}
2366"#;
2367 let first = build_rust_manifest("src/lib.rs", source).unwrap();
2368 let second = build_rust_manifest("src/lib.rs", source).unwrap();
2369 assert_eq!(first, second);
2370 assert!(first.points.iter().any(|point| {
2371 point.kind == PointKind::Function && point.label.as_deref() == Some("classify")
2372 }));
2373 assert!(first.points.iter().any(|point| {
2374 point.kind == PointKind::Function && point.label.as_deref() == Some("<closure>")
2375 }));
2376 let first_if = first
2377 .decisions
2378 .iter()
2379 .find(|decision| decision.line == 2)
2380 .unwrap();
2381 assert_eq!(first_if.conditions, ["first", "second", "third"]);
2382 assert_eq!(first_if.column, 20);
2383 assert!(
2384 first
2385 .branches
2386 .iter()
2387 .any(|branch| branch.kind == "for-loop")
2388 );
2389 let mut arms = first
2390 .branches
2391 .iter()
2392 .filter(|branch| branch.kind == "match-arm")
2393 .collect::<Vec<_>>();
2394 arms.sort_by_key(|branch| branch.line);
2395 assert_eq!(arms.len(), 2);
2396 assert_eq!(
2397 arms[0]
2398 .alternatives
2399 .iter()
2400 .map(|alternative| alternative.label.as_str())
2401 .collect::<Vec<_>>(),
2402 ["not selected", "selected"]
2403 );
2404 assert_eq!(
2407 arms[1]
2408 .alternatives
2409 .iter()
2410 .map(|alternative| alternative.label.as_str())
2411 .collect::<Vec<_>>(),
2412 ["selected"]
2413 );
2414 assert!(
2415 first
2416 .branches
2417 .iter()
2418 .any(|branch| branch.kind == "try-operator")
2419 );
2420 assert!(first.decisions.iter().all(|decision| {
2421 decision.id.starts_with("rs:decision:") && decision.conditions.len() >= 2
2422 }));
2423 assert!(first.limitations.is_empty());
2424 }
2425
2426 #[test]
2427 fn declares_macro_and_const_boundaries_instead_of_hiding_them() {
2428 let source = r#"const fn doubled(value: usize) -> usize { value * 2 }
2429
2430macro_rules! noop {
2431 () => {};
2432}
2433
2434fn checked(value: bool) -> bool {
2435 assert!(value);
2436 noop!();
2437 const { doubled(2) == 4 }
2438}
2439"#;
2440 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
2441 assert!(manifest.decisions.iter().any(|decision| {
2444 decision.line == 8 && decision.source == "value" && decision.conditions == ["value"]
2445 }));
2446 let ids = manifest
2447 .limitations
2448 .iter()
2449 .filter_map(limitation_kind_of)
2450 .collect::<BTreeSet<_>>();
2451 assert_eq!(
2452 ids,
2453 BTreeSet::from([
2454 "rust-const-context-not-instrumented",
2455 "rust-macro-expansion-not-instrumented"
2456 ])
2457 );
2458 assert!(!manifest.points.iter().any(|point| {
2459 point.kind == PointKind::Function && point.label.as_deref() == Some("doubled")
2460 }));
2461 }
2462
2463 #[test]
2464 fn transforms_points_and_nested_decisions_without_changing_behavior() {
2465 let source = r#"use std::sync::atomic::{AtomicUsize, Ordering};
2466
2467static CALLS: AtomicUsize = AtomicUsize::new(0);
2468
2469fn observed(name: &str, value: bool) -> bool {
2470 let order = CALLS.fetch_add(1, Ordering::SeqCst);
2471 println!("{order}:{name}:{value}");
2472 value
2473}
2474
2475fn classify(first: bool, second: bool, third: bool) -> i32 {
2476 if observed("a", first) && (observed("b", second) || observed("c", third)) {
2477 7
2478 } else {
2479 3
2480 }
2481}
2482
2483fn main() {
2484 let closure = |value: i32| value + 1;
2485 println!("result={}", closure(classify(true, false, true)));
2486}
2487"#;
2488 let transformed =
2489 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2490 assert!(transformed.code.contains("::condition("));
2491 assert!(transformed.code.contains("::decision("));
2492 assert!(transformed.code.contains("::hit("));
2493 let original = compile_and_run(source, "original");
2494 let instrumented = compile_and_run(
2495 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2496 "instrumented",
2497 );
2498 assert_eq!(instrumented.status, original.status);
2499 assert_eq!(instrumented.stdout, original.stdout);
2500 assert_eq!(instrumented.stderr, original.stderr);
2501 }
2502
2503 #[test]
2504 fn let_chains_take_derived_condition_probes_and_const_contexts_stay_declared() {
2505 let source = r#"const fn enabled(value: bool) -> bool {
2506 if value { true } else { false }
2507}
2508
2509fn classify(value: Option<bool>, fallback: bool) -> bool {
2510 if let Some(inner) = value && inner && fallback { true } else { false }
2511}
2512"#;
2513 let transformed =
2514 instrument_rust_source("src/lib.rs", source, "crate::__supercov_runtime_v1").unwrap();
2515 let ids = transformed
2516 .manifest
2517 .limitations
2518 .iter()
2519 .filter_map(limitation_kind_of)
2520 .collect::<BTreeSet<_>>();
2521 assert!(ids.contains("rust-const-context-not-instrumented"));
2522 assert!(!ids.contains("rust-let-chain-probes-not-injected"));
2523 assert!(
2526 transformed
2527 .code
2528 .contains("::reached(&mut __supercov_decision_")
2529 );
2530 assert!(
2531 transformed
2532 .code
2533 .contains("::condition((inner), &mut __supercov_decision_")
2534 );
2535 assert!(
2536 transformed
2537 .code
2538 .contains("::decision_chain(&mut __supercov_decision_")
2539 );
2540 assert!(transformed.code.contains("&& let Some(inner) = value &&"));
2541 assert!(!transformed.code.contains("condition((let"));
2542 }
2543
2544 #[test]
2545 fn std_macro_arguments_take_probes_and_assertions_are_decisions() {
2546 let source = r#"use std::fmt::Write as _;
2547
2548fn classify(values: &[i32], strict: bool) -> String {
2549 let mut out = String::new();
2550 assert!(values.len() < 10 && (strict || !values.is_empty()), "bad input {:?}", values);
2551 debug_assert!(values.iter().all(|v| *v > -100));
2552 let doubled = vec![values.iter().map(|v| v * 2).sum::<i32>(), if strict { 1 } else { 2 }];
2553 let repeated = vec![if strict { 1 } else { 0 }; values.len()];
2554 let small = matches!(values.first(), Some(v) if *v < 3);
2555 if matches!(values.len(), 1 | 2) && small {
2556 println!("small");
2557 }
2558 println!("{}", repeated.len() + small as usize);
2559 write!(out, "{}", doubled.iter().map(|d| if *d > 4 { "big" } else { "small" }).collect::<Vec<_>>().join(",")).unwrap();
2560 println!("{} {}", format!("{:?}", doubled), if values.first().copied().unwrap_or(0) > 0 && strict { "positive" } else { "other" });
2561 assert_eq!(doubled.len(), if strict { 2 } else { 2 }, "length for strict={strict}");
2562 out
2563}
2564
2565fn main() {
2566 println!("{}", classify(&[1, 2], true));
2567 println!("{}", classify(&[3], false));
2568 println!("{}", classify(&[], true));
2569 let total: i32 = dbg!(vec![1, 2, 3]).into_iter().sum();
2570 println!("{total}");
2571}
2572"#;
2573 let transformed =
2574 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2575 assert!(transformed.code.contains(
2578 r#", "assertion failed: {}", stringify!(values.iter().all(|v| *v > -100)))"#
2579 ));
2580 assert!(
2582 transformed
2583 .code
2584 .contains(r#"), "bad input {:?}", values);"#)
2585 );
2586 let assertion = transformed
2590 .manifest
2591 .decisions
2592 .iter()
2593 .find(|decision| decision.line == 5)
2594 .expect("assert! decision");
2595 assert_eq!(
2596 assertion.conditions,
2597 ["values.len() < 10", "strict", "!values.is_empty()"]
2598 );
2599 assert!(
2600 transformed
2601 .manifest
2602 .decisions
2603 .iter()
2604 .any(|decision| decision.line == 7)
2605 );
2606 assert!(
2607 transformed
2608 .manifest
2609 .decisions
2610 .iter()
2611 .any(|decision| decision.line == 9)
2612 );
2613 assert!(
2614 transformed
2615 .code
2616 .contains("assert!(({ let mut __supercov_decision_")
2617 );
2618 assert!(
2619 transformed
2620 .code
2621 .contains(", if ({ let mut __supercov_decision_")
2622 );
2623 assert!(
2625 transformed
2626 .code
2627 .contains("vec![if ({ let mut __supercov_decision_")
2628 );
2629 assert!(
2632 transformed
2633 .code
2634 .contains("let small = ({ let mut __supercov_decision_")
2635 );
2636 assert_eq!(
2637 transformed
2638 .manifest
2639 .decisions
2640 .iter()
2641 .filter(|decision| decision.line == 10)
2642 .count(),
2643 1
2644 );
2645 assert_eq!(
2646 transformed
2647 .manifest
2648 .decisions
2649 .iter()
2650 .filter(|decision| decision.line == 9)
2651 .count(),
2652 1
2653 );
2654 assert!(
2655 transformed
2656 .code
2657 .contains("vec![values.iter().map(|v| { crate::__supercov_runtime_v1::hit(")
2658 );
2659 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
2660 limitation.get("id").and_then(|id| id.as_str())
2661 == Some("rust-macro-expansion-not-instrumented")
2662 }));
2663 let original = compile_and_run(source, "original-macros");
2664 let instrumented = compile_and_run(
2665 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2666 "instrumented-macros",
2667 );
2668 assert_eq!(instrumented.status, original.status);
2669 assert_eq!(instrumented.stdout, original.stdout);
2670 let after_location = |stderr: &[u8]| {
2673 String::from_utf8_lossy(stderr)
2674 .lines()
2675 .map(|line| {
2676 line.split_once("] ")
2677 .map_or(line, |(_, rest)| rest)
2678 .to_owned()
2679 })
2680 .collect::<Vec<_>>()
2681 };
2682 assert_eq!(
2683 after_location(&instrumented.stderr),
2684 after_location(&original.stderr)
2685 );
2686 }
2687
2688 #[test]
2689 fn assertion_panic_messages_survive_instrumentation() {
2690 let source = r#"fn grow(len: usize, new_capacity: usize) {
2693 assert!(new_capacity >= len);
2694}
2695
2696fn check(value: i32) {
2697 assert!(value > 0 && value < 10, "value {value} out of range");
2698}
2699
2700// tokio: `assert!` only negates its operand, so a `&bool` is accepted.
2701fn all_seen(seen: &[bool]) {
2702 for was_seen in seen {
2703 assert!(was_seen);
2704 debug_assert!(was_seen, "seen");
2705 }
2706}
2707
2708fn main() {
2709 std::panic::set_hook(Box::new(|_| {}));
2710 all_seen(&[true, true]);
2711 match std::panic::catch_unwind(|| all_seen(&[true, false])) {
2712 Ok(()) => println!("ok"),
2713 Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
2714 }
2715 for (len, capacity) in [(3, 5), (8, 5)] {
2716 match std::panic::catch_unwind(|| grow(len, capacity)) {
2717 Ok(()) => println!("ok"),
2718 Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
2719 }
2720 }
2721 match std::panic::catch_unwind(|| check(12)) {
2722 Ok(()) => println!("ok"),
2723 Err(payload) => println!("{}", payload.downcast_ref::<String>().cloned().unwrap_or_default()),
2724 }
2725}
2726"#;
2727 let transformed =
2728 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2729 let original = compile_and_run(source, "original-assert-message");
2730 let instrumented = compile_and_run(
2731 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2732 "instrumented-assert-message",
2733 );
2734 assert_eq!(instrumented.status, original.status);
2735 assert_eq!(instrumented.stdout, original.stdout);
2736 assert!(
2737 String::from_utf8_lossy(&instrumented.stdout)
2738 .contains("assertion failed: new_capacity >= len")
2739 );
2740 assert!(
2741 String::from_utf8_lossy(&instrumented.stdout).contains("assertion failed: was_seen")
2742 );
2743 }
2744
2745 #[test]
2746 fn files_that_predate_a_reserved_word_still_instrument() {
2747 let source = r#"struct Rng(u64);
2749impl Rng {
2750 fn gen(&mut self) -> u64 {
2751 self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1);
2752 self.0 >> 33
2753 }
2754}
2755
2756fn main() {
2757 let mut rng = Rng(7);
2758 let mut odd = 0;
2759 for _ in 0..10 {
2760 if rng.gen() % 2 == 1 {
2761 odd += 1;
2762 }
2763 }
2764 println!("{odd}");
2765}
2766"#;
2767 let transformed =
2768 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2769 assert!(
2770 transformed
2771 .manifest
2772 .decisions
2773 .iter()
2774 .any(|decision| decision.line == 13)
2775 );
2776 let original = compile_and_run_edition(source, "original-gen", "2021");
2777 let instrumented = compile_and_run_edition(
2778 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2779 "instrumented-gen",
2780 "2021",
2781 );
2782 assert_eq!(instrumented.status, original.status);
2783 assert_eq!(instrumented.stdout, original.stdout);
2784 }
2785
2786 #[test]
2787 fn lone_let_conditions_compile_before_edition_2024_and_record_breaks() {
2788 let source = r#"fn first_even(values: &[i32]) -> Option<i32> {
2792 let mut it = values.iter();
2793 'scan: while let Some(value) = it.next() {
2794 if *value < 0 {
2795 break;
2796 }
2797 for _ in 0..1 {
2798 if *value == 99 {
2799 break 'scan;
2800 }
2801 if *value == 98 {
2802 break;
2803 }
2804 }
2805 if *value % 2 == 0 {
2806 return Some(*value);
2807 }
2808 }
2809 None
2810}
2811
2812fn describe(value: Option<i32>) -> &'static str {
2813 if let Some(inner) = value {
2814 if inner > 0 { "positive" } else { "non-positive" }
2815 } else if let None = value {
2816 "none"
2817 } else {
2818 "unreachable"
2819 }
2820}
2821
2822fn count(values: &[Option<i32>]) -> usize {
2823 let mut total = 0;
2824 for value in values {
2825 if let Some(_) = value {
2826 total += 1;
2827 }
2828 }
2829 total
2830}
2831
2832fn main() {
2833 println!("{:?} {:?} {:?} {:?}", first_even(&[1, 3, 4]), first_even(&[1, -1, 4]), first_even(&[99, 4]), first_even(&[98, 3, 6]));
2834 println!("{} {} {}", describe(Some(2)), describe(Some(-2)), describe(None));
2835 println!("{}", count(&[Some(1), None, Some(3)]));
2836}
2837"#;
2838 let transformed =
2839 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2840 assert!(!transformed.code.contains("&& let"));
2841 assert!(transformed.code.contains("__supercov_broke_"));
2842 assert_eq!(transformed.code.matches("= true; break").count(), 2);
2843 for edition in ["2021", "2024"] {
2844 let original =
2845 compile_and_run_edition(source, &format!("original-lone-let-{edition}"), edition);
2846 let instrumented = compile_and_run_edition(
2847 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2848 &format!("instrumented-lone-let-{edition}"),
2849 edition,
2850 );
2851 assert_eq!(instrumented.status, original.status);
2852 assert_eq!(instrumented.stdout, original.stdout);
2853 assert_eq!(instrumented.stderr, original.stderr);
2854 }
2855 }
2856
2857 #[test]
2858 fn let_chains_keep_their_behavior() {
2859 let source = r#"fn describe(value: Option<i32>, flag: bool) -> &'static str {
2860 if let Some(inner) = value && inner > 0 && flag {
2861 "positive"
2862 } else if let Some(inner) = value && (inner < 0 || flag) {
2863 "negative-or-flagged"
2864 } else {
2865 "other"
2866 }
2867}
2868
2869fn count_pairs(values: &[(Option<i32>, i32)]) -> i32 {
2870 let mut total = 0;
2871 let mut it = values.iter();
2872 while let Some((first, second)) = it.next() && let Some(inner) = first && *second > 0 {
2873 total += inner * second;
2874 if total > 100 {
2875 break;
2876 }
2877 }
2878 total
2879}
2880
2881fn tail(value: Option<&str>) -> usize {
2882 let pick = |v: Option<&str>| if let Some(text) = v && !text.is_empty() { text.len() } else { 0 };
2883 if let Some(text) = value && text.starts_with('x') {
2884 println!("x-prefixed");
2885 }
2886 pick(value)
2887}
2888
2889fn main() {
2890 for value in [Some(3), Some(-3), Some(0), None] {
2891 for flag in [true, false] {
2892 println!("{value:?} {flag} {}", describe(value, flag));
2893 }
2894 }
2895 println!("{}", count_pairs(&[(Some(2), 3), (Some(4), 5), (None, 1), (Some(9), 9)]));
2896 println!("{}", count_pairs(&[(Some(50), 3), (Some(4), 5)]));
2897 println!("{} {} {}", tail(Some("xyz")), tail(Some("")), tail(None));
2898}
2899"#;
2900 let transformed =
2901 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2902 assert_eq!(
2903 transformed.code.matches("const __SUPERCOV_CHAIN_").count(),
2904 5
2905 );
2906 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
2907 limitation.get("id").and_then(|id| id.as_str())
2908 == Some("rust-let-chain-probes-not-injected")
2909 }));
2910 let original = compile_and_run(source, "original-chains");
2911 let instrumented = compile_and_run(
2912 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2913 "instrumented-chains",
2914 );
2915 assert_eq!(instrumented.status, original.status);
2916 assert_eq!(instrumented.stdout, original.stdout);
2917 assert_eq!(instrumented.stderr, original.stderr);
2918 }
2919
2920 #[test]
2921 fn instrumented_const_and_static_initialisers_still_compile() {
2922 let source = r#"const DIRECT: usize = if cfg!(unix) { 100 } else { 1_000 };
2928static WIDTH: usize = if cfg!(unix) { 2 } else { 4 };
2929
2930enum Mode {
2931 Narrow = if cfg!(unix) { 1 } else { 2 },
2932}
2933
2934struct Buffer([u8; if cfg!(unix) { 4 } else { 8 }]);
2935
2936impl Buffer {
2937 const SPAN: usize = if cfg!(unix) { 5 } else { 9 };
2938}
2939
2940fn scaled(flag: bool) -> usize {
2941 const LOCAL: usize = if cfg!(unix) { 3 } else { 6 };
2942 if flag { LOCAL + Buffer::SPAN } else { DIRECT + WIDTH }
2943}
2944
2945fn main() {
2946 let buffer = Buffer([0; if cfg!(unix) { 4 } else { 8 }]);
2947 println!(
2948 "{} {} {} {}",
2949 scaled(true),
2950 scaled(false),
2951 Mode::Narrow as usize,
2952 buffer.0.len()
2953 );
2954}
2955"#;
2956 let transformed =
2957 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2958 assert!(transformed.code.contains("::decision("));
2961 let ids = transformed
2962 .manifest
2963 .limitations
2964 .iter()
2965 .filter_map(limitation_kind_of)
2966 .collect::<BTreeSet<_>>();
2967 assert!(ids.contains("rust-const-context-not-instrumented"));
2968
2969 let original = compile_and_run(source, "const-original");
2970 let instrumented = compile_and_run(
2971 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2972 "const-instrumented",
2973 );
2974 assert_eq!(instrumented.status, original.status);
2975 assert_eq!(instrumented.stdout, original.stdout);
2976 assert_eq!(instrumented.stderr, original.stderr);
2977 }
2978
2979 #[test]
2980 fn a_probed_global_allocator_would_recurse_into_itself() {
2981 let source = r#"use std::alloc::{GlobalAlloc, Layout, System};
2987
2988struct Odd;
2989
2990unsafe impl GlobalAlloc for Odd {
2991 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
2992 if layout.align() == 1 && layout.size() > 0 {
2993 System.alloc(layout)
2994 } else {
2995 System.alloc(layout)
2996 }
2997 }
2998
2999 unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
3000 System.dealloc(pointer, layout);
3001 }
3002}
3003
3004#[global_allocator]
3005static ODD: Odd = Odd;
3006
3007fn classify(flag: bool) -> usize {
3008 if flag { 1 } else { 2 }
3009}
3010
3011fn main() {
3012 let held = std::vec![7u8; 32];
3013 println!("{} {}", classify(!held.is_empty()), held.len());
3014}
3015"#;
3016 let transformed =
3017 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3018 let allocator = transformed
3020 .code
3021 .split("unsafe impl GlobalAlloc for Odd")
3022 .nth(1)
3023 .and_then(|rest| rest.split("#[global_allocator]").next())
3024 .expect("the instrumented source still contains the allocator impl");
3025 assert!(
3026 !allocator.contains("__supercov_runtime_v1"),
3027 "probe injected into a GlobalAlloc impl:\n{allocator}"
3028 );
3029 assert!(transformed.code.contains("::decision("));
3031 let ids = transformed
3032 .manifest
3033 .limitations
3034 .iter()
3035 .filter_map(limitation_kind_of)
3036 .collect::<BTreeSet<_>>();
3037 assert!(ids.contains("rust-global-allocator-not-instrumented"));
3038
3039 let original = compile_and_run(source, "alloc-original");
3040 let instrumented = compile_and_run(
3041 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3042 "alloc-instrumented",
3043 );
3044 assert_eq!(instrumented.status, original.status);
3045 assert_eq!(instrumented.stdout, original.stdout);
3046 assert_eq!(instrumented.stderr, original.stderr);
3047 }
3048
3049 #[test]
3050 fn match_arms_record_selection_without_changing_behavior() {
3051 let source = r#"#[derive(Debug)]
3052enum Shape { Dot, Line(i32), Box { w: i32, h: i32 } }
3053
3054fn area(shape: &Shape) -> i32 {
3055 match shape {
3056 Shape::Dot => 0,
3057 Shape::Line(length) if *length < 0 => -length,
3058 Shape::Line(length) => *length,
3059 Shape::Box { w, h } => {
3060 let area = w * h;
3061 area
3062 }
3063 }
3064}
3065
3066fn describe(value: i32) -> &'static str {
3067 let inner = |v: i32| match v { 0 => "none", 1 => "one", _ => "many" };
3068 match value {
3069 0 => inner(value),
3070 n if n < 0 => unsafe { std::hint::unreachable_unchecked() },
3071 n => match n % 2 {
3072 0 => "even",
3073 _ => inner(n),
3074 },
3075 }
3076}
3077
3078fn main() {
3079 for shape in [Shape::Dot, Shape::Line(-3), Shape::Line(4), Shape::Box { w: 2, h: 5 }] {
3080 println!("{shape:?}={}", area(&shape));
3081 }
3082 for value in [0, 1, 3, 8] {
3083 println!("{value}:{}", describe(value));
3084 }
3085}
3086"#;
3087 let transformed =
3088 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3089 assert!(transformed.code.contains("::arms(__SUPERCOV_ARMS_"));
3090 assert_eq!(
3091 transformed.code.matches("const __SUPERCOV_ARMS_").count(),
3092 4
3093 );
3094 let arms = transformed
3095 .manifest
3096 .branches
3097 .iter()
3098 .filter(|branch| branch.kind == "match-arm")
3099 .count();
3100 assert_eq!(arms, 4 + 3 + 3 + 2);
3101 for branch in transformed
3104 .manifest
3105 .branches
3106 .iter()
3107 .filter(|branch| branch.kind == "match-arm")
3108 {
3109 for alternative in &branch.alternatives {
3110 assert!(
3111 transformed.code.contains(&format!("{:?}", alternative.id)),
3112 "{} is not in any table",
3113 alternative.id
3114 );
3115 }
3116 }
3117 let original = compile_and_run(source, "original-arms");
3118 let instrumented = compile_and_run(
3119 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3120 "instrumented-arms",
3121 );
3122 assert_eq!(instrumented.status, original.status);
3123 assert_eq!(instrumented.stdout, original.stdout);
3124 assert_eq!(instrumented.stderr, original.stderr);
3125 }
3126
3127 #[test]
3128 fn loops_logic_and_try_record_their_branches_without_changing_behavior() {
3129 let source = r#"use std::ops::ControlFlow;
3130
3131fn total(values: &[i32]) -> i32 {
3132 let mut sum = 0;
3133 for value in values {
3134 sum += value;
3135 }
3136 'outer: for row in 0..3 {
3137 for column in 0..3 {
3138 if column > row {
3139 continue 'outer;
3140 }
3141 sum += row * column;
3142 }
3143 }
3144 sum
3145}
3146
3147fn first_even(values: &[i32]) -> Option<i32> {
3148 let mut index = 0;
3149 'scan: while index < values.len() {
3150 if values[index] % 2 == 0 {
3151 break 'scan;
3152 }
3153 index += 1;
3154 }
3155 let mut it = values.iter().skip(index);
3156 while let Some(value) = it.next() {
3157 return Some(*value);
3158 }
3159 None
3160}
3161
3162fn parse_twice(text: &str) -> Result<i32, String> {
3163 let value: i32 = text.trim().parse().map_err(|_| "bad".to_string())?;
3164 let doubled = Some(value).map(|v| v * 2).ok_or("none")?;
3165 Ok(doubled)
3166}
3167
3168fn halve(value: i32) -> Option<i32> {
3169 let even = (value % 2 == 0).then_some(value)?;
3170 Some(even / 2)
3171}
3172
3173fn flow(values: &[i32]) -> ControlFlow<i32, i32> {
3174 let mut sum = 0;
3175 for value in values {
3176 let step: ControlFlow<i32, i32> = if *value < 0 { ControlFlow::Break(*value) } else { ControlFlow::Continue(*value) };
3177 sum += step?;
3178 }
3179 ControlFlow::Continue(sum)
3180}
3181
3182fn gate(a: bool, b: bool, c: bool) -> bool {
3183 let both = a && b;
3184 let either = a || b || c;
3185 both || (either && !c) || (c && a && (b || !b))
3186}
3187
3188fn main() {
3189 println!("{} {}", total(&[]), total(&[1, 2, 3]));
3190 println!("{:?} {:?} {:?}", first_even(&[]), first_even(&[1, 3]), first_even(&[1, 4, 6]));
3191 println!("{:?} {:?}", parse_twice(" 21 "), parse_twice("x"));
3192 println!("{:?} {:?}", halve(8), halve(7));
3193 println!("{:?} {:?}", flow(&[1, 2]), flow(&[1, -5, 2]));
3194 for a in [false, true] {
3195 for b in [false, true] {
3196 for c in [false, true] {
3197 print!("{}", gate(a, b, c) as u8);
3198 }
3199 }
3200 }
3201 println!();
3202}
3203"#;
3204 let transformed =
3205 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3206 for marker in [
3207 "::logical((",
3208 "::for_loop((",
3209 "::entered(&mut __supercov_loop_",
3210 "::zero_iterations(__supercov_loop_",
3211 "::TryProbe::probe((",
3212 ] {
3213 assert!(transformed.code.contains(marker), "{marker} missing");
3214 }
3215 let kinds = |kind: &str| {
3216 transformed
3217 .manifest
3218 .branches
3219 .iter()
3220 .filter(|branch| branch.kind == kind)
3221 .count()
3222 };
3223 assert_eq!(kinds("for-loop"), 3 + 1 + 3);
3224 assert_eq!(kinds("while-loop"), 2);
3225 assert_eq!(kinds("try-operator"), 4);
3226 assert_eq!(kinds("logical-and"), 4);
3227 assert_eq!(kinds("logical-or"), 5);
3228 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
3229 limitation.get("id").and_then(|id| id.as_str())
3230 == Some("rust-structural-branch-probes-not-yet-injected")
3231 }));
3232 let original = compile_and_run(source, "original-structural");
3233 let instrumented = compile_and_run(
3234 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3235 "instrumented-structural",
3236 );
3237 assert_eq!(instrumented.status, original.status);
3238 assert_eq!(instrumented.stdout, original.stdout);
3239 assert_eq!(instrumented.stderr, original.stderr);
3240 }
3241
3242 #[test]
3243 fn cfg_gated_sibling_blocks_keep_their_tail_position() {
3244 let source = r#"pub fn is_available() -> bool {
3250 #[cfg(target_endian = "little")]
3251 {
3252 true
3253 }
3254 #[cfg(not(target_endian = "little"))]
3255 {
3256 false
3257 }
3258}
3259
3260fn main() {
3261 println!("{}", is_available());
3262}
3263"#;
3264 let transformed =
3265 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3266 let original = compile_and_run(source, "cfg-original");
3267 let instrumented = compile_and_run(
3268 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3269 "cfg-instrumented",
3270 );
3271 assert_eq!(instrumented.status, original.status);
3272 assert_eq!(instrumented.stdout, original.stdout);
3273 assert!(
3275 transformed
3276 .code
3277 .contains("{\n\ncrate::__supercov_runtime_v1::hit(")
3278 || transformed
3279 .code
3280 .contains("{\ncrate::__supercov_runtime_v1::hit(")
3281 );
3282
3283 let attributed_let = r#"fn main() {
3287 #[cfg(target_endian = "little")]
3288 let value = 1;
3289 #[cfg(not(target_endian = "little"))]
3290 let value = 2;
3291 #[cfg(target_endian = "little")]
3292 let borrowed: &String = &String::from("little");
3293 #[cfg(not(target_endian = "little"))]
3294 let borrowed: &String = &String::from("big");
3295 #[cfg(target_endian = "little")]
3296 print!("le ");
3297 #[cfg(not(target_endian = "little"))]
3298 print!("be ");
3299 #[allow(unused_assignments)]
3300 let mut later;
3301 later = value + 1;
3302 println!("{value} {borrowed} {later}");
3303}
3304"#;
3305 let transformed = instrument_rust_source(
3306 "src/main.rs",
3307 attributed_let,
3308 "crate::__supercov_runtime_v1",
3309 )
3310 .unwrap();
3311 let ids = transformed
3312 .manifest
3313 .limitations
3314 .iter()
3315 .filter_map(limitation_kind_of)
3316 .collect::<BTreeSet<_>>();
3317 assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
3319 assert!(
3320 transformed
3321 .code
3322 .contains("let value = { crate::__supercov_runtime_v1::hit(")
3323 );
3324 assert!(
3325 transformed
3326 .code
3327 .contains("let borrowed: &String = { crate::__supercov_runtime_v1::hit(")
3328 );
3329 assert!(
3330 transformed
3331 .code
3332 .contains("] { crate::__supercov_runtime_v1::hit(")
3333 );
3334 let original = compile_and_run(attributed_let, "cfg-let-original");
3335 let instrumented = compile_and_run(
3336 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3337 "cfg-let-instrumented",
3338 );
3339 assert_eq!(instrumented.status, original.status);
3340 assert_eq!(instrumented.stdout, original.stdout);
3341 assert_eq!(instrumented.stderr, original.stderr);
3342
3343 let trailing_macro = r#"macro_rules! pick { ($e:expr) => { $e } }
3348fn value() -> i32 {
3349 let base = 20;
3350 #[rustfmt::skip]
3351 pick! { base + 1 }
3352}
3353fn effect() {
3354 #[rustfmt::skip]
3355 println! { "effect" }
3356}
3357fn gated() {
3358 #[cfg(target_endian = "little")]
3359 println! { "little" }
3360}
3361fn main() {
3362 effect();
3363 gated();
3364 println!("{}", value());
3365}
3366"#;
3367 let transformed = instrument_rust_source(
3368 "src/main.rs",
3369 trailing_macro,
3370 "crate::__supercov_runtime_v1",
3371 )
3372 .unwrap();
3373 assert!(
3374 transformed
3375 .code
3376 .contains("{ crate::__supercov_runtime_v1::hit(\"rs:statement:")
3377 );
3378 assert!(
3379 transformed
3380 .code
3381 .contains("); #[rustfmt::skip]\n pick! { base + 1 } }")
3382 );
3383 assert!(
3384 transformed
3385 .code
3386 .contains("); #[rustfmt::skip]\n println! { \"effect\" } }")
3387 );
3388 assert!(
3389 transformed.code.contains(
3390 "\n #[cfg(target_endian = \"little\")]\n println! { \"little\" }\n"
3391 )
3392 );
3393 let ids = transformed
3394 .manifest
3395 .limitations
3396 .iter()
3397 .filter_map(limitation_kind_of)
3398 .collect::<BTreeSet<_>>();
3399 assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
3400 let original = compile_and_run(trailing_macro, "trailing-macro-original");
3401 let instrumented = compile_and_run(
3402 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3403 "trailing-macro-instrumented",
3404 );
3405 assert_eq!(
3406 instrumented.status,
3407 original.status,
3408 "{}",
3409 String::from_utf8_lossy(&instrumented.stderr)
3410 );
3411 assert_eq!(instrumented.stdout, original.stdout);
3412 assert_eq!(instrumented.stderr, original.stderr);
3413 }
3414
3415 #[test]
3416 fn an_attributed_macro_statement_keeps_its_macro_in_statement_position() {
3417 let source = r#"macro_rules! trace {
3422 ($($arg:tt)*) => {
3423 #[cfg(target_endian = "little")]
3424 {
3425 println!($($arg)+);
3426 }
3427 }
3428}
3429fn manual() {
3430 #[cfg(any(target_endian = "little", target_endian = "big"))]
3431 trace!("manual");
3432 let _ = 1;
3433}
3434fn main() {
3435 manual();
3436}
3437"#;
3438 let transformed =
3439 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3440 assert!(
3443 transformed.code.contains(r#"trace!("manual"); }"#),
3444 "{}",
3445 transformed.code
3446 );
3447 let original = compile_and_run(source, "attributed-macro-statement-original");
3448 let instrumented = compile_and_run(
3449 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3450 "attributed-macro-statement-instrumented",
3451 );
3452 assert_eq!(
3453 instrumented.status,
3454 original.status,
3455 "{}",
3456 String::from_utf8_lossy(&instrumented.stderr)
3457 );
3458 assert_eq!(instrumented.stdout, original.stdout);
3459 assert_eq!(instrumented.stderr, original.stderr);
3460 }
3461
3462 #[test]
3463 fn an_attributed_lets_macro_initialiser_keeps_its_value() {
3464 let source = r#"macro_rules! first {
3470 ($e:expr) => { $e }
3471}
3472fn value(flag: bool) -> i32 {
3473 #[cfg(any(target_endian = "little", target_endian = "big"))]
3474 let chosen = first!(if flag { 7 } else { 3 });
3475 chosen + 1
3476}
3477fn main() {
3478 println!("{}", value(true));
3479}
3480"#;
3481 let transformed =
3482 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3483 assert!(
3486 !transformed
3487 .code
3488 .contains("first!(if flag { 7 } else { 3 }); }"),
3489 "{}",
3490 transformed.code
3491 );
3492 let original = compile_and_run(source, "attributed-let-macro-original");
3493 let instrumented = compile_and_run(
3494 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3495 "attributed-let-macro-instrumented",
3496 );
3497 assert_eq!(
3498 instrumented.status,
3499 original.status,
3500 "{}",
3501 String::from_utf8_lossy(&instrumented.stderr)
3502 );
3503 assert_eq!(instrumented.stdout, original.stdout);
3504 assert_eq!(instrumented.stdout, b"8\n");
3505 }
3506
3507 #[test]
3508 fn obligations_no_probe_can_reach_are_declined() {
3509 let source = r#"pub struct Tagged(usize);
3515impl Tagged {
3516 pub const fn new(len: usize, on_heap: bool) -> Self {
3517 Self(if on_heap { len << 1 } else { len })
3518 }
3519 pub fn plain(len: usize) -> Self {
3520 Self(if len > 0 { len } else { 0 })
3521 }
3522}
3523"#;
3524 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
3525 let declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
3526 assert!(!declined.is_empty(), "the const fn was not declined");
3527
3528 let line_of = |id: &str| {
3530 manifest
3531 .points
3532 .iter()
3533 .find(|point| point.id == id)
3534 .map(|point| point.line)
3535 .or_else(|| {
3536 manifest
3537 .decisions
3538 .iter()
3539 .find(|decision| decision.id == id)
3540 .map(|decision| decision.line)
3541 })
3542 .or_else(|| {
3543 manifest
3544 .branches
3545 .iter()
3546 .find(|branch| branch.id == id)
3547 .map(|branch| branch.line)
3548 })
3549 };
3550 for id in &declined {
3551 let line = line_of(id).unwrap_or_else(|| panic!("no obligation {id}"));
3552 assert!(
3553 (3..=5).contains(&line),
3554 "declined an obligation outside the const fn, at line {line}"
3555 );
3556 }
3557 let measured = manifest
3558 .points
3559 .iter()
3560 .map(|point| (point.id.clone(), point.line))
3561 .filter(|(id, _)| !declined.contains(id))
3562 .collect::<Vec<_>>();
3563 assert!(
3564 measured.iter().any(|(_, line)| (6..=8).contains(line)),
3565 "the plain fn must stay measured: {measured:?}"
3566 );
3567 }
3568
3569 #[test]
3570 fn rejects_non_crate_local_runtime_paths() {
3571 assert_eq!(
3572 instrument_rust_source("src/lib.rs", "fn okay() {}", "supercov::runtime"),
3573 Err(RustInstrumenterError::InvalidRuntimePath)
3574 );
3575 }
3576
3577 #[test]
3578 fn rejects_invalid_rust_without_partial_obligations() {
3579 assert!(matches!(
3580 build_rust_manifest("src/lib.rs", "fn broken( {\n"),
3581 Err(RustInstrumenterError::Parse(_))
3582 ));
3583 }
3584}