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 cannot_carry_probe(node: &ra_ap_syntax::SyntaxNode) -> bool {
146 in_const_context(node) || in_global_allocator(node)
147}
148
149fn range_offsets(range: TextRange) -> (usize, usize) {
150 (usize::from(range.start()), usize::from(range.end()))
151}
152
153fn push_wrapper(
154 insertions: &mut Vec<Insertion>,
155 range: TextRange,
156 scope: TextRange,
157 rank: usize,
158 prefix: String,
159 suffix: String,
160) {
161 let (start, end) = range_offsets(range);
162 let (scope_start, scope_end) = range_offsets(scope);
163 let scope_len = scope_end - scope_start;
164 insertions.push(Insertion {
165 offset: start,
166 kind: InsertionKind::Start,
167 scope_len,
168 rank,
169 text: prefix,
170 });
171 insertions.push(Insertion {
172 offset: end,
173 kind: InsertionKind::End,
174 scope_len,
175 rank,
176 text: suffix,
177 });
178}
179
180fn push_direct(insertions: &mut Vec<Insertion>, offset: usize, text: String) {
181 insertions.push(Insertion {
182 offset,
183 kind: InsertionKind::Direct,
184 scope_len: 0,
185 rank: 0,
186 text,
187 });
188}
189
190fn apply_insertions(
191 source: &str,
192 mut insertions: Vec<Insertion>,
193) -> Result<String, RustInstrumenterError> {
194 if insertions
195 .iter()
196 .any(|edit| edit.offset > source.len() || !source.is_char_boundary(edit.offset))
197 {
198 return Err(RustInstrumenterError::InvalidRange);
199 }
200 insertions.sort_by(|left, right| {
201 left.offset.cmp(&right.offset).then_with(|| {
202 let kind_order = |kind: InsertionKind| match kind {
203 InsertionKind::End => 0,
204 InsertionKind::Direct => 1,
205 InsertionKind::Start => 2,
206 };
207 kind_order(left.kind)
208 .cmp(&kind_order(right.kind))
209 .then_with(|| match left.kind {
210 InsertionKind::End => left
211 .scope_len
212 .cmp(&right.scope_len)
213 .then_with(|| right.rank.cmp(&left.rank)),
214 InsertionKind::Direct => std::cmp::Ordering::Equal,
215 InsertionKind::Start => right
216 .scope_len
217 .cmp(&left.scope_len)
218 .then_with(|| left.rank.cmp(&right.rank)),
219 })
220 })
221 });
222
223 let mut output = source.to_owned();
224 let mut index = insertions.len();
225 while index > 0 {
226 let offset = insertions[index - 1].offset;
227 let start = insertions[..index].partition_point(|insertion| insertion.offset < offset);
228 let text = insertions[start..index]
229 .iter()
230 .map(|insertion| insertion.text.as_str())
231 .collect::<String>();
232 output.insert_str(offset, &text);
233 index = start;
234 }
235 Ok(output)
236}
237
238fn add_manifest_limitation(manifest: &mut CoverageManifest, file: &str, id: &str, reason: &str) {
239 if manifest
240 .limitations
241 .iter()
242 .any(|limitation| limitation.get("id").and_then(|value| value.as_str()) == Some(id))
243 {
244 return;
245 }
246 manifest.limitations.push(json!({
247 "id": id,
248 "kind": "rust-frontend-readiness",
249 "file": file,
250 "line": 1,
251 "column": 0,
252 "source": "",
253 "reason": reason
254 }));
255}
256
257fn allocate_frame_name(
258 file: &str,
259 condition: &ast::Expr,
260 kind: &str,
261 identifiers: &mut BTreeSet<String>,
262) -> String {
263 let id = stable_id(file, "decision", condition.syntax().text_range(), kind);
264 let suffix = id.rsplit(':').next().unwrap_or("decision");
265 let base = format!("__supercov_decision_{suffix}");
266 let mut candidate = base.clone();
267 let mut attempt = 0_usize;
268 while !identifiers.insert(candidate.clone()) {
269 attempt += 1;
270 candidate = format!("{base}_{attempt}");
271 }
272 candidate
273}
274
275fn allocate_table_name(
278 file: &str,
279 expression: &ast::MatchExpr,
280 identifiers: &mut BTreeSet<String>,
281) -> String {
282 let id = stable_id(file, "match", expression.syntax().text_range(), "arms");
283 let suffix = id
284 .rsplit(':')
285 .next()
286 .unwrap_or("match")
287 .to_ascii_uppercase();
288 let base = format!("__SUPERCOV_ARMS_{suffix}");
289 let mut candidate = base.clone();
290 let mut attempt = 0_usize;
291 while !identifiers.insert(candidate.clone()) {
292 attempt += 1;
293 candidate = format!("{base}_{attempt}");
294 }
295 candidate
296}
297
298fn allocate_flag_name(
300 file: &str,
301 expression: &ast::WhileExpr,
302 identifiers: &mut BTreeSet<String>,
303) -> String {
304 let id = stable_id(file, "loop", expression.syntax().text_range(), "flag");
305 let suffix = id.rsplit(':').next().unwrap_or("loop");
306 let base = format!("__supercov_loop_{suffix}");
307 let mut candidate = base.clone();
308 let mut attempt = 0_usize;
309 while !identifiers.insert(candidate.clone()) {
310 attempt += 1;
311 candidate = format!("{base}_{attempt}");
312 }
313 candidate
314}
315
316impl std::error::Error for RustInstrumenterError {}
317
318struct SourceLocations<'a> {
319 source: &'a str,
320 line_starts: Vec<usize>,
321}
322
323impl<'a> SourceLocations<'a> {
324 fn new(source: &'a str) -> Self {
325 let mut line_starts = vec![0];
326 line_starts.extend(
327 source
328 .bytes()
329 .enumerate()
330 .filter_map(|(index, byte)| (byte == b'\n').then_some(index + 1)),
331 );
332 Self {
333 source,
334 line_starts,
335 }
336 }
337
338 fn range(&self, range: TextRange) -> Result<(usize, usize), RustInstrumenterError> {
339 let start = usize::from(range.start());
340 let end = usize::from(range.end());
341 if start > end
342 || end > self.source.len()
343 || !self.source.is_char_boundary(start)
344 || !self.source.is_char_boundary(end)
345 {
346 return Err(RustInstrumenterError::InvalidRange);
347 }
348 Ok((start, end))
349 }
350
351 fn line_column(&self, offset: usize) -> (usize, usize) {
352 let line_index = self.line_starts.partition_point(|start| *start <= offset) - 1;
353 (line_index + 1, offset - self.line_starts[line_index])
354 }
355
356 fn text(&self, range: TextRange) -> Result<String, RustInstrumenterError> {
357 let (start, end) = self.range(range)?;
358 Ok(self.source[start..end].trim().to_owned())
359 }
360}
361
362fn stable_id(file: &str, kind: &str, range: TextRange, suffix: &str) -> String {
363 let mut hash = Sha256::new();
364 let start = usize::from(range.start()).to_string();
365 let end = usize::from(range.end()).to_string();
366 for value in [file, kind, &start, &end, suffix] {
367 hash.update(value.as_bytes());
368 hash.update([0]);
369 }
370 let digest = hash.finalize();
371 let mut encoded = String::with_capacity(24);
372 for byte in &digest[..12] {
373 use std::fmt::Write as _;
374 write!(&mut encoded, "{byte:02x}").expect("writing to a string cannot fail");
375 }
376 format!("rs:{kind}:{encoded}")
377}
378
379struct RustObligationCollector<'a> {
380 file: &'a str,
381 locations: SourceLocations<'a>,
382 manifest: CoverageManifest,
383 point_ids: BTreeSet<String>,
384 decision_ids: BTreeSet<String>,
385 branch_ids: BTreeSet<String>,
386 limitation_ids: BTreeSet<&'static str>,
387 error: Option<RustInstrumenterError>,
388}
389
390impl<'a> RustObligationCollector<'a> {
391 fn new(file: &'a str, source: &'a str) -> Self {
392 Self {
393 file,
394 locations: SourceLocations::new(source),
395 manifest: CoverageManifest {
396 unmeasured: Vec::new(),
397 decisions: Vec::new(),
398 points: Vec::new(),
399 branches: Vec::new(),
400 limitations: Vec::new(),
401 scope: None,
402 },
403 point_ids: BTreeSet::new(),
404 decision_ids: BTreeSet::new(),
405 branch_ids: BTreeSet::new(),
406 limitation_ids: BTreeSet::new(),
407 error: None,
408 }
409 }
410
411 fn location_source(&mut self, range: TextRange) -> Option<(usize, usize, String)> {
412 let result = self.locations.range(range).map(|(start, _)| {
413 let (line, column) = self.locations.line_column(start);
414 (line, column, self.locations.text(range))
415 });
416 match result {
417 Ok((line, column, Ok(source))) => Some((line, column, source)),
418 Ok((_, _, Err(error))) | Err(error) => {
419 self.error.get_or_insert(error);
420 None
421 }
422 }
423 }
424
425 fn point(&mut self, range: TextRange, kind: PointKind, label: Option<String>) {
426 let kind_name = match kind {
427 PointKind::Statement => "statement",
428 PointKind::Function => "function",
429 };
430 let id = stable_id(self.file, kind_name, range, label.as_deref().unwrap_or(""));
431 if !self.point_ids.insert(id.clone()) {
432 return;
433 }
434 let Some((line, column, source)) = self.location_source(range) else {
435 return;
436 };
437 self.manifest.points.push(PointMeta {
438 id,
439 kind,
440 file: self.file.into(),
441 line,
442 column,
443 source,
444 label,
445 });
446 }
447
448 fn atomic_condition_ranges(expression: &ast::Expr, ranges: &mut Vec<TextRange>) {
449 match expression {
450 ast::Expr::ParenExpr(paren) => {
451 if let Some(inner) = paren.expr() {
452 Self::atomic_condition_ranges(&inner, ranges);
453 } else {
454 ranges.push(expression.syntax().text_range());
455 }
456 }
457 ast::Expr::BinExpr(binary)
458 if matches!(
459 binary.op_kind(),
460 Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))
461 ) =>
462 {
463 if let Some(left) = binary.lhs() {
464 Self::atomic_condition_ranges(&left, ranges);
465 }
466 if let Some(right) = binary.rhs() {
467 Self::atomic_condition_ranges(&right, ranges);
468 }
469 }
470 _ => ranges.push(expression.syntax().text_range()),
471 }
472 }
473
474 fn decision(&mut self, test: &ast::Expr, kind: &str) {
475 let range = test.syntax().text_range();
476 let id = stable_id(self.file, "decision", range, kind);
477 if !self.decision_ids.insert(id.clone()) {
478 return;
479 }
480 let Some((line, column, source)) = self.location_source(range) else {
481 return;
482 };
483 let mut condition_ranges = Vec::new();
484 Self::atomic_condition_ranges(test, &mut condition_ranges);
485 let mut conditions = Vec::with_capacity(condition_ranges.len());
486 for condition in condition_ranges {
487 match self.locations.text(condition) {
488 Ok(source) => conditions.push(source),
489 Err(error) => {
490 self.error.get_or_insert(error);
491 return;
492 }
493 }
494 }
495 self.manifest.decisions.push(DecisionMeta {
496 id: id.clone(),
497 file: self.file.into(),
498 line,
499 column,
500 source: source.clone(),
501 conditions,
502 kind: kind.into(),
503 });
504 self.branch_with_id(
505 format!("{id}:outcome"),
506 range,
507 kind,
508 source,
509 [("true", "true"), ("false", "false")],
510 );
511 }
512
513 fn branch<const N: usize>(
514 &mut self,
515 range: TextRange,
516 kind: &str,
517 alternatives: [(&str, &str); N],
518 ) {
519 let id = stable_id(self.file, "branch", range, kind);
520 let Some((_, _, source)) = self.location_source(range) else {
521 return;
522 };
523 self.branch_with_id(id, range, kind, source, alternatives);
524 }
525
526 fn branch_with_id<const N: usize>(
527 &mut self,
528 id: String,
529 range: TextRange,
530 kind: &str,
531 source: String,
532 alternatives: [(&str, &str); N],
533 ) {
534 if !self.branch_ids.insert(id.clone()) {
535 return;
536 }
537 let Some((line, column, _)) = self.location_source(range) else {
538 return;
539 };
540 self.manifest.branches.push(BranchMeta {
541 id: id.clone(),
542 kind: kind.into(),
543 file: self.file.into(),
544 line,
545 column,
546 source,
547 alternatives: alternatives
548 .into_iter()
549 .map(|(suffix, label)| BranchAlternativeMeta {
550 id: format!("{id}:{suffix}"),
551 label: label.into(),
552 })
553 .collect(),
554 });
555 }
556
557 fn limitation(&mut self, id: &'static str, reason: &'static str) {
558 if !self.limitation_ids.insert(id) {
559 return;
560 }
561 self.manifest.limitations.push(json!({
562 "id": id,
563 "kind": "rust-frontend-readiness",
564 "file": self.file,
565 "line": 1,
566 "column": 0,
567 "source": "",
568 "reason": reason
569 }));
570 }
571
572 fn collect(
573 mut self,
574 file: &SourceFile,
575 assertions: &[TextRange],
576 matches: &[TextRange],
577 ) -> Result<CoverageManifest, RustInstrumenterError> {
578 let root = file.syntax();
579
580 for list in root.descendants().filter_map(ast::StmtList::cast) {
581 for statement in list.statements() {
582 match statement {
583 ast::Stmt::ExprStmt(statement) => {
584 self.point(statement.syntax().text_range(), PointKind::Statement, None);
585 }
586 ast::Stmt::LetStmt(statement) => {
587 self.point(statement.syntax().text_range(), PointKind::Statement, None);
588 }
589 ast::Stmt::Item(_) => {}
590 }
591 }
592 if let Some(tail) = list.tail_expr() {
593 self.point(tail.syntax().text_range(), PointKind::Statement, None);
594 }
595 }
596
597 for function in root.descendants().filter_map(ast::Fn::cast) {
598 if function.body().is_none() {
599 continue;
600 }
601 if function.const_token().is_some() {
602 self.limitation(
603 "rust-const-context-not-instrumented",
604 "Runtime probes cannot execute in const fn or compile-time evaluation",
605 );
606 continue;
607 }
608 let label = function.name().map(|name| name.text().to_string());
609 self.point(function.syntax().text_range(), PointKind::Function, label);
610 }
611
612 for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
613 self.point(
614 closure.syntax().text_range(),
615 PointKind::Function,
616 Some("<closure>".into()),
617 );
618 }
619
620 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
621 if let Some(condition) = expression.condition() {
622 self.decision(&condition, "if");
623 }
624 }
625 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
626 if let Some(condition) = expression.condition() {
627 self.decision(&condition, "while");
628 }
629 self.branch(
630 expression.syntax().text_range(),
631 "while-loop",
632 [("zero", "zero iterations"), ("entered", "entered")],
633 );
634 }
635 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
636 if let Some(condition) = guard.condition() {
637 self.decision(&condition, "match-guard");
638 }
639 }
640 for arguments in assertions {
641 if let Some(condition) = assertion_condition(root, *arguments) {
642 self.decision(&condition, "assert");
643 }
644 }
645 for expression in standalone_matches(root, matches, assertions) {
646 self.decision(&expression, "matches");
647 }
648
649 for binary in root.descendants().filter_map(ast::BinExpr::cast) {
650 let kind = match binary.op_kind() {
651 Some(BinaryOp::LogicOp(LogicOp::And)) => "logical-and",
652 Some(BinaryOp::LogicOp(LogicOp::Or)) => "logical-or",
653 _ => continue,
654 };
655 let range = binary.rhs().map_or_else(
656 || binary.syntax().text_range(),
657 |right| right.syntax().text_range(),
658 );
659 self.branch(
660 range,
661 kind,
662 [
663 ("short-circuit", "short-circuited"),
664 ("evaluated", "right operand evaluated"),
665 ],
666 );
667 }
668
669 for expression in root.descendants().filter_map(ast::ForExpr::cast) {
670 self.branch(
671 expression.syntax().text_range(),
672 "for-loop",
673 [("zero", "zero iterations"), ("entered", "entered")],
674 );
675 }
676 for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
677 let Some(list) = expression.match_arm_list() else {
678 continue;
679 };
680 let arms = list.arms().collect::<Vec<_>>();
681 let last = arms.len().saturating_sub(1);
682 for (index, arm) in arms.iter().enumerate() {
683 let range = arm.syntax().text_range();
684 if index == last {
685 self.branch(range, "match-arm", [("selected", "selected")]);
689 } else {
690 self.branch(
691 range,
692 "match-arm",
693 [("missed", "not selected"), ("selected", "selected")],
694 );
695 }
696 }
697 }
698 for expression in root.descendants().filter_map(ast::TryExpr::cast) {
699 self.branch(
700 expression.syntax().text_range(),
701 "try-operator",
702 [("continued", "continued"), ("returned", "early return")],
703 );
704 }
705
706 if root.descendants().any(|node| {
707 ast::MacroCall::can_cast(node.kind()) || ast::MacroExpr::can_cast(node.kind())
708 }) {
709 self.limitation(
712 "rust-macro-expansion-not-instrumented",
713 "Arguments of macros other than the std expression macros (assert!, println!, vec!, ...) and all macro expansions are not part of the owned source denominator",
714 );
715 }
716
717 let bears_obligation = |node: &ra_ap_syntax::SyntaxNode| {
722 ast::StmtList::cast(node.clone()).is_some_and(|list| {
723 list.statements().next().is_some() || list.tail_expr().is_some()
724 }) || ast::IfExpr::can_cast(node.kind())
725 || ast::WhileExpr::can_cast(node.kind())
726 || ast::MatchGuard::can_cast(node.kind())
727 || ast::ForExpr::can_cast(node.kind())
728 || ast::MatchArm::can_cast(node.kind())
729 || ast::TryExpr::can_cast(node.kind())
730 || ast::ClosureExpr::can_cast(node.kind())
731 || ast::BinExpr::cast(node.clone()).is_some_and(|binary| {
732 matches!(
733 binary.op_kind(),
734 Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))
735 )
736 })
737 };
738 if root
739 .descendants()
740 .any(|node| bears_obligation(&node) && in_const_context(&node))
741 {
742 self.limitation(
743 "rust-const-context-not-instrumented",
744 "Runtime probes cannot execute in const fn or compile-time evaluation",
745 );
746 }
747 if root
748 .descendants()
749 .any(|node| bears_obligation(&node) && in_global_allocator(&node))
750 {
751 self.limitation(
752 "rust-global-allocator-not-instrumented",
753 "Probing a GlobalAlloc implementation recurses into itself, because the runtime allocates",
754 );
755 }
756
757 if let Some(error) = self.error {
758 return Err(error);
759 }
760 self.manifest
761 .decisions
762 .sort_by(|left, right| left.id.cmp(&right.id));
763 self.manifest
764 .points
765 .sort_by(|left, right| left.id.cmp(&right.id));
766 self.manifest
767 .branches
768 .sort_by(|left, right| left.id.cmp(&right.id));
769 self.manifest.limitations.sort_by(|left, right| {
770 left.get("id")
771 .and_then(|value| value.as_str())
772 .cmp(&right.get("id").and_then(|value| value.as_str()))
773 });
774 Ok(self.manifest)
775 }
776}
777
778pub const FAILED_TRANSFORM_DUMP_ENV: &str = "SUPERCOV_RUST_DUMP_FAILED_INSTRUMENTATION";
781
782const EXPRESSION_MACROS: &[&str] = &[
791 "assert",
792 "debug_assert",
793 "assert_eq",
794 "assert_ne",
795 "debug_assert_eq",
796 "debug_assert_ne",
797 "println",
798 "print",
799 "eprintln",
800 "eprint",
801 "format",
802 "format_args",
803 "write",
804 "writeln",
805 "panic",
806 "unreachable",
807 "todo",
808 "unimplemented",
809 "vec",
810 "dbg",
811 "matches",
812];
813
814const ASSERTION_MACROS: &[&str] = &["assert", "debug_assert"];
816
817struct ExpressionView {
821 text: String,
822 assertions: Vec<TextRange>,
823 matches: Vec<TextRange>,
825}
826
827const EDITIONS: [Edition; 4] = [
833 Edition::Edition2024,
834 Edition::Edition2021,
835 Edition::Edition2018,
836 Edition::Edition2015,
837];
838
839fn parse_any_edition(source: &str) -> Result<(SourceFile, Edition), Vec<String>> {
840 let mut newest_errors = None;
841 for edition in EDITIONS {
842 let parsed = SourceFile::parse(source, edition);
843 let errors = parsed.errors();
844 if errors.is_empty() {
845 return Ok((parsed.tree(), edition));
846 }
847 newest_errors.get_or_insert_with(|| {
848 errors
849 .into_iter()
850 .map(|error| error.to_string())
851 .collect::<Vec<_>>()
852 });
853 }
854 Err(newest_errors.unwrap_or_default())
855}
856
857fn expression_view(source: &str, edition: Edition) -> ExpressionView {
858 let mut text = source.to_owned();
859 let mut assertions = Vec::new();
860 let mut matches = Vec::new();
861 for _ in 0..16 {
864 let tree = SourceFile::parse(&text, edition).tree();
865 let Some(next) =
866 rewrite_expression_macros(&text, &tree, edition, &mut assertions, &mut matches)
867 else {
868 break;
869 };
870 text = next;
871 }
872 ExpressionView {
873 text,
874 assertions,
875 matches,
876 }
877}
878
879fn is_statement_macro(call: &ast::MacroCall) -> bool {
882 call.syntax().parent().is_some_and(|parent| {
883 ast::MacroExpr::can_cast(parent.kind())
884 && parent.parent().is_some_and(|grandparent| {
885 ast::ExprStmt::can_cast(grandparent.kind())
886 || ast::StmtList::can_cast(grandparent.kind())
887 })
888 })
889}
890
891fn first_top_level_comma_end(arguments: &ast::TokenTree) -> Option<usize> {
893 arguments
894 .syntax()
895 .children_with_tokens()
896 .filter_map(|element| element.into_token())
897 .find(|token| token.kind() == SyntaxKind::COMMA)
898 .map(|token| usize::from(token.text_range().end()))
899}
900
901fn rewrite_expression_macros(
904 source: &str,
905 tree: &SourceFile,
906 edition: Edition,
907 assertions: &mut Vec<TextRange>,
908 matches: &mut Vec<TextRange>,
909) -> Option<String> {
910 let mut text = source.as_bytes().to_vec();
911 let mut changed = false;
912 for call in tree.syntax().descendants().filter_map(ast::MacroCall::cast) {
913 let Some(name) = call
914 .path()
915 .and_then(|path| path.segment())
916 .and_then(|segment| segment.name_ref())
917 .map(|name| name.text().to_string())
918 else {
919 continue;
920 };
921 if !EXPRESSION_MACROS.contains(&name.as_str()) {
922 continue;
923 }
924 let (Some(bang), Some(arguments)) = (call.excl_token(), call.token_tree()) else {
925 continue;
926 };
927 let parenthesised = arguments.l_paren_token().is_some();
928 if !parenthesised && arguments.l_brack_token().is_none() {
929 continue;
930 }
931 let range = arguments.syntax().text_range();
932 let (start, end) = (usize::from(range.start()), usize::from(range.end()));
933 let mut rewritten = source.as_bytes()[start..end].to_vec();
934 if !parenthesised {
935 rewritten[0] = b'(';
936 *rewritten
937 .last_mut()
938 .expect("a token tree has a closing delimiter") = b')';
939 }
940 if name == "matches" {
941 let Some(comma_end) = first_top_level_comma_end(&arguments) else {
944 continue;
945 };
946 for byte in &mut rewritten[comma_end - start..end - start - 1] {
947 if *byte != b'\n' {
948 *byte = b' ';
949 }
950 }
951 }
952 let probe = format!(
954 "fn __supercov() {{ let _ = __f{}; }}",
955 String::from_utf8_lossy(&rewritten)
956 );
957 if !SourceFile::parse(&probe, edition).errors().is_empty() {
958 if !parenthesised && !is_statement_macro(&call) {
962 let array = source.as_bytes()[start..end].to_vec();
963 let array_probe = format!(
964 "fn __supercov() {{ let _ = {}; }}",
965 String::from_utf8_lossy(&array)
966 );
967 if SourceFile::parse(&array_probe, edition).errors().is_empty() {
968 let prefix_start = usize::from(call.syntax().text_range().start());
969 let prefix_start = call.attrs().last().map_or(prefix_start, |attribute| {
970 usize::from(attribute.syntax().text_range().end())
971 });
972 for byte in &mut text[prefix_start..start] {
973 if *byte != b'\n' {
974 *byte = b' ';
975 }
976 }
977 changed = true;
978 }
979 }
980 continue;
981 }
982 text[usize::from(bang.text_range().start())] = b'_';
983 text[start..end].copy_from_slice(&rewritten);
984 changed = true;
985 if ASSERTION_MACROS.contains(&name.as_str()) {
986 assertions.push(range);
987 }
988 if name == "matches" {
989 matches.push(call.syntax().text_range());
990 }
991 }
992 changed.then(|| String::from_utf8(text).expect("rewriting ASCII keeps the source UTF-8"))
993}
994
995fn assertion_argument_count(root: &ra_ap_syntax::SyntaxNode, arguments: TextRange) -> usize {
998 root.descendants()
999 .find(|node| node.text_range() == arguments && ast::ArgList::can_cast(node.kind()))
1000 .and_then(ast::ArgList::cast)
1001 .map_or(0, |list| list.args().count())
1002}
1003
1004fn assertion_condition(root: &ra_ap_syntax::SyntaxNode, arguments: TextRange) -> Option<ast::Expr> {
1007 root.descendants()
1008 .find(|node| node.text_range() == arguments && ast::ArgList::can_cast(node.kind()))
1009 .and_then(ast::ArgList::cast)?
1010 .args()
1011 .next()
1012}
1013
1014struct ParsedSource {
1019 tree: SourceFile,
1020 assertions: Vec<TextRange>,
1021 matches: Vec<TextRange>,
1022 edition: Edition,
1023}
1024
1025fn parse_for_instrumentation(source: &str) -> Result<ParsedSource, RustInstrumenterError> {
1026 if source.len() > u32::MAX as usize {
1027 return Err(RustInstrumenterError::SourceTooLarge);
1028 }
1029 let (tree, edition) = parse_any_edition(source).map_err(RustInstrumenterError::Parse)?;
1030 let view = expression_view(source, edition);
1031 let parsed_view = SourceFile::parse(&view.text, edition);
1032 if parsed_view.errors().is_empty() {
1033 Ok(ParsedSource {
1034 tree: parsed_view.tree(),
1035 assertions: view.assertions,
1036 matches: view.matches,
1037 edition,
1038 })
1039 } else {
1040 Ok(ParsedSource {
1041 tree,
1042 assertions: Vec::new(),
1043 matches: Vec::new(),
1044 edition,
1045 })
1046 }
1047}
1048
1049fn standalone_matches(
1053 root: &ra_ap_syntax::SyntaxNode,
1054 matches: &[TextRange],
1055 assertions: &[TextRange],
1056) -> Vec<ast::Expr> {
1057 let mut atoms = Vec::new();
1058 let mut conditions = Vec::new();
1059 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
1060 conditions.extend(expression.condition());
1061 }
1062 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
1063 conditions.extend(expression.condition());
1064 }
1065 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
1066 conditions.extend(guard.condition());
1067 }
1068 for arguments in assertions {
1069 conditions.extend(assertion_condition(root, *arguments));
1070 }
1071 for condition in &conditions {
1072 RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
1073 }
1074 matches
1075 .iter()
1076 .filter(|range| !atoms.contains(range))
1077 .filter_map(|range| {
1078 root.descendants()
1079 .find(|node| node.text_range() == *range && ast::CallExpr::can_cast(node.kind()))
1080 .and_then(ast::Expr::cast)
1081 })
1082 .collect()
1083}
1084
1085pub fn build_rust_manifest(
1086 file: &str,
1087 source: &str,
1088) -> Result<CoverageManifest, RustInstrumenterError> {
1089 let parsed = parse_for_instrumentation(source)?;
1090 RustObligationCollector::new(file, source).collect(
1091 &parsed.tree,
1092 &parsed.assertions,
1093 &parsed.matches,
1094 )
1095}
1096
1097fn block_entry_offset(block: &ast::BlockExpr) -> Option<usize> {
1098 let list = block.stmt_list()?;
1099 list.attrs()
1100 .last()
1101 .map(|attribute| usize::from(attribute.syntax().text_range().end()))
1102 .or_else(|| {
1103 list.l_curly_token()
1104 .map(|token| usize::from(token.text_range().end()))
1105 })
1106}
1107
1108fn range_after_attributes(node: &impl HasAttrs) -> TextRange {
1111 let range = node.syntax().text_range();
1112 node.attrs().last().map_or(range, |attribute| {
1113 TextRange::new(attribute.syntax().text_range().end(), range.end())
1114 })
1115}
1116
1117fn has_let(expression: &ast::Expr) -> bool {
1118 expression
1119 .syntax()
1120 .descendants()
1121 .any(|node| ast::LetExpr::can_cast(node.kind()))
1122}
1123
1124enum ChainHost<'a> {
1126 If(&'a ast::IfExpr),
1127 While(&'a ast::WhileExpr),
1128}
1129
1130fn allocate_chain_table_name(
1132 file: &str,
1133 condition: &ast::Expr,
1134 identifiers: &mut BTreeSet<String>,
1135) -> String {
1136 let id = stable_id(file, "chain", condition.syntax().text_range(), "operators");
1137 let suffix = id
1138 .rsplit(':')
1139 .next()
1140 .unwrap_or("chain")
1141 .to_ascii_uppercase();
1142 let base = format!("__SUPERCOV_CHAIN_{suffix}");
1143 let mut candidate = base.clone();
1144 let mut attempt = 0_usize;
1145 while !identifiers.insert(candidate.clone()) {
1146 attempt += 1;
1147 candidate = format!("{base}_{attempt}");
1148 }
1149 candidate
1150}
1151
1152fn allocate_identifier(
1154 file: &str,
1155 range: TextRange,
1156 kind: &str,
1157 identifiers: &mut BTreeSet<String>,
1158) -> String {
1159 let id = stable_id(file, kind, range, "");
1160 let suffix = id.rsplit(':').next().unwrap_or(kind);
1161 let base = format!("__supercov_{kind}_{suffix}");
1162 let mut candidate = base.clone();
1163 let mut attempt = 0_usize;
1164 while !identifiers.insert(candidate.clone()) {
1165 attempt += 1;
1166 candidate = format!("{base}_{attempt}");
1167 }
1168 candidate
1169}
1170
1171fn own_breaks(body: &ast::BlockExpr, label: Option<ast::Label>) -> Vec<TextRange> {
1175 let own_label = label
1176 .and_then(|label| label.lifetime())
1177 .map(|lifetime| lifetime.text().to_string());
1178 body.syntax()
1179 .descendants()
1180 .filter_map(ast::BreakExpr::cast)
1181 .filter(|expression| match expression.lifetime() {
1182 Some(lifetime) => own_label.as_deref() == Some(lifetime.text().to_string().as_str()),
1183 None => !expression
1184 .syntax()
1185 .ancestors()
1186 .skip(1)
1187 .take_while(|ancestor| ancestor != body.syntax())
1188 .any(|ancestor| {
1189 ast::LoopExpr::can_cast(ancestor.kind())
1190 || ast::WhileExpr::can_cast(ancestor.kind())
1191 || ast::ForExpr::can_cast(ancestor.kind())
1192 || ast::ClosureExpr::can_cast(ancestor.kind())
1193 }),
1194 })
1195 .map(|expression| expression.syntax().text_range())
1196 .collect()
1197}
1198
1199fn instrument_let_chain(
1215 insertions: &mut Vec<Insertion>,
1216 runtime_path: &str,
1217 file: &str,
1218 condition: &ast::Expr,
1219 host: ChainHost<'_>,
1220 identifiers: &mut BTreeSet<String>,
1221) {
1222 let (kind, host_range, body, label) = match &host {
1225 ChainHost::If(expression) => (
1226 "if",
1227 range_after_attributes(*expression),
1228 expression.then_branch(),
1229 None,
1230 ),
1231 ChainHost::While(expression) => (
1232 "while",
1233 range_after_attributes(*expression),
1234 expression.loop_body(),
1235 expression.label(),
1236 ),
1237 };
1238 let Some(body) = body else {
1239 return;
1240 };
1241 let Some(body_offset) = block_entry_offset(&body) else {
1242 return;
1243 };
1244 let range = condition.syntax().text_range();
1245 let id = stable_id(file, "decision", range, kind);
1246 let mut atoms = Vec::new();
1247 RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
1248 let lets = condition
1249 .syntax()
1250 .descendants()
1251 .filter_map(ast::LetExpr::cast)
1252 .map(|expression| expression.syntax().text_range())
1253 .collect::<Vec<_>>();
1254 let frame = allocate_frame_name(file, condition, kind, identifiers);
1255 let table = allocate_chain_table_name(file, condition, identifiers);
1256 let single_let = atoms.len() == 1;
1257 let broke = match &host {
1258 ChainHost::While(_) if single_let => {
1259 Some(allocate_identifier(file, range, "broke", identifiers))
1260 }
1261 _ => None,
1262 };
1263
1264 let mut operators = Vec::new();
1265 for binary in condition
1266 .syntax()
1267 .descendants()
1268 .filter_map(ast::BinExpr::cast)
1269 {
1270 if !matches!(binary.op_kind(), Some(BinaryOp::LogicOp(LogicOp::And))) {
1273 continue;
1274 }
1275 let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
1276 continue;
1277 };
1278 if !has_let(&left) {
1279 continue;
1280 }
1281 let branch = stable_id(file, "branch", right.syntax().text_range(), "logical-and");
1282 let right_range = right.syntax().text_range();
1283 let Some(first) = atoms
1284 .iter()
1285 .position(|atom| right_range.contains_range(*atom))
1286 else {
1287 continue;
1288 };
1289 operators.push(format!(
1290 "({first}, {:?}, {:?})",
1291 format!("{branch}:short-circuit"),
1292 format!("{branch}:evaluated")
1293 ));
1294 }
1295
1296 let mark = format!("{runtime_path}::reached(&mut {frame}, 0);");
1297 let record_false = format!("{runtime_path}::decision_chain(&mut {frame}, false, {table});");
1298 let mut prefix = format!(
1299 "{{ const {table}: &[(usize, &str, &str)] = &[{}]; let mut {frame} = {runtime_path}::DecisionFrame::new({id:?}, {}); ",
1300 operators.join(", "),
1301 atoms.len()
1302 );
1303 if single_let {
1304 prefix.push_str(&mark);
1305 prefix.push(' ');
1306 }
1307 if let Some(broke) = &broke {
1308 prefix.push_str(&format!("let mut {broke} = false; "));
1309 }
1310 let suffix = match &host {
1311 ChainHost::If(expression) => match expression.else_branch() {
1312 Some(_) => " }".to_owned(),
1313 None => format!(" else {{ {record_false} }} }}"),
1314 },
1315 ChainHost::While(_) => match &broke {
1316 Some(broke) => format!(" if !{broke} {{ {mark} {record_false} }} }}"),
1317 None => format!(" {record_false} }}"),
1318 },
1319 };
1320 push_wrapper(insertions, host_range, host_range, 1, prefix, suffix);
1321 if !single_let {
1322 push_direct(
1323 insertions,
1324 usize::from(range.start()),
1325 format!("{runtime_path}::reached(&mut {frame}, 0) && "),
1326 );
1327 }
1328 for (index, atom) in atoms.iter().enumerate() {
1329 if lets.contains(atom) {
1330 if index > 0 {
1331 push_direct(
1332 insertions,
1333 usize::from(atom.start()),
1334 format!("{runtime_path}::reached(&mut {frame}, {index}) && "),
1335 );
1336 }
1337 } else {
1338 push_wrapper(
1339 insertions,
1340 *atom,
1341 *atom,
1342 1,
1343 format!("{runtime_path}::condition(("),
1344 format!("), &mut {frame}, {index})"),
1345 );
1346 }
1347 }
1348 let mut entry = String::new();
1349 if broke.is_some() {
1350 entry.push_str(&format!("\n{mark}"));
1351 }
1352 entry.push_str(&format!(
1353 "\n{runtime_path}::decision_chain(&mut {frame}, true, {table});"
1354 ));
1355 push_direct(insertions, body_offset, entry);
1356 if let Some(broke) = &broke {
1357 for break_range in own_breaks(&body, label) {
1358 push_wrapper(
1359 insertions,
1360 break_range,
1361 break_range,
1362 0,
1363 format!("{{ {broke} = true; "),
1364 " }".into(),
1365 );
1366 }
1367 }
1368 if let ChainHost::If(expression) = &host {
1369 match expression.else_branch() {
1370 Some(ast::ElseBranch::Block(block)) => {
1371 if let Some(offset) = block_entry_offset(&block) {
1372 push_direct(insertions, offset, format!("\n{record_false}"));
1373 }
1374 }
1375 Some(ast::ElseBranch::IfExpr(nested)) => {
1376 let nested_range = nested.syntax().text_range();
1377 push_wrapper(
1378 insertions,
1379 nested_range,
1380 nested_range,
1381 0,
1382 format!("{{ {record_false} "),
1383 " }".into(),
1384 );
1385 }
1386 None => {}
1387 }
1388 }
1389}
1390
1391fn enclosing_block_entry(node: &ra_ap_syntax::SyntaxNode) -> Option<usize> {
1395 node.ancestors()
1396 .skip(1)
1397 .find_map(ast::BlockExpr::cast)
1398 .and_then(|block| block_entry_offset(&block))
1399}
1400
1401fn plain_block(block: &ast::BlockExpr) -> bool {
1405 block
1406 .syntax()
1407 .first_token()
1408 .is_some_and(|token| token.kind() == SyntaxKind::L_CURLY)
1409}
1410
1411fn instrument_decision(
1412 insertions: &mut Vec<Insertion>,
1413 runtime_path: &str,
1414 file: &str,
1415 condition: &ast::Expr,
1416 kind: &str,
1417 frame_name: &str,
1418) -> bool {
1419 if cannot_carry_probe(condition.syntax())
1420 || condition
1421 .syntax()
1422 .descendants()
1423 .any(|node| ast::LetExpr::can_cast(node.kind()))
1424 {
1425 return false;
1426 }
1427 let range = condition.syntax().text_range();
1428 let id = stable_id(file, "decision", range, kind);
1429 let mut condition_ranges = Vec::new();
1430 RustObligationCollector::atomic_condition_ranges(condition, &mut condition_ranges);
1431 push_wrapper(
1432 insertions,
1433 range,
1434 range,
1435 0,
1436 format!(
1437 "({{ let mut {frame_name} = {runtime_path}::DecisionFrame::new({id:?}, {}); {runtime_path}::decision((",
1438 condition_ranges.len()
1439 ),
1440 format!("), &mut {frame_name}) }})"),
1441 );
1442 for (index, atomic_range) in condition_ranges.into_iter().enumerate() {
1446 push_wrapper(
1447 insertions,
1448 atomic_range,
1449 atomic_range,
1450 1,
1451 format!("{runtime_path}::condition(("),
1452 format!("), &mut {frame_name}, {index})"),
1453 );
1454 }
1455 true
1456}
1457
1458pub fn instrument_rust_source(
1468 file: &str,
1469 source: &str,
1470 runtime_path: &str,
1471) -> Result<RustInstrumentedSource, RustInstrumenterError> {
1472 if !valid_runtime_path(runtime_path) {
1473 return Err(RustInstrumenterError::InvalidRuntimePath);
1474 }
1475 let mut manifest = build_rust_manifest(file, source)?;
1476 let ParsedSource {
1477 tree,
1478 assertions,
1479 matches,
1480 edition,
1481 } = parse_for_instrumentation(source)?;
1482 let root = tree.syntax();
1483 let mut insertions = Vec::new();
1484 let mut identifiers = root
1485 .descendants_with_tokens()
1486 .filter_map(|element| element.into_token())
1487 .filter(|token| token.kind() == SyntaxKind::IDENT)
1488 .map(|token| token.text().to_string())
1489 .collect::<BTreeSet<_>>();
1490
1491 let mut skipped_attributed_statement = false;
1492 let attributed_probe = |insertions: &mut Vec<Insertion>,
1503 skipped: &mut bool,
1504 expression: Option<ast::Expr>,
1505 has_attrs: bool,
1506 trailing: bool,
1507 range: TextRange,
1508 id: String| {
1509 if !has_attrs {
1510 push_direct(
1511 insertions,
1512 usize::from(range.start()),
1513 format!("{runtime_path}::hit({id:?});"),
1514 );
1515 return;
1516 }
1517 let Some(expression) = expression else {
1518 *skipped = true;
1519 return;
1520 };
1521 if let ast::Expr::BlockExpr(block) = &expression
1522 && let Some(offset) = block_entry_offset(block)
1523 {
1524 push_direct(
1525 insertions,
1526 offset,
1527 format!("\n{runtime_path}::hit({id:?});"),
1528 );
1529 return;
1530 }
1531 if trailing && let ast::Expr::MacroExpr(_) = &expression {
1542 if expression.attrs().any(|attribute| {
1543 attribute.meta().is_some_and(|meta| {
1546 let text = meta.syntax().text().to_string();
1547 let name = text
1548 .chars()
1549 .take_while(|character| character.is_alphanumeric() || *character == '_')
1550 .collect::<String>();
1551 matches!(name.as_str(), "cfg" | "cfg_attr")
1552 })
1553 }) {
1554 *skipped = true;
1555 return;
1556 }
1557 let range = expression.syntax().text_range();
1558 push_wrapper(
1559 insertions,
1560 range,
1561 range,
1562 0,
1563 format!("{{ {runtime_path}::hit({id:?}); "),
1564 " }".into(),
1565 );
1566 return;
1567 }
1568 let start = expression.attrs().last().map_or_else(
1574 || expression.syntax().text_range().start(),
1575 |attribute| attribute.syntax().text_range().end(),
1576 );
1577 let wrapped = TextRange::new(start, expression.syntax().text_range().end());
1578 push_wrapper(
1579 insertions,
1580 wrapped,
1581 wrapped,
1582 0,
1583 format!(" {{ {runtime_path}::hit({id:?}); ("),
1584 ") }".into(),
1585 );
1586 };
1587 for list in root.descendants().filter_map(ast::StmtList::cast) {
1588 let last_statement = list.statements().last();
1589 for statement in list.statements() {
1590 let (range, expression, has_attrs, trailing) = match &statement {
1591 ast::Stmt::ExprStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
1592 let expression = statement.expr();
1593 let has_attrs = expression
1596 .as_ref()
1597 .is_some_and(|expression| expression.attrs().next().is_some());
1598 let trailing = statement.semicolon_token().is_none()
1601 && list.tail_expr().is_none()
1602 && last_statement.as_ref() == Some(&ast::Stmt::ExprStmt(statement.clone()));
1603 (
1604 statement.syntax().text_range(),
1605 expression,
1606 has_attrs,
1607 trailing,
1608 )
1609 }
1610 ast::Stmt::LetStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
1611 let has_attrs = statement.attrs().next().is_some();
1612 let initializer = has_attrs.then(|| statement.initializer()).flatten();
1615 (
1616 statement.syntax().text_range(),
1617 initializer,
1618 has_attrs,
1619 false,
1620 )
1621 }
1622 _ => continue,
1623 };
1624 let id = stable_id(file, "statement", range, "");
1625 attributed_probe(
1626 &mut insertions,
1627 &mut skipped_attributed_statement,
1628 expression,
1629 has_attrs,
1630 trailing,
1631 range,
1632 id,
1633 );
1634 }
1635 if let Some(tail) = list
1636 .tail_expr()
1637 .filter(|tail| !cannot_carry_probe(tail.syntax()))
1638 {
1639 let range = tail.syntax().text_range();
1640 let id = stable_id(file, "statement", range, "");
1641 let has_attrs = tail.attrs().next().is_some();
1642 attributed_probe(
1643 &mut insertions,
1644 &mut skipped_attributed_statement,
1645 Some(tail),
1646 has_attrs,
1647 true,
1648 range,
1649 id,
1650 );
1651 }
1652 }
1653
1654 for function in root.descendants().filter_map(ast::Fn::cast) {
1655 if cannot_carry_probe(function.syntax()) {
1658 continue;
1659 }
1660 let Some(body) = function.body() else {
1661 continue;
1662 };
1663 let label = function.name().map(|name| name.text().to_string());
1664 let id = stable_id(
1665 file,
1666 "function",
1667 function.syntax().text_range(),
1668 label.as_deref().unwrap_or(""),
1669 );
1670 if let Some(offset) = block_entry_offset(&body) {
1671 push_direct(
1672 &mut insertions,
1673 offset,
1674 format!("\n{runtime_path}::hit({id:?});"),
1675 );
1676 }
1677 }
1678
1679 for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
1680 let Some(body) = closure.body() else {
1681 continue;
1682 };
1683 if cannot_carry_probe(body.syntax()) {
1684 continue;
1685 }
1686 let id = stable_id(file, "function", closure.syntax().text_range(), "<closure>");
1687 if let ast::Expr::BlockExpr(block) = &body {
1688 if let Some(offset) = block_entry_offset(block) {
1689 push_direct(
1690 &mut insertions,
1691 offset,
1692 format!("\n{runtime_path}::hit({id:?});"),
1693 );
1694 }
1695 } else {
1696 let range = body.syntax().text_range();
1697 push_wrapper(
1698 &mut insertions,
1699 range,
1700 closure.syntax().text_range(),
1701 0,
1702 format!("{{ {runtime_path}::hit({id:?}); ("),
1703 ") }".into(),
1704 );
1705 }
1706 }
1707
1708 for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
1715 if cannot_carry_probe(expression.syntax()) {
1716 continue;
1717 }
1718 let Some(list) = expression.match_arm_list() else {
1719 continue;
1720 };
1721 let arms = list.arms().collect::<Vec<_>>();
1722 if arms.is_empty() {
1723 continue;
1724 }
1725 let Some(table_offset) = enclosing_block_entry(expression.syntax()) else {
1726 continue;
1727 };
1728 let table = allocate_table_name(file, &expression, &mut identifiers);
1729 let entries = arms
1730 .iter()
1731 .map(|arm| {
1732 let id = stable_id(file, "branch", arm.syntax().text_range(), "match-arm");
1733 format!(
1734 "{:?}, {:?}",
1735 format!("{id}:missed"),
1736 format!("{id}:selected")
1737 )
1738 })
1739 .collect::<Vec<_>>()
1740 .join(", ");
1741 push_direct(
1742 &mut insertions,
1743 table_offset,
1744 format!("\nconst {table}: &[&str] = &[{entries}];"),
1745 );
1746 for (index, arm) in arms.iter().enumerate() {
1747 let Some(body) = arm.expr() else {
1748 continue;
1749 };
1750 let call = format!("{runtime_path}::arms({table}, {index});");
1751 match &body {
1752 ast::Expr::BlockExpr(block) if plain_block(block) => {
1753 if let Some(offset) = block_entry_offset(block) {
1754 push_direct(&mut insertions, offset, format!("\n{call}"));
1755 }
1756 }
1757 _ => push_wrapper(
1758 &mut insertions,
1759 body.syntax().text_range(),
1760 arm.syntax().text_range(),
1761 0,
1762 format!("{{ {call} ("),
1763 ") }".into(),
1764 ),
1765 }
1766 }
1767 }
1768
1769 for binary in root.descendants().filter_map(ast::BinExpr::cast) {
1773 let short_circuits_when = match binary.op_kind() {
1774 Some(BinaryOp::LogicOp(LogicOp::And)) => false,
1775 Some(BinaryOp::LogicOp(LogicOp::Or)) => true,
1776 _ => continue,
1777 };
1778 if cannot_carry_probe(binary.syntax()) {
1779 continue;
1780 }
1781 let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
1782 continue;
1783 };
1784 if has_let(&left) {
1788 continue;
1789 }
1790 let kind = if short_circuits_when {
1791 "logical-or"
1792 } else {
1793 "logical-and"
1794 };
1795 let id = stable_id(file, "branch", right.syntax().text_range(), kind);
1796 push_wrapper(
1797 &mut insertions,
1798 left.syntax().text_range(),
1799 binary.syntax().text_range(),
1800 2,
1801 format!("{runtime_path}::logical(("),
1802 format!(
1803 "), {short_circuits_when}, {:?}, {:?})",
1804 format!("{id}:short-circuit"),
1805 format!("{id}:evaluated")
1806 ),
1807 );
1808 }
1809
1810 for expression in root.descendants().filter_map(ast::ForExpr::cast) {
1814 if cannot_carry_probe(expression.syntax()) {
1815 continue;
1816 }
1817 let Some(iterable) = expression.iterable() else {
1818 continue;
1819 };
1820 let id = stable_id(file, "branch", expression.syntax().text_range(), "for-loop");
1821 push_wrapper(
1825 &mut insertions,
1826 iterable.syntax().text_range(),
1827 iterable.syntax().text_range(),
1828 0,
1829 format!("{runtime_path}::for_loop(("),
1830 format!(
1831 "), {:?}, {:?})",
1832 format!("{id}:zero"),
1833 format!("{id}:entered")
1834 ),
1835 );
1836 }
1837
1838 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
1842 if cannot_carry_probe(expression.syntax()) {
1843 continue;
1844 }
1845 let Some(offset) = expression.loop_body().as_ref().and_then(block_entry_offset) else {
1846 continue;
1847 };
1848 let id = stable_id(
1849 file,
1850 "branch",
1851 expression.syntax().text_range(),
1852 "while-loop",
1853 );
1854 let flag = allocate_flag_name(file, &expression, &mut identifiers);
1855 let range = range_after_attributes(&expression);
1856 push_wrapper(
1857 &mut insertions,
1858 range,
1859 range,
1860 0,
1861 format!("{{ let mut {flag} = true; "),
1862 format!(
1863 " {runtime_path}::zero_iterations({flag}, {:?}) }}",
1864 format!("{id}:zero")
1865 ),
1866 );
1867 push_direct(
1868 &mut insertions,
1869 offset,
1870 format!(
1871 "\n{runtime_path}::entered(&mut {flag}, {:?});",
1872 format!("{id}:entered")
1873 ),
1874 );
1875 }
1876
1877 for expression in root.descendants().filter_map(ast::TryExpr::cast) {
1881 if cannot_carry_probe(expression.syntax()) {
1882 continue;
1883 }
1884 let Some(operand) = expression.expr() else {
1885 continue;
1886 };
1887 let id = stable_id(
1888 file,
1889 "branch",
1890 expression.syntax().text_range(),
1891 "try-operator",
1892 );
1893 push_wrapper(
1896 &mut insertions,
1897 operand.syntax().text_range(),
1898 operand.syntax().text_range(),
1899 0,
1900 format!("{runtime_path}::TryProbe::probe(("),
1901 format!(
1902 "), {:?}, {:?})",
1903 format!("{id}:continued"),
1904 format!("{id}:returned")
1905 ),
1906 );
1907 }
1908
1909 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
1910 let Some(condition) = expression.condition() else {
1911 continue;
1912 };
1913 if has_let(&condition) {
1914 if !cannot_carry_probe(condition.syntax()) {
1915 instrument_let_chain(
1916 &mut insertions,
1917 runtime_path,
1918 file,
1919 &condition,
1920 ChainHost::If(&expression),
1921 &mut identifiers,
1922 );
1923 }
1924 continue;
1925 }
1926 let frame_name = allocate_frame_name(file, &condition, "if", &mut identifiers);
1927 instrument_decision(
1928 &mut insertions,
1929 runtime_path,
1930 file,
1931 &condition,
1932 "if",
1933 &frame_name,
1934 );
1935 }
1936 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
1937 let Some(condition) = expression.condition() else {
1938 continue;
1939 };
1940 if has_let(&condition) {
1941 if !cannot_carry_probe(condition.syntax()) {
1942 instrument_let_chain(
1943 &mut insertions,
1944 runtime_path,
1945 file,
1946 &condition,
1947 ChainHost::While(&expression),
1948 &mut identifiers,
1949 );
1950 }
1951 continue;
1952 }
1953 let frame_name = allocate_frame_name(file, &condition, "while", &mut identifiers);
1954 instrument_decision(
1955 &mut insertions,
1956 runtime_path,
1957 file,
1958 &condition,
1959 "while",
1960 &frame_name,
1961 );
1962 }
1963 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
1964 if let Some(condition) = guard.condition() {
1965 let frame_name = allocate_frame_name(file, &condition, "match-guard", &mut identifiers);
1966 instrument_decision(
1967 &mut insertions,
1968 runtime_path,
1969 file,
1970 &condition,
1971 "match-guard",
1972 &frame_name,
1973 );
1974 }
1975 }
1976 for arguments in &assertions {
1983 let Some(condition) = assertion_condition(root, *arguments) else {
1984 continue;
1985 };
1986 let frame_name = allocate_frame_name(file, &condition, "assert", &mut identifiers);
1987 if !instrument_decision(
1988 &mut insertions,
1989 runtime_path,
1990 file,
1991 &condition,
1992 "assert",
1993 &frame_name,
1994 ) {
1995 continue;
1996 }
1997 if assertion_argument_count(root, *arguments) == 1 {
1998 let range = condition.syntax().text_range();
1999 let original = &source[usize::from(range.start())..usize::from(range.end())];
2000 push_direct(
2001 &mut insertions,
2002 usize::from(range.end()),
2003 format!(", \"assertion failed: {{}}\", stringify!({original})"),
2004 );
2005 }
2006 }
2007 for expression in standalone_matches(root, &matches, &assertions) {
2010 let frame_name = allocate_frame_name(file, &expression, "matches", &mut identifiers);
2011 instrument_decision(
2012 &mut insertions,
2013 runtime_path,
2014 file,
2015 &expression,
2016 "matches",
2017 &frame_name,
2018 );
2019 }
2020
2021 if skipped_attributed_statement {
2022 add_manifest_limitation(
2023 &mut manifest,
2024 file,
2025 "rust-attributed-statement-probes-not-injected",
2026 "A `let` without an initializer that carries outer attributes has no expression to hold a probe",
2027 );
2028 }
2029 manifest.limitations.sort_by(|left, right| {
2030 left.get("id")
2031 .and_then(|value| value.as_str())
2032 .cmp(&right.get("id").and_then(|value| value.as_str()))
2033 });
2034
2035 let code = apply_insertions(source, insertions)?;
2036 let transformed = SourceFile::parse(&code, edition);
2037 let errors = transformed
2038 .errors()
2039 .into_iter()
2040 .map(|error| error.to_string())
2041 .collect::<Vec<_>>();
2042 if !errors.is_empty() {
2043 if let Some(directory) = std::env::var_os(FAILED_TRANSFORM_DUMP_ENV) {
2047 let name = file.replace(['/', '\\'], "__");
2048 let _ = std::fs::create_dir_all(&directory);
2049 let _ = std::fs::write(std::path::Path::new(&directory).join(name), &code);
2050 }
2051 return Err(RustInstrumenterError::Parse(errors));
2052 }
2053 Ok(RustInstrumentedSource { code, manifest })
2054}
2055
2056#[cfg(test)]
2057mod tests {
2058 use std::{
2059 fs,
2060 process::Command,
2061 time::{SystemTime, UNIX_EPOCH},
2062 };
2063
2064 use super::*;
2065
2066 const NOOP_RUNTIME: &str = r#"
2067#[doc(hidden)]
2068mod __supercov_runtime_v1 {
2069 pub struct DecisionFrame;
2070 impl DecisionFrame {
2071 pub fn new(_: &'static str, _: usize) -> Self { Self }
2072 }
2073 pub fn hit(_: &'static str) {}
2074 pub fn arms(_: &[&'static str], _: usize) {}
2075 pub fn logical(left: bool, _: bool, _: &'static str, _: &'static str) -> bool { left }
2076 pub fn for_loop<I: IntoIterator>(iterable: I, _: &'static str, _: &'static str) -> I::IntoIter {
2077 iterable.into_iter()
2078 }
2079 pub fn entered(_: &mut bool, _: &'static str) {}
2080 pub fn zero_iterations(_: bool, _: &'static str) {}
2081 pub trait TryProbe: Sized {
2082 fn probe(self, _: &'static str, _: &'static str) -> Self { self }
2083 }
2084 impl<T> TryProbe for T {}
2085 pub fn condition<V: std::ops::Not<Output = bool>>(value: V, _: &mut DecisionFrame, _: usize) -> bool { !!value }
2086 pub fn decision(value: bool, _: &mut DecisionFrame) -> bool { value }
2087 pub fn reached(_: &mut DecisionFrame, _: usize) -> bool { true }
2088 pub fn decision_chain(_: &mut DecisionFrame, _: bool, _: &[(usize, &'static str, &'static str)]) {}
2089}
2090"#;
2091
2092 fn compile_and_run(source: &str, name: &str) -> std::process::Output {
2093 compile_and_run_edition(source, name, "2024")
2094 }
2095
2096 fn compile_and_run_edition(source: &str, name: &str, edition: &str) -> std::process::Output {
2097 let nonce = SystemTime::now()
2098 .duration_since(UNIX_EPOCH)
2099 .unwrap()
2100 .as_nanos();
2101 let directory = std::env::temp_dir().join(format!(
2102 "supercov-rust-transform-{}-{nonce}-{name}",
2103 std::process::id()
2104 ));
2105 fs::create_dir(&directory).unwrap();
2106 let input = directory.join("main.rs");
2107 let binary = directory.join("program");
2108 fs::write(&input, source).unwrap();
2109 let compile = Command::new("rustc")
2110 .arg(format!("--edition={edition}"))
2111 .arg(&input)
2112 .arg("-o")
2113 .arg(&binary)
2114 .output()
2115 .unwrap();
2116 assert!(
2117 compile.status.success(),
2118 "rustc failed:\n{}\nsource:\n{source}",
2119 String::from_utf8_lossy(&compile.stderr)
2120 );
2121 let output = Command::new(&binary).output().unwrap();
2122 fs::remove_dir_all(directory).unwrap();
2123 output
2124 }
2125
2126 #[test]
2127 fn discovers_rust_obligations_with_exact_ranges_and_stable_ids() {
2128 let source = r#"fn classify<T>(values: &[T], first: bool, second: bool, third: bool) -> Option<&T> {
2129 let picked = if first && (second || third) {
2130 values.first()?
2131 } else {
2132 None
2133 };
2134 for value in values {
2135 if first || second {
2136 return Some(value);
2137 }
2138 }
2139 match picked {
2140 Some(value) if second && third => Some(value),
2141 _ => None,
2142 }
2143}
2144
2145fn closure(value: i32) -> bool {
2146 (|candidate| candidate > 0)(value)
2147}
2148"#;
2149 let first = build_rust_manifest("src/lib.rs", source).unwrap();
2150 let second = build_rust_manifest("src/lib.rs", source).unwrap();
2151 assert_eq!(first, second);
2152 assert!(first.points.iter().any(|point| {
2153 point.kind == PointKind::Function && point.label.as_deref() == Some("classify")
2154 }));
2155 assert!(first.points.iter().any(|point| {
2156 point.kind == PointKind::Function && point.label.as_deref() == Some("<closure>")
2157 }));
2158 let first_if = first
2159 .decisions
2160 .iter()
2161 .find(|decision| decision.line == 2)
2162 .unwrap();
2163 assert_eq!(first_if.conditions, ["first", "second", "third"]);
2164 assert_eq!(first_if.column, 20);
2165 assert!(
2166 first
2167 .branches
2168 .iter()
2169 .any(|branch| branch.kind == "for-loop")
2170 );
2171 let mut arms = first
2172 .branches
2173 .iter()
2174 .filter(|branch| branch.kind == "match-arm")
2175 .collect::<Vec<_>>();
2176 arms.sort_by_key(|branch| branch.line);
2177 assert_eq!(arms.len(), 2);
2178 assert_eq!(
2179 arms[0]
2180 .alternatives
2181 .iter()
2182 .map(|alternative| alternative.label.as_str())
2183 .collect::<Vec<_>>(),
2184 ["not selected", "selected"]
2185 );
2186 assert_eq!(
2189 arms[1]
2190 .alternatives
2191 .iter()
2192 .map(|alternative| alternative.label.as_str())
2193 .collect::<Vec<_>>(),
2194 ["selected"]
2195 );
2196 assert!(
2197 first
2198 .branches
2199 .iter()
2200 .any(|branch| branch.kind == "try-operator")
2201 );
2202 assert!(first.decisions.iter().all(|decision| {
2203 decision.id.starts_with("rs:decision:") && decision.conditions.len() >= 2
2204 }));
2205 assert!(first.limitations.is_empty());
2206 }
2207
2208 #[test]
2209 fn declares_macro_and_const_boundaries_instead_of_hiding_them() {
2210 let source = r#"const fn doubled(value: usize) -> usize { value * 2 }
2211
2212macro_rules! noop {
2213 () => {};
2214}
2215
2216fn checked(value: bool) -> bool {
2217 assert!(value);
2218 noop!();
2219 const { doubled(2) == 4 }
2220}
2221"#;
2222 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
2223 assert!(manifest.decisions.iter().any(|decision| {
2226 decision.line == 8 && decision.source == "value" && decision.conditions == ["value"]
2227 }));
2228 let ids = manifest
2229 .limitations
2230 .iter()
2231 .filter_map(|limitation| limitation.get("id")?.as_str())
2232 .collect::<BTreeSet<_>>();
2233 assert_eq!(
2234 ids,
2235 BTreeSet::from([
2236 "rust-const-context-not-instrumented",
2237 "rust-macro-expansion-not-instrumented"
2238 ])
2239 );
2240 assert!(!manifest.points.iter().any(|point| {
2241 point.kind == PointKind::Function && point.label.as_deref() == Some("doubled")
2242 }));
2243 }
2244
2245 #[test]
2246 fn transforms_points_and_nested_decisions_without_changing_behavior() {
2247 let source = r#"use std::sync::atomic::{AtomicUsize, Ordering};
2248
2249static CALLS: AtomicUsize = AtomicUsize::new(0);
2250
2251fn observed(name: &str, value: bool) -> bool {
2252 let order = CALLS.fetch_add(1, Ordering::SeqCst);
2253 println!("{order}:{name}:{value}");
2254 value
2255}
2256
2257fn classify(first: bool, second: bool, third: bool) -> i32 {
2258 if observed("a", first) && (observed("b", second) || observed("c", third)) {
2259 7
2260 } else {
2261 3
2262 }
2263}
2264
2265fn main() {
2266 let closure = |value: i32| value + 1;
2267 println!("result={}", closure(classify(true, false, true)));
2268}
2269"#;
2270 let transformed =
2271 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2272 assert!(transformed.code.contains("::condition("));
2273 assert!(transformed.code.contains("::decision("));
2274 assert!(transformed.code.contains("::hit("));
2275 let original = compile_and_run(source, "original");
2276 let instrumented = compile_and_run(
2277 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2278 "instrumented",
2279 );
2280 assert_eq!(instrumented.status, original.status);
2281 assert_eq!(instrumented.stdout, original.stdout);
2282 assert_eq!(instrumented.stderr, original.stderr);
2283 }
2284
2285 #[test]
2286 fn let_chains_take_derived_condition_probes_and_const_contexts_stay_declared() {
2287 let source = r#"const fn enabled(value: bool) -> bool {
2288 if value { true } else { false }
2289}
2290
2291fn classify(value: Option<bool>, fallback: bool) -> bool {
2292 if let Some(inner) = value && inner && fallback { true } else { false }
2293}
2294"#;
2295 let transformed =
2296 instrument_rust_source("src/lib.rs", source, "crate::__supercov_runtime_v1").unwrap();
2297 let ids = transformed
2298 .manifest
2299 .limitations
2300 .iter()
2301 .filter_map(|limitation| limitation.get("id")?.as_str())
2302 .collect::<BTreeSet<_>>();
2303 assert!(ids.contains("rust-const-context-not-instrumented"));
2304 assert!(!ids.contains("rust-let-chain-probes-not-injected"));
2305 assert!(
2308 transformed
2309 .code
2310 .contains("::reached(&mut __supercov_decision_")
2311 );
2312 assert!(
2313 transformed
2314 .code
2315 .contains("::condition((inner), &mut __supercov_decision_")
2316 );
2317 assert!(
2318 transformed
2319 .code
2320 .contains("::decision_chain(&mut __supercov_decision_")
2321 );
2322 assert!(transformed.code.contains("&& let Some(inner) = value &&"));
2323 assert!(!transformed.code.contains("condition((let"));
2324 }
2325
2326 #[test]
2327 fn std_macro_arguments_take_probes_and_assertions_are_decisions() {
2328 let source = r#"use std::fmt::Write as _;
2329
2330fn classify(values: &[i32], strict: bool) -> String {
2331 let mut out = String::new();
2332 assert!(values.len() < 10 && (strict || !values.is_empty()), "bad input {:?}", values);
2333 debug_assert!(values.iter().all(|v| *v > -100));
2334 let doubled = vec![values.iter().map(|v| v * 2).sum::<i32>(), if strict { 1 } else { 2 }];
2335 let repeated = vec![if strict { 1 } else { 0 }; values.len()];
2336 let small = matches!(values.first(), Some(v) if *v < 3);
2337 if matches!(values.len(), 1 | 2) && small {
2338 println!("small");
2339 }
2340 println!("{}", repeated.len() + small as usize);
2341 write!(out, "{}", doubled.iter().map(|d| if *d > 4 { "big" } else { "small" }).collect::<Vec<_>>().join(",")).unwrap();
2342 println!("{} {}", format!("{:?}", doubled), if values.first().copied().unwrap_or(0) > 0 && strict { "positive" } else { "other" });
2343 assert_eq!(doubled.len(), if strict { 2 } else { 2 }, "length for strict={strict}");
2344 out
2345}
2346
2347fn main() {
2348 println!("{}", classify(&[1, 2], true));
2349 println!("{}", classify(&[3], false));
2350 println!("{}", classify(&[], true));
2351 let total: i32 = dbg!(vec![1, 2, 3]).into_iter().sum();
2352 println!("{total}");
2353}
2354"#;
2355 let transformed =
2356 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2357 assert!(transformed.code.contains(
2360 r#", "assertion failed: {}", stringify!(values.iter().all(|v| *v > -100)))"#
2361 ));
2362 assert!(
2364 transformed
2365 .code
2366 .contains(r#"), "bad input {:?}", values);"#)
2367 );
2368 let assertion = transformed
2372 .manifest
2373 .decisions
2374 .iter()
2375 .find(|decision| decision.line == 5)
2376 .expect("assert! decision");
2377 assert_eq!(
2378 assertion.conditions,
2379 ["values.len() < 10", "strict", "!values.is_empty()"]
2380 );
2381 assert!(
2382 transformed
2383 .manifest
2384 .decisions
2385 .iter()
2386 .any(|decision| decision.line == 7)
2387 );
2388 assert!(
2389 transformed
2390 .manifest
2391 .decisions
2392 .iter()
2393 .any(|decision| decision.line == 9)
2394 );
2395 assert!(
2396 transformed
2397 .code
2398 .contains("assert!(({ let mut __supercov_decision_")
2399 );
2400 assert!(
2401 transformed
2402 .code
2403 .contains(", if ({ let mut __supercov_decision_")
2404 );
2405 assert!(
2407 transformed
2408 .code
2409 .contains("vec![if ({ let mut __supercov_decision_")
2410 );
2411 assert!(
2414 transformed
2415 .code
2416 .contains("let small = ({ let mut __supercov_decision_")
2417 );
2418 assert_eq!(
2419 transformed
2420 .manifest
2421 .decisions
2422 .iter()
2423 .filter(|decision| decision.line == 10)
2424 .count(),
2425 1
2426 );
2427 assert_eq!(
2428 transformed
2429 .manifest
2430 .decisions
2431 .iter()
2432 .filter(|decision| decision.line == 9)
2433 .count(),
2434 1
2435 );
2436 assert!(
2437 transformed
2438 .code
2439 .contains("vec![values.iter().map(|v| { crate::__supercov_runtime_v1::hit(")
2440 );
2441 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
2442 limitation.get("id").and_then(|id| id.as_str())
2443 == Some("rust-macro-expansion-not-instrumented")
2444 }));
2445 let original = compile_and_run(source, "original-macros");
2446 let instrumented = compile_and_run(
2447 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2448 "instrumented-macros",
2449 );
2450 assert_eq!(instrumented.status, original.status);
2451 assert_eq!(instrumented.stdout, original.stdout);
2452 let after_location = |stderr: &[u8]| {
2455 String::from_utf8_lossy(stderr)
2456 .lines()
2457 .map(|line| {
2458 line.split_once("] ")
2459 .map_or(line, |(_, rest)| rest)
2460 .to_owned()
2461 })
2462 .collect::<Vec<_>>()
2463 };
2464 assert_eq!(
2465 after_location(&instrumented.stderr),
2466 after_location(&original.stderr)
2467 );
2468 }
2469
2470 #[test]
2471 fn assertion_panic_messages_survive_instrumentation() {
2472 let source = r#"fn grow(len: usize, new_capacity: usize) {
2475 assert!(new_capacity >= len);
2476}
2477
2478fn check(value: i32) {
2479 assert!(value > 0 && value < 10, "value {value} out of range");
2480}
2481
2482// tokio: `assert!` only negates its operand, so a `&bool` is accepted.
2483fn all_seen(seen: &[bool]) {
2484 for was_seen in seen {
2485 assert!(was_seen);
2486 debug_assert!(was_seen, "seen");
2487 }
2488}
2489
2490fn main() {
2491 std::panic::set_hook(Box::new(|_| {}));
2492 all_seen(&[true, true]);
2493 match std::panic::catch_unwind(|| all_seen(&[true, false])) {
2494 Ok(()) => println!("ok"),
2495 Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
2496 }
2497 for (len, capacity) in [(3, 5), (8, 5)] {
2498 match std::panic::catch_unwind(|| grow(len, capacity)) {
2499 Ok(()) => println!("ok"),
2500 Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
2501 }
2502 }
2503 match std::panic::catch_unwind(|| check(12)) {
2504 Ok(()) => println!("ok"),
2505 Err(payload) => println!("{}", payload.downcast_ref::<String>().cloned().unwrap_or_default()),
2506 }
2507}
2508"#;
2509 let transformed =
2510 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2511 let original = compile_and_run(source, "original-assert-message");
2512 let instrumented = compile_and_run(
2513 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2514 "instrumented-assert-message",
2515 );
2516 assert_eq!(instrumented.status, original.status);
2517 assert_eq!(instrumented.stdout, original.stdout);
2518 assert!(
2519 String::from_utf8_lossy(&instrumented.stdout)
2520 .contains("assertion failed: new_capacity >= len")
2521 );
2522 assert!(
2523 String::from_utf8_lossy(&instrumented.stdout).contains("assertion failed: was_seen")
2524 );
2525 }
2526
2527 #[test]
2528 fn files_that_predate_a_reserved_word_still_instrument() {
2529 let source = r#"struct Rng(u64);
2531impl Rng {
2532 fn gen(&mut self) -> u64 {
2533 self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1);
2534 self.0 >> 33
2535 }
2536}
2537
2538fn main() {
2539 let mut rng = Rng(7);
2540 let mut odd = 0;
2541 for _ in 0..10 {
2542 if rng.gen() % 2 == 1 {
2543 odd += 1;
2544 }
2545 }
2546 println!("{odd}");
2547}
2548"#;
2549 let transformed =
2550 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2551 assert!(
2552 transformed
2553 .manifest
2554 .decisions
2555 .iter()
2556 .any(|decision| decision.line == 13)
2557 );
2558 let original = compile_and_run_edition(source, "original-gen", "2021");
2559 let instrumented = compile_and_run_edition(
2560 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2561 "instrumented-gen",
2562 "2021",
2563 );
2564 assert_eq!(instrumented.status, original.status);
2565 assert_eq!(instrumented.stdout, original.stdout);
2566 }
2567
2568 #[test]
2569 fn lone_let_conditions_compile_before_edition_2024_and_record_breaks() {
2570 let source = r#"fn first_even(values: &[i32]) -> Option<i32> {
2574 let mut it = values.iter();
2575 'scan: while let Some(value) = it.next() {
2576 if *value < 0 {
2577 break;
2578 }
2579 for _ in 0..1 {
2580 if *value == 99 {
2581 break 'scan;
2582 }
2583 if *value == 98 {
2584 break;
2585 }
2586 }
2587 if *value % 2 == 0 {
2588 return Some(*value);
2589 }
2590 }
2591 None
2592}
2593
2594fn describe(value: Option<i32>) -> &'static str {
2595 if let Some(inner) = value {
2596 if inner > 0 { "positive" } else { "non-positive" }
2597 } else if let None = value {
2598 "none"
2599 } else {
2600 "unreachable"
2601 }
2602}
2603
2604fn count(values: &[Option<i32>]) -> usize {
2605 let mut total = 0;
2606 for value in values {
2607 if let Some(_) = value {
2608 total += 1;
2609 }
2610 }
2611 total
2612}
2613
2614fn main() {
2615 println!("{:?} {:?} {:?} {:?}", first_even(&[1, 3, 4]), first_even(&[1, -1, 4]), first_even(&[99, 4]), first_even(&[98, 3, 6]));
2616 println!("{} {} {}", describe(Some(2)), describe(Some(-2)), describe(None));
2617 println!("{}", count(&[Some(1), None, Some(3)]));
2618}
2619"#;
2620 let transformed =
2621 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2622 assert!(!transformed.code.contains("&& let"));
2623 assert!(transformed.code.contains("__supercov_broke_"));
2624 assert_eq!(transformed.code.matches("= true; break").count(), 2);
2625 for edition in ["2021", "2024"] {
2626 let original =
2627 compile_and_run_edition(source, &format!("original-lone-let-{edition}"), edition);
2628 let instrumented = compile_and_run_edition(
2629 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2630 &format!("instrumented-lone-let-{edition}"),
2631 edition,
2632 );
2633 assert_eq!(instrumented.status, original.status);
2634 assert_eq!(instrumented.stdout, original.stdout);
2635 assert_eq!(instrumented.stderr, original.stderr);
2636 }
2637 }
2638
2639 #[test]
2640 fn let_chains_keep_their_behavior() {
2641 let source = r#"fn describe(value: Option<i32>, flag: bool) -> &'static str {
2642 if let Some(inner) = value && inner > 0 && flag {
2643 "positive"
2644 } else if let Some(inner) = value && (inner < 0 || flag) {
2645 "negative-or-flagged"
2646 } else {
2647 "other"
2648 }
2649}
2650
2651fn count_pairs(values: &[(Option<i32>, i32)]) -> i32 {
2652 let mut total = 0;
2653 let mut it = values.iter();
2654 while let Some((first, second)) = it.next() && let Some(inner) = first && *second > 0 {
2655 total += inner * second;
2656 if total > 100 {
2657 break;
2658 }
2659 }
2660 total
2661}
2662
2663fn tail(value: Option<&str>) -> usize {
2664 let pick = |v: Option<&str>| if let Some(text) = v && !text.is_empty() { text.len() } else { 0 };
2665 if let Some(text) = value && text.starts_with('x') {
2666 println!("x-prefixed");
2667 }
2668 pick(value)
2669}
2670
2671fn main() {
2672 for value in [Some(3), Some(-3), Some(0), None] {
2673 for flag in [true, false] {
2674 println!("{value:?} {flag} {}", describe(value, flag));
2675 }
2676 }
2677 println!("{}", count_pairs(&[(Some(2), 3), (Some(4), 5), (None, 1), (Some(9), 9)]));
2678 println!("{}", count_pairs(&[(Some(50), 3), (Some(4), 5)]));
2679 println!("{} {} {}", tail(Some("xyz")), tail(Some("")), tail(None));
2680}
2681"#;
2682 let transformed =
2683 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2684 assert_eq!(
2685 transformed.code.matches("const __SUPERCOV_CHAIN_").count(),
2686 5
2687 );
2688 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
2689 limitation.get("id").and_then(|id| id.as_str())
2690 == Some("rust-let-chain-probes-not-injected")
2691 }));
2692 let original = compile_and_run(source, "original-chains");
2693 let instrumented = compile_and_run(
2694 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2695 "instrumented-chains",
2696 );
2697 assert_eq!(instrumented.status, original.status);
2698 assert_eq!(instrumented.stdout, original.stdout);
2699 assert_eq!(instrumented.stderr, original.stderr);
2700 }
2701
2702 #[test]
2703 fn instrumented_const_and_static_initialisers_still_compile() {
2704 let source = r#"const DIRECT: usize = if cfg!(unix) { 100 } else { 1_000 };
2710static WIDTH: usize = if cfg!(unix) { 2 } else { 4 };
2711
2712enum Mode {
2713 Narrow = if cfg!(unix) { 1 } else { 2 },
2714}
2715
2716struct Buffer([u8; if cfg!(unix) { 4 } else { 8 }]);
2717
2718impl Buffer {
2719 const SPAN: usize = if cfg!(unix) { 5 } else { 9 };
2720}
2721
2722fn scaled(flag: bool) -> usize {
2723 const LOCAL: usize = if cfg!(unix) { 3 } else { 6 };
2724 if flag { LOCAL + Buffer::SPAN } else { DIRECT + WIDTH }
2725}
2726
2727fn main() {
2728 let buffer = Buffer([0; if cfg!(unix) { 4 } else { 8 }]);
2729 println!(
2730 "{} {} {} {}",
2731 scaled(true),
2732 scaled(false),
2733 Mode::Narrow as usize,
2734 buffer.0.len()
2735 );
2736}
2737"#;
2738 let transformed =
2739 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2740 assert!(transformed.code.contains("::decision("));
2743 let ids = transformed
2744 .manifest
2745 .limitations
2746 .iter()
2747 .filter_map(|limitation| limitation.get("id")?.as_str())
2748 .collect::<BTreeSet<_>>();
2749 assert!(ids.contains("rust-const-context-not-instrumented"));
2750
2751 let original = compile_and_run(source, "const-original");
2752 let instrumented = compile_and_run(
2753 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2754 "const-instrumented",
2755 );
2756 assert_eq!(instrumented.status, original.status);
2757 assert_eq!(instrumented.stdout, original.stdout);
2758 assert_eq!(instrumented.stderr, original.stderr);
2759 }
2760
2761 #[test]
2762 fn a_probed_global_allocator_would_recurse_into_itself() {
2763 let source = r#"use std::alloc::{GlobalAlloc, Layout, System};
2769
2770struct Odd;
2771
2772unsafe impl GlobalAlloc for Odd {
2773 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
2774 if layout.align() == 1 && layout.size() > 0 {
2775 System.alloc(layout)
2776 } else {
2777 System.alloc(layout)
2778 }
2779 }
2780
2781 unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
2782 System.dealloc(pointer, layout);
2783 }
2784}
2785
2786#[global_allocator]
2787static ODD: Odd = Odd;
2788
2789fn classify(flag: bool) -> usize {
2790 if flag { 1 } else { 2 }
2791}
2792
2793fn main() {
2794 let held = std::vec![7u8; 32];
2795 println!("{} {}", classify(!held.is_empty()), held.len());
2796}
2797"#;
2798 let transformed =
2799 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2800 let allocator = transformed
2802 .code
2803 .split("unsafe impl GlobalAlloc for Odd")
2804 .nth(1)
2805 .and_then(|rest| rest.split("#[global_allocator]").next())
2806 .expect("the instrumented source still contains the allocator impl");
2807 assert!(
2808 !allocator.contains("__supercov_runtime_v1"),
2809 "probe injected into a GlobalAlloc impl:\n{allocator}"
2810 );
2811 assert!(transformed.code.contains("::decision("));
2813 let ids = transformed
2814 .manifest
2815 .limitations
2816 .iter()
2817 .filter_map(|limitation| limitation.get("id")?.as_str())
2818 .collect::<BTreeSet<_>>();
2819 assert!(ids.contains("rust-global-allocator-not-instrumented"));
2820
2821 let original = compile_and_run(source, "alloc-original");
2822 let instrumented = compile_and_run(
2823 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2824 "alloc-instrumented",
2825 );
2826 assert_eq!(instrumented.status, original.status);
2827 assert_eq!(instrumented.stdout, original.stdout);
2828 assert_eq!(instrumented.stderr, original.stderr);
2829 }
2830
2831 #[test]
2832 fn match_arms_record_selection_without_changing_behavior() {
2833 let source = r#"#[derive(Debug)]
2834enum Shape { Dot, Line(i32), Box { w: i32, h: i32 } }
2835
2836fn area(shape: &Shape) -> i32 {
2837 match shape {
2838 Shape::Dot => 0,
2839 Shape::Line(length) if *length < 0 => -length,
2840 Shape::Line(length) => *length,
2841 Shape::Box { w, h } => {
2842 let area = w * h;
2843 area
2844 }
2845 }
2846}
2847
2848fn describe(value: i32) -> &'static str {
2849 let inner = |v: i32| match v { 0 => "none", 1 => "one", _ => "many" };
2850 match value {
2851 0 => inner(value),
2852 n if n < 0 => unsafe { std::hint::unreachable_unchecked() },
2853 n => match n % 2 {
2854 0 => "even",
2855 _ => inner(n),
2856 },
2857 }
2858}
2859
2860fn main() {
2861 for shape in [Shape::Dot, Shape::Line(-3), Shape::Line(4), Shape::Box { w: 2, h: 5 }] {
2862 println!("{shape:?}={}", area(&shape));
2863 }
2864 for value in [0, 1, 3, 8] {
2865 println!("{value}:{}", describe(value));
2866 }
2867}
2868"#;
2869 let transformed =
2870 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2871 assert!(transformed.code.contains("::arms(__SUPERCOV_ARMS_"));
2872 assert_eq!(
2873 transformed.code.matches("const __SUPERCOV_ARMS_").count(),
2874 4
2875 );
2876 let arms = transformed
2877 .manifest
2878 .branches
2879 .iter()
2880 .filter(|branch| branch.kind == "match-arm")
2881 .count();
2882 assert_eq!(arms, 4 + 3 + 3 + 2);
2883 for branch in transformed
2886 .manifest
2887 .branches
2888 .iter()
2889 .filter(|branch| branch.kind == "match-arm")
2890 {
2891 for alternative in &branch.alternatives {
2892 assert!(
2893 transformed.code.contains(&format!("{:?}", alternative.id)),
2894 "{} is not in any table",
2895 alternative.id
2896 );
2897 }
2898 }
2899 let original = compile_and_run(source, "original-arms");
2900 let instrumented = compile_and_run(
2901 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2902 "instrumented-arms",
2903 );
2904 assert_eq!(instrumented.status, original.status);
2905 assert_eq!(instrumented.stdout, original.stdout);
2906 assert_eq!(instrumented.stderr, original.stderr);
2907 }
2908
2909 #[test]
2910 fn loops_logic_and_try_record_their_branches_without_changing_behavior() {
2911 let source = r#"use std::ops::ControlFlow;
2912
2913fn total(values: &[i32]) -> i32 {
2914 let mut sum = 0;
2915 for value in values {
2916 sum += value;
2917 }
2918 'outer: for row in 0..3 {
2919 for column in 0..3 {
2920 if column > row {
2921 continue 'outer;
2922 }
2923 sum += row * column;
2924 }
2925 }
2926 sum
2927}
2928
2929fn first_even(values: &[i32]) -> Option<i32> {
2930 let mut index = 0;
2931 'scan: while index < values.len() {
2932 if values[index] % 2 == 0 {
2933 break 'scan;
2934 }
2935 index += 1;
2936 }
2937 let mut it = values.iter().skip(index);
2938 while let Some(value) = it.next() {
2939 return Some(*value);
2940 }
2941 None
2942}
2943
2944fn parse_twice(text: &str) -> Result<i32, String> {
2945 let value: i32 = text.trim().parse().map_err(|_| "bad".to_string())?;
2946 let doubled = Some(value).map(|v| v * 2).ok_or("none")?;
2947 Ok(doubled)
2948}
2949
2950fn halve(value: i32) -> Option<i32> {
2951 let even = (value % 2 == 0).then_some(value)?;
2952 Some(even / 2)
2953}
2954
2955fn flow(values: &[i32]) -> ControlFlow<i32, i32> {
2956 let mut sum = 0;
2957 for value in values {
2958 let step: ControlFlow<i32, i32> = if *value < 0 { ControlFlow::Break(*value) } else { ControlFlow::Continue(*value) };
2959 sum += step?;
2960 }
2961 ControlFlow::Continue(sum)
2962}
2963
2964fn gate(a: bool, b: bool, c: bool) -> bool {
2965 let both = a && b;
2966 let either = a || b || c;
2967 both || (either && !c) || (c && a && (b || !b))
2968}
2969
2970fn main() {
2971 println!("{} {}", total(&[]), total(&[1, 2, 3]));
2972 println!("{:?} {:?} {:?}", first_even(&[]), first_even(&[1, 3]), first_even(&[1, 4, 6]));
2973 println!("{:?} {:?}", parse_twice(" 21 "), parse_twice("x"));
2974 println!("{:?} {:?}", halve(8), halve(7));
2975 println!("{:?} {:?}", flow(&[1, 2]), flow(&[1, -5, 2]));
2976 for a in [false, true] {
2977 for b in [false, true] {
2978 for c in [false, true] {
2979 print!("{}", gate(a, b, c) as u8);
2980 }
2981 }
2982 }
2983 println!();
2984}
2985"#;
2986 let transformed =
2987 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2988 for marker in [
2989 "::logical((",
2990 "::for_loop((",
2991 "::entered(&mut __supercov_loop_",
2992 "::zero_iterations(__supercov_loop_",
2993 "::TryProbe::probe((",
2994 ] {
2995 assert!(transformed.code.contains(marker), "{marker} missing");
2996 }
2997 let kinds = |kind: &str| {
2998 transformed
2999 .manifest
3000 .branches
3001 .iter()
3002 .filter(|branch| branch.kind == kind)
3003 .count()
3004 };
3005 assert_eq!(kinds("for-loop"), 3 + 1 + 3);
3006 assert_eq!(kinds("while-loop"), 2);
3007 assert_eq!(kinds("try-operator"), 4);
3008 assert_eq!(kinds("logical-and"), 4);
3009 assert_eq!(kinds("logical-or"), 5);
3010 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
3011 limitation.get("id").and_then(|id| id.as_str())
3012 == Some("rust-structural-branch-probes-not-yet-injected")
3013 }));
3014 let original = compile_and_run(source, "original-structural");
3015 let instrumented = compile_and_run(
3016 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3017 "instrumented-structural",
3018 );
3019 assert_eq!(instrumented.status, original.status);
3020 assert_eq!(instrumented.stdout, original.stdout);
3021 assert_eq!(instrumented.stderr, original.stderr);
3022 }
3023
3024 #[test]
3025 fn cfg_gated_sibling_blocks_keep_their_tail_position() {
3026 let source = r#"pub fn is_available() -> bool {
3032 #[cfg(target_endian = "little")]
3033 {
3034 true
3035 }
3036 #[cfg(not(target_endian = "little"))]
3037 {
3038 false
3039 }
3040}
3041
3042fn main() {
3043 println!("{}", is_available());
3044}
3045"#;
3046 let transformed =
3047 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3048 let original = compile_and_run(source, "cfg-original");
3049 let instrumented = compile_and_run(
3050 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3051 "cfg-instrumented",
3052 );
3053 assert_eq!(instrumented.status, original.status);
3054 assert_eq!(instrumented.stdout, original.stdout);
3055 assert!(
3057 transformed
3058 .code
3059 .contains("{\n\ncrate::__supercov_runtime_v1::hit(")
3060 || transformed
3061 .code
3062 .contains("{\ncrate::__supercov_runtime_v1::hit(")
3063 );
3064
3065 let attributed_let = r#"fn main() {
3069 #[cfg(target_endian = "little")]
3070 let value = 1;
3071 #[cfg(not(target_endian = "little"))]
3072 let value = 2;
3073 #[cfg(target_endian = "little")]
3074 let borrowed: &String = &String::from("little");
3075 #[cfg(not(target_endian = "little"))]
3076 let borrowed: &String = &String::from("big");
3077 #[cfg(target_endian = "little")]
3078 print!("le ");
3079 #[cfg(not(target_endian = "little"))]
3080 print!("be ");
3081 #[allow(unused_assignments)]
3082 let mut later;
3083 later = value + 1;
3084 println!("{value} {borrowed} {later}");
3085}
3086"#;
3087 let transformed = instrument_rust_source(
3088 "src/main.rs",
3089 attributed_let,
3090 "crate::__supercov_runtime_v1",
3091 )
3092 .unwrap();
3093 let ids = transformed
3094 .manifest
3095 .limitations
3096 .iter()
3097 .filter_map(|limitation| limitation.get("id")?.as_str())
3098 .collect::<BTreeSet<_>>();
3099 assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
3101 assert!(
3102 transformed
3103 .code
3104 .contains("let value = { crate::__supercov_runtime_v1::hit(")
3105 );
3106 assert!(
3107 transformed
3108 .code
3109 .contains("let borrowed: &String = { crate::__supercov_runtime_v1::hit(")
3110 );
3111 assert!(
3112 transformed
3113 .code
3114 .contains("] { crate::__supercov_runtime_v1::hit(")
3115 );
3116 let original = compile_and_run(attributed_let, "cfg-let-original");
3117 let instrumented = compile_and_run(
3118 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3119 "cfg-let-instrumented",
3120 );
3121 assert_eq!(instrumented.status, original.status);
3122 assert_eq!(instrumented.stdout, original.stdout);
3123 assert_eq!(instrumented.stderr, original.stderr);
3124
3125 let trailing_macro = r#"macro_rules! pick { ($e:expr) => { $e } }
3130fn value() -> i32 {
3131 let base = 20;
3132 #[rustfmt::skip]
3133 pick! { base + 1 }
3134}
3135fn effect() {
3136 #[rustfmt::skip]
3137 println! { "effect" }
3138}
3139fn gated() {
3140 #[cfg(target_endian = "little")]
3141 println! { "little" }
3142}
3143fn main() {
3144 effect();
3145 gated();
3146 println!("{}", value());
3147}
3148"#;
3149 let transformed = instrument_rust_source(
3150 "src/main.rs",
3151 trailing_macro,
3152 "crate::__supercov_runtime_v1",
3153 )
3154 .unwrap();
3155 assert!(
3156 transformed
3157 .code
3158 .contains("{ crate::__supercov_runtime_v1::hit(\"rs:statement:")
3159 );
3160 assert!(
3161 transformed
3162 .code
3163 .contains("); #[rustfmt::skip]\n pick! { base + 1 } }")
3164 );
3165 assert!(
3166 transformed
3167 .code
3168 .contains("); #[rustfmt::skip]\n println! { \"effect\" } }")
3169 );
3170 assert!(
3171 transformed.code.contains(
3172 "\n #[cfg(target_endian = \"little\")]\n println! { \"little\" }\n"
3173 )
3174 );
3175 let ids = transformed
3176 .manifest
3177 .limitations
3178 .iter()
3179 .filter_map(|limitation| limitation.get("id")?.as_str())
3180 .collect::<BTreeSet<_>>();
3181 assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
3182 let original = compile_and_run(trailing_macro, "trailing-macro-original");
3183 let instrumented = compile_and_run(
3184 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3185 "trailing-macro-instrumented",
3186 );
3187 assert_eq!(
3188 instrumented.status,
3189 original.status,
3190 "{}",
3191 String::from_utf8_lossy(&instrumented.stderr)
3192 );
3193 assert_eq!(instrumented.stdout, original.stdout);
3194 assert_eq!(instrumented.stderr, original.stderr);
3195 }
3196
3197 #[test]
3198 fn rejects_non_crate_local_runtime_paths() {
3199 assert_eq!(
3200 instrument_rust_source("src/lib.rs", "fn okay() {}", "supercov::runtime"),
3201 Err(RustInstrumenterError::InvalidRuntimePath)
3202 );
3203 }
3204
3205 #[test]
3206 fn rejects_invalid_rust_without_partial_obligations() {
3207 assert!(matches!(
3208 build_rust_manifest("src/lib.rs", "fn broken( {\n"),
3209 Err(RustInstrumenterError::Parse(_))
3210 ));
3211 }
3212}