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 self.point_located(range, range, kind, label);
454 }
455
456 fn point_located(
460 &mut self,
461 range: TextRange,
462 location: TextRange,
463 kind: PointKind,
464 label: Option<String>,
465 ) {
466 let kind_name = match kind {
467 PointKind::Statement => "statement",
468 PointKind::Function => "function",
469 };
470 let id = stable_id(self.file, kind_name, range, label.as_deref().unwrap_or(""));
471 self.obligation_ranges.push((id.clone(), range));
472 if !self.point_ids.insert(id.clone()) {
473 return;
474 }
475 let Some((line, column, source)) = self.location_source(location) else {
476 return;
477 };
478 self.manifest.points.push(PointMeta {
479 id,
480 kind,
481 file: self.file.into(),
482 line,
483 column,
484 source,
485 label,
486 });
487 }
488
489 fn atomic_condition_ranges(expression: &ast::Expr, ranges: &mut Vec<TextRange>) {
490 match expression {
491 ast::Expr::ParenExpr(paren) => {
492 if let Some(inner) = paren.expr() {
493 Self::atomic_condition_ranges(&inner, ranges);
494 } else {
495 ranges.push(expression.syntax().text_range());
496 }
497 }
498 ast::Expr::BinExpr(binary)
499 if matches!(
500 binary.op_kind(),
501 Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))
502 ) =>
503 {
504 if let Some(left) = binary.lhs() {
505 Self::atomic_condition_ranges(&left, ranges);
506 }
507 if let Some(right) = binary.rhs() {
508 Self::atomic_condition_ranges(&right, ranges);
509 }
510 }
511 _ => ranges.push(expression.syntax().text_range()),
512 }
513 }
514
515 fn decision(&mut self, test: &ast::Expr, kind: &str) {
516 let range = test.syntax().text_range();
517 let id = stable_id(self.file, "decision", range, kind);
518 self.obligation_ranges.push((id.clone(), range));
519 if !self.decision_ids.insert(id.clone()) {
520 return;
521 }
522 let Some((line, column, source)) = self.location_source(range) else {
523 return;
524 };
525 let mut condition_ranges = Vec::new();
526 Self::atomic_condition_ranges(test, &mut condition_ranges);
527 let mut conditions = Vec::with_capacity(condition_ranges.len());
528 for condition in condition_ranges {
529 match self.locations.text(condition) {
530 Ok(source) => conditions.push(source),
531 Err(error) => {
532 self.error.get_or_insert(error);
533 return;
534 }
535 }
536 }
537 self.manifest.decisions.push(DecisionMeta {
538 id: id.clone(),
539 file: self.file.into(),
540 line,
541 column,
542 source: source.clone(),
543 conditions,
544 kind: kind.into(),
545 });
546 self.branch_with_id(
547 format!("{id}:outcome"),
548 range,
549 kind,
550 source,
551 [("true", "true"), ("false", "false")],
552 );
553 }
554
555 fn branch<const N: usize>(
556 &mut self,
557 range: TextRange,
558 kind: &str,
559 alternatives: [(&str, &str); N],
560 ) {
561 let id = stable_id(self.file, "branch", range, kind);
562 let Some((_, _, source)) = self.location_source(range) else {
563 return;
564 };
565 self.branch_with_id(id, range, kind, source, alternatives);
566 }
567
568 fn branch_with_id<const N: usize>(
569 &mut self,
570 id: String,
571 range: TextRange,
572 kind: &str,
573 source: String,
574 alternatives: [(&str, &str); N],
575 ) {
576 self.obligation_ranges.push((id.clone(), range));
577 if !self.branch_ids.insert(id.clone()) {
578 return;
579 }
580 let Some((line, column, _)) = self.location_source(range) else {
581 return;
582 };
583 self.manifest.branches.push(BranchMeta {
584 id: id.clone(),
585 kind: kind.into(),
586 file: self.file.into(),
587 line,
588 column,
589 source,
590 alternatives: alternatives
591 .into_iter()
592 .map(|(suffix, label)| BranchAlternativeMeta {
593 id: format!("{id}:{suffix}"),
594 label: label.into(),
595 })
596 .collect(),
597 });
598 }
599
600 fn decline_unreachable_obligations(&mut self, root: &ra_ap_syntax::SyntaxNode) {
605 let unreachable = root
608 .descendants()
609 .filter(|node| {
610 (ast::Fn::cast(node.clone())
611 .is_some_and(|function| function.const_token().is_some())
612 || ast::BlockExpr::cast(node.clone())
613 .is_some_and(|block| block.const_token().is_some())
614 || ast::Const::can_cast(node.kind())
615 || ast::Static::can_cast(node.kind()))
616 || ast::Impl::can_cast(node.kind()) && in_global_allocator_impl(node)
617 })
618 .map(|node| node.text_range())
619 .collect::<Vec<_>>();
620 if unreachable.is_empty() {
621 return;
622 }
623 let mut declined = self
624 .manifest
625 .unmeasured
626 .iter()
627 .cloned()
628 .collect::<BTreeSet<_>>();
629 for (id, range) in &self.obligation_ranges {
630 if unreachable
631 .iter()
632 .any(|region| region.contains_range(*range))
633 {
634 declined.insert(id.clone());
635 }
636 }
637 self.manifest.unmeasured = declined.into_iter().collect();
638 }
639
640 fn site_limitation(&mut self, id: &'static str, range: TextRange, reason: String) {
646 if self.site_limitations.contains(id) {
647 return;
648 }
649 let Some((line, column, source)) = self.location_source(range) else {
650 return;
651 };
652 self.site_limitations.insert(id);
653 let source = source.lines().next().unwrap_or("").trim();
656 let source = if source.chars().count() > 120 {
657 format!("{}...", source.chars().take(117).collect::<String>())
658 } else {
659 source.to_owned()
660 };
661 self.manifest.limitations.push(json!({
662 "id": limitation_id(id, self.file),
663 "kind": "source-scope",
664 "file": self.file,
665 "line": line,
666 "column": column,
667 "source": source,
668 "blocking": false,
672 "reason": reason
673 }));
674 }
675
676 fn collect(
677 mut self,
678 file: &SourceFile,
679 assertions: &[TextRange],
680 matches: &[TextRange],
681 ) -> Result<CoverageManifest, RustInstrumenterError> {
682 let root = file.syntax();
683
684 for list in root.descendants().filter_map(ast::StmtList::cast) {
685 for statement in list.statements() {
686 match statement {
687 ast::Stmt::ExprStmt(statement) => {
688 self.point(statement.syntax().text_range(), PointKind::Statement, None);
689 }
690 ast::Stmt::LetStmt(statement) => {
691 self.point(statement.syntax().text_range(), PointKind::Statement, None);
692 }
693 ast::Stmt::Item(_) => {}
694 }
695 }
696 if let Some(tail) = list.tail_expr() {
697 self.point(tail.syntax().text_range(), PointKind::Statement, None);
698 }
699 }
700
701 for function in root.descendants().filter_map(ast::Fn::cast) {
702 if function.body().is_none() {
703 continue;
704 }
705 if function.const_token().is_some() {
706 self.site_limitation(
707 "rust-const-context-not-instrumented",
708 function.syntax().text_range(),
709 "Runtime probes cannot execute in const fn or compile-time evaluation".into(),
710 );
711 continue;
712 }
713 let label = function.name().map(|name| name.text().to_string());
714 self.point_located(
715 function.syntax().text_range(),
716 item_range(function.syntax()),
717 PointKind::Function,
718 label,
719 );
720 }
721
722 for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
723 self.point(
724 closure.syntax().text_range(),
725 PointKind::Function,
726 Some("<closure>".into()),
727 );
728 }
729
730 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
731 if let Some(condition) = expression.condition() {
732 self.decision(&condition, "if");
733 }
734 }
735 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
736 if let Some(condition) = expression.condition() {
737 self.decision(&condition, "while");
738 }
739 self.branch(
740 expression.syntax().text_range(),
741 "while-loop",
742 [("zero", "zero iterations"), ("entered", "entered")],
743 );
744 }
745 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
746 if let Some(condition) = guard.condition() {
747 self.decision(&condition, "match-guard");
748 }
749 }
750 for arguments in assertions {
751 if let Some(condition) = assertion_condition(root, *arguments) {
752 self.decision(&condition, "assert");
753 }
754 }
755 for expression in standalone_matches(root, matches, assertions) {
756 self.decision(&expression, "matches");
757 }
758
759 for binary in root.descendants().filter_map(ast::BinExpr::cast) {
760 let kind = match binary.op_kind() {
761 Some(BinaryOp::LogicOp(LogicOp::And)) => "logical-and",
762 Some(BinaryOp::LogicOp(LogicOp::Or)) => "logical-or",
763 _ => continue,
764 };
765 let range = binary.rhs().map_or_else(
766 || binary.syntax().text_range(),
767 |right| right.syntax().text_range(),
768 );
769 self.branch(
770 range,
771 kind,
772 [
773 ("short-circuit", "short-circuited"),
774 ("evaluated", "right operand evaluated"),
775 ],
776 );
777 }
778
779 for expression in root.descendants().filter_map(ast::ForExpr::cast) {
780 self.branch(
781 expression.syntax().text_range(),
782 "for-loop",
783 [("zero", "zero iterations"), ("entered", "entered")],
784 );
785 }
786 for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
787 let Some(list) = expression.match_arm_list() else {
788 continue;
789 };
790 let arms = list.arms().collect::<Vec<_>>();
791 let last = arms.len().saturating_sub(1);
792 for (index, arm) in arms.iter().enumerate() {
793 let range = arm.syntax().text_range();
794 if index == last {
795 self.branch(range, "match-arm", [("selected", "selected")]);
799 } else {
800 self.branch(
801 range,
802 "match-arm",
803 [("missed", "not selected"), ("selected", "selected")],
804 );
805 }
806 }
807 }
808 for expression in root.descendants().filter_map(ast::TryExpr::cast) {
809 self.branch(
810 expression.syntax().text_range(),
811 "try-operator",
812 [("continued", "continued"), ("returned", "early return")],
813 );
814 }
815
816 let mut macro_names = BTreeSet::new();
822 let mut first_macro = None;
823 let mut macro_sites = 0;
824 for call in root.descendants().filter_map(ast::MacroCall::cast) {
825 macro_names.insert(
826 call.path()
827 .map(|path| path.syntax().text().to_string())
828 .unwrap_or_else(|| "?".into()),
829 );
830 first_macro.get_or_insert_with(|| call.syntax().text_range());
831 macro_sites += 1;
832 }
833 if let Some(range) = first_macro {
834 let named = macro_names
835 .iter()
836 .take(4)
837 .map(|name| format!("`{name}!`"))
838 .collect::<Vec<_>>()
839 .join(", ");
840 let rest = macro_names.len().saturating_sub(4);
841 self.site_limitation(
842 "rust-macro-expansion-not-instrumented",
843 range,
844 format!(
845 "{named}{} expand in the compiler: their arguments and expansions are outside the owned source denominator ({macro_sites} call site{} in this file)",
846 if rest == 0 {
847 String::new()
848 } else {
849 format!(" and {rest} other macro{}", if rest == 1 { "" } else { "s" })
850 },
851 if macro_sites == 1 { "" } else { "s" }
852 ),
853 );
854 }
855
856 let bears_obligation = |node: &ra_ap_syntax::SyntaxNode| {
861 ast::StmtList::cast(node.clone()).is_some_and(|list| {
862 list.statements().next().is_some() || list.tail_expr().is_some()
863 }) || ast::IfExpr::can_cast(node.kind())
864 || ast::WhileExpr::can_cast(node.kind())
865 || ast::MatchGuard::can_cast(node.kind())
866 || ast::ForExpr::can_cast(node.kind())
867 || ast::MatchArm::can_cast(node.kind())
868 || ast::TryExpr::can_cast(node.kind())
869 || ast::ClosureExpr::can_cast(node.kind())
870 || ast::BinExpr::cast(node.clone()).is_some_and(|binary| {
871 matches!(
872 binary.op_kind(),
873 Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))
874 )
875 })
876 };
877 if let Some(node) = root
878 .descendants()
879 .find(|node| bears_obligation(node) && in_const_context(node))
880 {
881 self.site_limitation(
882 "rust-const-context-not-instrumented",
883 node.text_range(),
884 "Runtime probes cannot execute in const fn or compile-time evaluation; this and any later const context in the file stay declared".into(),
885 );
886 }
887 if let Some(node) = root
888 .descendants()
889 .find(|node| bears_obligation(node) && in_global_allocator(node))
890 {
891 self.site_limitation(
892 "rust-global-allocator-not-instrumented",
893 node.text_range(),
894 "Probing a GlobalAlloc implementation recurses into itself, because the runtime allocates".into(),
895 );
896 }
897
898 self.decline_unreachable_obligations(root);
899
900 if let Some(error) = self.error {
901 return Err(error);
902 }
903 self.manifest
904 .decisions
905 .sort_by(|left, right| left.id.cmp(&right.id));
906 self.manifest
907 .points
908 .sort_by(|left, right| left.id.cmp(&right.id));
909 self.manifest
910 .branches
911 .sort_by(|left, right| left.id.cmp(&right.id));
912 self.manifest.limitations.sort_by(|left, right| {
913 left.get("id")
914 .and_then(|value| value.as_str())
915 .cmp(&right.get("id").and_then(|value| value.as_str()))
916 });
917 Ok(self.manifest)
918 }
919}
920
921pub const FAILED_TRANSFORM_DUMP_ENV: &str = "SUPERCOV_RUST_DUMP_FAILED_INSTRUMENTATION";
924
925const EXPRESSION_MACROS: &[&str] = &[
934 "assert",
935 "debug_assert",
936 "assert_eq",
937 "assert_ne",
938 "debug_assert_eq",
939 "debug_assert_ne",
940 "println",
941 "print",
942 "eprintln",
943 "eprint",
944 "format",
945 "format_args",
946 "write",
947 "writeln",
948 "panic",
949 "unreachable",
950 "todo",
951 "unimplemented",
952 "vec",
953 "dbg",
954 "matches",
955];
956
957const ASSERTION_MACROS: &[&str] = &["assert", "debug_assert"];
959
960const ASSERTION_STATEMENT_MACROS: &[&str] = &[
963 "assert",
964 "assert_eq",
965 "assert_ne",
966 "debug_assert",
967 "debug_assert_eq",
968 "debug_assert_ne",
969];
970
971struct ExpressionView {
975 text: String,
976 assertions: Vec<TextRange>,
977 assertion_calls: Vec<TextRange>,
980 matches: Vec<TextRange>,
982}
983
984const EDITIONS: [Edition; 4] = [
990 Edition::Edition2024,
991 Edition::Edition2021,
992 Edition::Edition2018,
993 Edition::Edition2015,
994];
995
996fn parse_any_edition(source: &str) -> Result<(SourceFile, Edition), Vec<String>> {
997 let mut newest_errors = None;
998 for edition in EDITIONS {
999 let parsed = SourceFile::parse(source, edition);
1000 let errors = parsed.errors();
1001 if errors.is_empty() {
1002 return Ok((parsed.tree(), edition));
1003 }
1004 newest_errors.get_or_insert_with(|| {
1005 errors
1006 .into_iter()
1007 .map(|error| error.to_string())
1008 .collect::<Vec<_>>()
1009 });
1010 }
1011 Err(newest_errors.unwrap_or_default())
1012}
1013
1014fn expression_view(source: &str, edition: Edition) -> ExpressionView {
1015 let mut text = source.to_owned();
1016 let mut assertions = Vec::new();
1017 let mut assertion_calls = Vec::new();
1018 let mut matches = Vec::new();
1019 for _ in 0..16 {
1022 let tree = SourceFile::parse(&text, edition).tree();
1023 let Some(next) = rewrite_expression_macros(
1024 &text,
1025 &tree,
1026 edition,
1027 &mut assertions,
1028 &mut assertion_calls,
1029 &mut matches,
1030 ) else {
1031 break;
1032 };
1033 text = next;
1034 }
1035 ExpressionView {
1036 text,
1037 assertions,
1038 assertion_calls,
1039 matches,
1040 }
1041}
1042
1043fn is_statement_macro(call: &ast::MacroCall) -> bool {
1046 call.syntax().parent().is_some_and(|parent| {
1047 ast::MacroExpr::can_cast(parent.kind())
1048 && parent.parent().is_some_and(|grandparent| {
1049 ast::ExprStmt::can_cast(grandparent.kind())
1050 || ast::StmtList::can_cast(grandparent.kind())
1051 })
1052 })
1053}
1054
1055fn first_top_level_comma_end(arguments: &ast::TokenTree) -> Option<usize> {
1057 arguments
1058 .syntax()
1059 .children_with_tokens()
1060 .filter_map(|element| element.into_token())
1061 .find(|token| token.kind() == SyntaxKind::COMMA)
1062 .map(|token| usize::from(token.text_range().end()))
1063}
1064
1065fn rewrite_expression_macros(
1068 source: &str,
1069 tree: &SourceFile,
1070 edition: Edition,
1071 assertions: &mut Vec<TextRange>,
1072 assertion_calls: &mut Vec<TextRange>,
1073 matches: &mut Vec<TextRange>,
1074) -> Option<String> {
1075 let mut text = source.as_bytes().to_vec();
1076 let mut changed = false;
1077 for call in tree.syntax().descendants().filter_map(ast::MacroCall::cast) {
1078 let Some(name) = call
1079 .path()
1080 .and_then(|path| path.segment())
1081 .and_then(|segment| segment.name_ref())
1082 .map(|name| name.text().to_string())
1083 else {
1084 continue;
1085 };
1086 if !EXPRESSION_MACROS.contains(&name.as_str()) {
1087 continue;
1088 }
1089 let (Some(bang), Some(arguments)) = (call.excl_token(), call.token_tree()) else {
1090 continue;
1091 };
1092 let parenthesised = arguments.l_paren_token().is_some();
1093 if !parenthesised && arguments.l_brack_token().is_none() {
1094 continue;
1095 }
1096 let range = arguments.syntax().text_range();
1097 let (start, end) = (usize::from(range.start()), usize::from(range.end()));
1098 let mut rewritten = source.as_bytes()[start..end].to_vec();
1099 if !parenthesised {
1100 rewritten[0] = b'(';
1101 *rewritten
1102 .last_mut()
1103 .expect("a token tree has a closing delimiter") = b')';
1104 }
1105 if name == "matches" {
1106 let Some(comma_end) = first_top_level_comma_end(&arguments) else {
1109 continue;
1110 };
1111 for byte in &mut rewritten[comma_end - start..end - start - 1] {
1112 if *byte != b'\n' {
1113 *byte = b' ';
1114 }
1115 }
1116 }
1117 let probe = format!(
1119 "fn __supercov() {{ let _ = __f{}; }}",
1120 String::from_utf8_lossy(&rewritten)
1121 );
1122 if !SourceFile::parse(&probe, edition).errors().is_empty() {
1123 if !parenthesised && !is_statement_macro(&call) {
1127 let array = source.as_bytes()[start..end].to_vec();
1128 let array_probe = format!(
1129 "fn __supercov() {{ let _ = {}; }}",
1130 String::from_utf8_lossy(&array)
1131 );
1132 if SourceFile::parse(&array_probe, edition).errors().is_empty() {
1133 let prefix_start = usize::from(call.syntax().text_range().start());
1134 let prefix_start = call.attrs().last().map_or(prefix_start, |attribute| {
1135 usize::from(attribute.syntax().text_range().end())
1136 });
1137 for byte in &mut text[prefix_start..start] {
1138 if *byte != b'\n' {
1139 *byte = b' ';
1140 }
1141 }
1142 changed = true;
1143 }
1144 }
1145 continue;
1146 }
1147 text[usize::from(bang.text_range().start())] = b'_';
1148 text[start..end].copy_from_slice(&rewritten);
1149 changed = true;
1150 if ASSERTION_MACROS.contains(&name.as_str()) {
1151 assertions.push(range);
1152 }
1153 if ASSERTION_STATEMENT_MACROS.contains(&name.as_str()) {
1154 assertion_calls.push(call.syntax().text_range());
1155 }
1156 if name == "matches" {
1157 matches.push(call.syntax().text_range());
1158 }
1159 }
1160 changed.then(|| String::from_utf8(text).expect("rewriting ASCII keeps the source UTF-8"))
1161}
1162
1163fn assertion_argument_count(root: &ra_ap_syntax::SyntaxNode, arguments: TextRange) -> usize {
1166 root.descendants()
1167 .find(|node| node.text_range() == arguments && ast::ArgList::can_cast(node.kind()))
1168 .and_then(ast::ArgList::cast)
1169 .map_or(0, |list| list.args().count())
1170}
1171
1172fn assertion_condition(root: &ra_ap_syntax::SyntaxNode, arguments: TextRange) -> Option<ast::Expr> {
1175 root.descendants()
1176 .find(|node| node.text_range() == arguments && ast::ArgList::can_cast(node.kind()))
1177 .and_then(ast::ArgList::cast)?
1178 .args()
1179 .next()
1180}
1181
1182struct ParsedSource {
1187 tree: SourceFile,
1188 assertions: Vec<TextRange>,
1189 assertion_calls: Vec<TextRange>,
1190 matches: Vec<TextRange>,
1191 edition: Edition,
1192}
1193
1194fn parse_for_instrumentation(source: &str) -> Result<ParsedSource, RustInstrumenterError> {
1195 if source.len() > u32::MAX as usize {
1196 return Err(RustInstrumenterError::SourceTooLarge);
1197 }
1198 let (tree, edition) = parse_any_edition(source).map_err(RustInstrumenterError::Parse)?;
1199 let view = expression_view(source, edition);
1200 let parsed_view = SourceFile::parse(&view.text, edition);
1201 if parsed_view.errors().is_empty() {
1202 Ok(ParsedSource {
1203 tree: parsed_view.tree(),
1204 assertions: view.assertions,
1205 assertion_calls: view.assertion_calls,
1206 matches: view.matches,
1207 edition,
1208 })
1209 } else {
1210 Ok(ParsedSource {
1211 tree,
1212 assertions: Vec::new(),
1213 assertion_calls: Vec::new(),
1214 matches: Vec::new(),
1215 edition,
1216 })
1217 }
1218}
1219
1220fn standalone_matches(
1224 root: &ra_ap_syntax::SyntaxNode,
1225 matches: &[TextRange],
1226 assertions: &[TextRange],
1227) -> Vec<ast::Expr> {
1228 let mut atoms = Vec::new();
1229 let mut conditions = Vec::new();
1230 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
1231 conditions.extend(expression.condition());
1232 }
1233 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
1234 conditions.extend(expression.condition());
1235 }
1236 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
1237 conditions.extend(guard.condition());
1238 }
1239 for arguments in assertions {
1240 conditions.extend(assertion_condition(root, *arguments));
1241 }
1242 for condition in &conditions {
1243 RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
1244 }
1245 matches
1246 .iter()
1247 .filter(|range| !atoms.contains(range))
1248 .filter_map(|range| {
1249 root.descendants()
1250 .find(|node| node.text_range() == *range && ast::CallExpr::can_cast(node.kind()))
1251 .and_then(ast::Expr::cast)
1252 })
1253 .collect()
1254}
1255
1256pub fn build_rust_manifest(
1257 file: &str,
1258 source: &str,
1259) -> Result<CoverageManifest, RustInstrumenterError> {
1260 let parsed = parse_for_instrumentation(source)?;
1261 RustObligationCollector::new(file, source).collect(
1262 &parsed.tree,
1263 &parsed.assertions,
1264 &parsed.matches,
1265 )
1266}
1267
1268fn block_entry_offset(block: &ast::BlockExpr) -> Option<usize> {
1269 let list = block.stmt_list()?;
1270 list.attrs()
1271 .last()
1272 .map(|attribute| usize::from(attribute.syntax().text_range().end()))
1273 .or_else(|| {
1274 list.l_curly_token()
1275 .map(|token| usize::from(token.text_range().end()))
1276 })
1277}
1278
1279fn item_range(node: &ra_ap_syntax::SyntaxNode) -> TextRange {
1284 let range = node.text_range();
1285 node.children_with_tokens()
1286 .find(|element| match element {
1287 ra_ap_syntax::NodeOrToken::Token(token) => !token.kind().is_trivia(),
1288 ra_ap_syntax::NodeOrToken::Node(child) => !ast::Attr::can_cast(child.kind()),
1289 })
1290 .map_or(range, |element| {
1291 TextRange::new(element.text_range().start(), range.end())
1292 })
1293}
1294
1295fn range_after_attributes(node: &impl HasAttrs) -> TextRange {
1296 let range = node.syntax().text_range();
1297 node.attrs().last().map_or(range, |attribute| {
1298 TextRange::new(attribute.syntax().text_range().end(), range.end())
1299 })
1300}
1301
1302fn has_let(expression: &ast::Expr) -> bool {
1303 expression
1304 .syntax()
1305 .descendants()
1306 .any(|node| ast::LetExpr::can_cast(node.kind()))
1307}
1308
1309enum ChainHost<'a> {
1311 If(&'a ast::IfExpr),
1312 While(&'a ast::WhileExpr),
1313}
1314
1315fn allocate_chain_table_name(
1317 file: &str,
1318 condition: &ast::Expr,
1319 identifiers: &mut BTreeSet<String>,
1320) -> String {
1321 let id = stable_id(file, "chain", condition.syntax().text_range(), "operators");
1322 let suffix = id
1323 .rsplit(':')
1324 .next()
1325 .unwrap_or("chain")
1326 .to_ascii_uppercase();
1327 let base = format!("__SUPERCOV_CHAIN_{suffix}");
1328 let mut candidate = base.clone();
1329 let mut attempt = 0_usize;
1330 while !identifiers.insert(candidate.clone()) {
1331 attempt += 1;
1332 candidate = format!("{base}_{attempt}");
1333 }
1334 candidate
1335}
1336
1337fn allocate_identifier(
1339 file: &str,
1340 range: TextRange,
1341 kind: &str,
1342 identifiers: &mut BTreeSet<String>,
1343) -> String {
1344 let id = stable_id(file, kind, range, "");
1345 let suffix = id.rsplit(':').next().unwrap_or(kind);
1346 let base = format!("__supercov_{kind}_{suffix}");
1347 let mut candidate = base.clone();
1348 let mut attempt = 0_usize;
1349 while !identifiers.insert(candidate.clone()) {
1350 attempt += 1;
1351 candidate = format!("{base}_{attempt}");
1352 }
1353 candidate
1354}
1355
1356fn own_breaks(body: &ast::BlockExpr, label: Option<ast::Label>) -> Vec<TextRange> {
1360 let own_label = label
1361 .and_then(|label| label.lifetime())
1362 .map(|lifetime| lifetime.text().to_string());
1363 body.syntax()
1364 .descendants()
1365 .filter_map(ast::BreakExpr::cast)
1366 .filter(|expression| match expression.lifetime() {
1367 Some(lifetime) => own_label.as_deref() == Some(lifetime.text().to_string().as_str()),
1368 None => !expression
1369 .syntax()
1370 .ancestors()
1371 .skip(1)
1372 .take_while(|ancestor| ancestor != body.syntax())
1373 .any(|ancestor| {
1374 ast::LoopExpr::can_cast(ancestor.kind())
1375 || ast::WhileExpr::can_cast(ancestor.kind())
1376 || ast::ForExpr::can_cast(ancestor.kind())
1377 || ast::ClosureExpr::can_cast(ancestor.kind())
1378 }),
1379 })
1380 .map(|expression| expression.syntax().text_range())
1381 .collect()
1382}
1383
1384fn instrument_let_chain(
1400 insertions: &mut Vec<Insertion>,
1401 runtime_path: &str,
1402 file: &str,
1403 condition: &ast::Expr,
1404 host: ChainHost<'_>,
1405 identifiers: &mut BTreeSet<String>,
1406) {
1407 let (kind, host_range, body, label) = match &host {
1410 ChainHost::If(expression) => (
1411 "if",
1412 range_after_attributes(*expression),
1413 expression.then_branch(),
1414 None,
1415 ),
1416 ChainHost::While(expression) => (
1417 "while",
1418 range_after_attributes(*expression),
1419 expression.loop_body(),
1420 expression.label(),
1421 ),
1422 };
1423 let Some(body) = body else {
1424 return;
1425 };
1426 let Some(body_offset) = block_entry_offset(&body) else {
1427 return;
1428 };
1429 let range = condition.syntax().text_range();
1430 let id = stable_id(file, "decision", range, kind);
1431 let mut atoms = Vec::new();
1432 RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
1433 let lets = condition
1434 .syntax()
1435 .descendants()
1436 .filter_map(ast::LetExpr::cast)
1437 .map(|expression| expression.syntax().text_range())
1438 .collect::<Vec<_>>();
1439 let frame = allocate_frame_name(file, condition, kind, identifiers);
1440 let table = allocate_chain_table_name(file, condition, identifiers);
1441 let single_let = atoms.len() == 1;
1442 let broke = match &host {
1443 ChainHost::While(_) if single_let => {
1444 Some(allocate_identifier(file, range, "broke", identifiers))
1445 }
1446 _ => None,
1447 };
1448
1449 let mut operators = Vec::new();
1450 for binary in condition
1451 .syntax()
1452 .descendants()
1453 .filter_map(ast::BinExpr::cast)
1454 {
1455 if !matches!(binary.op_kind(), Some(BinaryOp::LogicOp(LogicOp::And))) {
1458 continue;
1459 }
1460 let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
1461 continue;
1462 };
1463 if !has_let(&left) {
1464 continue;
1465 }
1466 let branch = stable_id(file, "branch", right.syntax().text_range(), "logical-and");
1467 let right_range = right.syntax().text_range();
1468 let Some(first) = atoms
1469 .iter()
1470 .position(|atom| right_range.contains_range(*atom))
1471 else {
1472 continue;
1473 };
1474 operators.push(format!(
1475 "({first}, {:?}, {:?})",
1476 format!("{branch}:short-circuit"),
1477 format!("{branch}:evaluated")
1478 ));
1479 }
1480
1481 let mark = format!("{runtime_path}::reached(&mut {frame}, 0);");
1482 let record_false = format!("{runtime_path}::decision_chain(&mut {frame}, false, {table});");
1483 let mut prefix = format!(
1484 "{{ const {table}: &[(usize, &str, &str)] = &[{}]; let mut {frame} = {runtime_path}::DecisionFrame::new({id:?}, {}); ",
1485 operators.join(", "),
1486 atoms.len()
1487 );
1488 if single_let {
1489 prefix.push_str(&mark);
1490 prefix.push(' ');
1491 }
1492 if let Some(broke) = &broke {
1493 prefix.push_str(&format!("let mut {broke} = false; "));
1494 }
1495 let suffix = match &host {
1496 ChainHost::If(expression) => match expression.else_branch() {
1497 Some(_) => " }".to_owned(),
1498 None => format!(" else {{ {record_false} }} }}"),
1499 },
1500 ChainHost::While(_) => match &broke {
1501 Some(broke) => format!(" if !{broke} {{ {mark} {record_false} }} }}"),
1502 None => format!(" {record_false} }}"),
1503 },
1504 };
1505 push_wrapper(insertions, host_range, host_range, 1, prefix, suffix);
1506 if !single_let {
1507 push_direct(
1508 insertions,
1509 usize::from(range.start()),
1510 format!("{runtime_path}::reached(&mut {frame}, 0) && "),
1511 );
1512 }
1513 for (index, atom) in atoms.iter().enumerate() {
1514 if lets.contains(atom) {
1515 if index > 0 {
1516 push_direct(
1517 insertions,
1518 usize::from(atom.start()),
1519 format!("{runtime_path}::reached(&mut {frame}, {index}) && "),
1520 );
1521 }
1522 } else {
1523 push_wrapper(
1524 insertions,
1525 *atom,
1526 *atom,
1527 1,
1528 format!("{runtime_path}::condition(("),
1529 format!("), &mut {frame}, {index})"),
1530 );
1531 }
1532 }
1533 let mut entry = String::new();
1534 if broke.is_some() {
1535 entry.push_str(&format!("\n{mark}"));
1536 }
1537 entry.push_str(&format!(
1538 "\n{runtime_path}::decision_chain(&mut {frame}, true, {table});"
1539 ));
1540 push_direct(insertions, body_offset, entry);
1541 if let Some(broke) = &broke {
1542 for break_range in own_breaks(&body, label) {
1543 push_wrapper(
1544 insertions,
1545 break_range,
1546 break_range,
1547 0,
1548 format!("{{ {broke} = true; "),
1549 " }".into(),
1550 );
1551 }
1552 }
1553 if let ChainHost::If(expression) = &host {
1554 match expression.else_branch() {
1555 Some(ast::ElseBranch::Block(block)) => {
1556 if let Some(offset) = block_entry_offset(&block) {
1557 push_direct(insertions, offset, format!("\n{record_false}"));
1558 }
1559 }
1560 Some(ast::ElseBranch::IfExpr(nested)) => {
1561 let nested_range = nested.syntax().text_range();
1562 push_wrapper(
1563 insertions,
1564 nested_range,
1565 nested_range,
1566 0,
1567 format!("{{ {record_false} "),
1568 " }".into(),
1569 );
1570 }
1571 None => {}
1572 }
1573 }
1574}
1575
1576fn enclosing_block_entry(node: &ra_ap_syntax::SyntaxNode) -> Option<usize> {
1580 node.ancestors()
1581 .skip(1)
1582 .find_map(ast::BlockExpr::cast)
1583 .and_then(|block| block_entry_offset(&block))
1584}
1585
1586fn plain_block(block: &ast::BlockExpr) -> bool {
1590 block
1591 .syntax()
1592 .first_token()
1593 .is_some_and(|token| token.kind() == SyntaxKind::L_CURLY)
1594}
1595
1596fn instrument_decision(
1597 insertions: &mut Vec<Insertion>,
1598 runtime_path: &str,
1599 file: &str,
1600 condition: &ast::Expr,
1601 kind: &str,
1602 frame_name: &str,
1603) -> bool {
1604 if cannot_carry_probe(condition.syntax())
1605 || condition
1606 .syntax()
1607 .descendants()
1608 .any(|node| ast::LetExpr::can_cast(node.kind()))
1609 {
1610 return false;
1611 }
1612 let range = condition.syntax().text_range();
1613 let id = stable_id(file, "decision", range, kind);
1614 let mut condition_ranges = Vec::new();
1615 RustObligationCollector::atomic_condition_ranges(condition, &mut condition_ranges);
1616 push_wrapper(
1617 insertions,
1618 range,
1619 range,
1620 0,
1621 format!(
1622 "({{ let mut {frame_name} = {runtime_path}::DecisionFrame::new({id:?}, {}); {runtime_path}::decision((",
1623 condition_ranges.len()
1624 ),
1625 format!("), &mut {frame_name}) }})"),
1626 );
1627 for (index, atomic_range) in condition_ranges.into_iter().enumerate() {
1631 push_wrapper(
1632 insertions,
1633 atomic_range,
1634 atomic_range,
1635 1,
1636 format!("{runtime_path}::condition(("),
1637 format!("), &mut {frame_name}, {index})"),
1638 );
1639 }
1640 true
1641}
1642
1643pub fn instrument_rust_source(
1653 file: &str,
1654 source: &str,
1655 runtime_path: &str,
1656) -> Result<RustInstrumentedSource, RustInstrumenterError> {
1657 if !valid_runtime_path(runtime_path) {
1658 return Err(RustInstrumenterError::InvalidRuntimePath);
1659 }
1660 let parsed = parse_for_instrumentation(source)?;
1666 let mut manifest = RustObligationCollector::new(file, source).collect(
1667 &parsed.tree,
1668 &parsed.assertions,
1669 &parsed.matches,
1670 )?;
1671 let ParsedSource {
1672 tree,
1673 assertions,
1674 assertion_calls,
1675 matches,
1676 edition,
1677 } = parsed;
1678 let root = tree.syntax();
1679 let mut insertions = Vec::new();
1680 let mut identifiers = root
1681 .descendants_with_tokens()
1682 .filter_map(|element| element.into_token())
1683 .filter(|token| token.kind() == SyntaxKind::IDENT)
1684 .map(|token| token.text().to_string())
1685 .collect::<BTreeSet<_>>();
1686
1687 let mut skipped_attributed_statement = false;
1688 let attributed_probe = |insertions: &mut Vec<Insertion>,
1699 skipped: &mut bool,
1700 expression: Option<ast::Expr>,
1701 has_attrs: bool,
1702 statement: bool,
1706 trailing: bool,
1707 range: TextRange,
1708 id: String| {
1709 if !has_attrs {
1710 push_direct(
1711 insertions,
1712 usize::from(range.start()),
1713 format!("{runtime_path}::hit({id:?});"),
1714 );
1715 return;
1716 }
1717 let Some(expression) = expression else {
1718 *skipped = true;
1719 return;
1720 };
1721 if let ast::Expr::BlockExpr(block) = &expression
1722 && let Some(offset) = block_entry_offset(block)
1723 {
1724 push_direct(
1725 insertions,
1726 offset,
1727 format!("\n{runtime_path}::hit({id:?});"),
1728 );
1729 return;
1730 }
1731 if trailing && let ast::Expr::MacroExpr(_) = &expression {
1742 if expression.attrs().any(|attribute| {
1743 attribute.meta().is_some_and(|meta| {
1746 let text = meta.syntax().text().to_string();
1747 let name = text
1748 .chars()
1749 .take_while(|character| character.is_alphanumeric() || *character == '_')
1750 .collect::<String>();
1751 matches!(name.as_str(), "cfg" | "cfg_attr")
1752 })
1753 }) {
1754 *skipped = true;
1755 return;
1756 }
1757 let range = expression.syntax().text_range();
1758 push_wrapper(
1759 insertions,
1760 range,
1761 range,
1762 0,
1763 format!("{{ {runtime_path}::hit({id:?}); "),
1764 " }".into(),
1765 );
1766 return;
1767 }
1768 if statement && let ast::Expr::MacroExpr(_) = &expression {
1775 let start = expression.attrs().last().map_or_else(
1776 || expression.syntax().text_range().start(),
1777 |attribute| attribute.syntax().text_range().end(),
1778 );
1779 let wrapped = TextRange::new(start, expression.syntax().text_range().end());
1780 push_wrapper(
1781 insertions,
1782 wrapped,
1783 wrapped,
1784 0,
1785 format!(" {{ {runtime_path}::hit({id:?}); "),
1786 "; }".into(),
1787 );
1788 return;
1789 }
1790 let start = expression.attrs().last().map_or_else(
1796 || expression.syntax().text_range().start(),
1797 |attribute| attribute.syntax().text_range().end(),
1798 );
1799 let wrapped = TextRange::new(start, expression.syntax().text_range().end());
1800 push_wrapper(
1801 insertions,
1802 wrapped,
1803 wrapped,
1804 0,
1805 format!(" {{ {runtime_path}::hit({id:?}); ("),
1806 ") }".into(),
1807 );
1808 };
1809 for list in root.descendants().filter_map(ast::StmtList::cast) {
1810 let last_statement = list.statements().last();
1811 for statement in list.statements() {
1812 let (range, expression, has_attrs, statement_expression, trailing) = match &statement {
1813 ast::Stmt::ExprStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
1814 let expression = statement.expr();
1815 let has_attrs = expression
1818 .as_ref()
1819 .is_some_and(|expression| expression.attrs().next().is_some());
1820 let trailing = statement.semicolon_token().is_none()
1823 && list.tail_expr().is_none()
1824 && last_statement.as_ref() == Some(&ast::Stmt::ExprStmt(statement.clone()));
1825 (
1826 statement.syntax().text_range(),
1827 expression,
1828 has_attrs,
1829 true,
1830 trailing,
1831 )
1832 }
1833 ast::Stmt::LetStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
1834 let has_attrs = statement.attrs().next().is_some();
1835 let initializer = has_attrs.then(|| statement.initializer()).flatten();
1838 (
1839 statement.syntax().text_range(),
1840 initializer,
1841 has_attrs,
1842 false,
1843 false,
1844 )
1845 }
1846 _ => continue,
1847 };
1848 let id = stable_id(file, "statement", range, "");
1849 if !has_attrs
1856 && !trailing
1857 && assertion_calls
1858 .iter()
1859 .any(|call| range.contains_range(*call))
1860 {
1861 push_direct(
1862 &mut insertions,
1863 usize::from(range.end()),
1864 format!("{runtime_path}::assertion({id:?});"),
1865 );
1866 }
1867 attributed_probe(
1868 &mut insertions,
1869 &mut skipped_attributed_statement,
1870 expression,
1871 has_attrs,
1872 statement_expression,
1873 trailing,
1874 range,
1875 id,
1876 );
1877 }
1878 if let Some(tail) = list
1879 .tail_expr()
1880 .filter(|tail| !cannot_carry_probe(tail.syntax()))
1881 {
1882 let range = tail.syntax().text_range();
1883 let id = stable_id(file, "statement", range, "");
1884 let has_attrs = tail.attrs().next().is_some();
1885 attributed_probe(
1886 &mut insertions,
1887 &mut skipped_attributed_statement,
1888 Some(tail),
1889 has_attrs,
1890 true,
1891 true,
1892 range,
1893 id,
1894 );
1895 }
1896 }
1897
1898 for function in root.descendants().filter_map(ast::Fn::cast) {
1899 if cannot_carry_probe(function.syntax()) {
1902 continue;
1903 }
1904 let Some(body) = function.body() else {
1905 continue;
1906 };
1907 let label = function.name().map(|name| name.text().to_string());
1908 let id = stable_id(
1909 file,
1910 "function",
1911 function.syntax().text_range(),
1912 label.as_deref().unwrap_or(""),
1913 );
1914 if let Some(offset) = block_entry_offset(&body) {
1915 push_direct(
1916 &mut insertions,
1917 offset,
1918 format!("\n{runtime_path}::hit({id:?});"),
1919 );
1920 }
1921 }
1922
1923 for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
1924 let Some(body) = closure.body() else {
1925 continue;
1926 };
1927 if cannot_carry_probe(body.syntax()) {
1928 continue;
1929 }
1930 let id = stable_id(file, "function", closure.syntax().text_range(), "<closure>");
1931 if let ast::Expr::BlockExpr(block) = &body {
1932 if let Some(offset) = block_entry_offset(block) {
1933 push_direct(
1934 &mut insertions,
1935 offset,
1936 format!("\n{runtime_path}::hit({id:?});"),
1937 );
1938 }
1939 } else {
1940 let range = body.syntax().text_range();
1941 push_wrapper(
1942 &mut insertions,
1943 range,
1944 closure.syntax().text_range(),
1945 0,
1946 format!("{{ {runtime_path}::hit({id:?}); ("),
1947 ") }".into(),
1948 );
1949 }
1950 }
1951
1952 for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
1959 if cannot_carry_probe(expression.syntax()) {
1960 continue;
1961 }
1962 let Some(list) = expression.match_arm_list() else {
1963 continue;
1964 };
1965 let arms = list.arms().collect::<Vec<_>>();
1966 if arms.is_empty() {
1967 continue;
1968 }
1969 let Some(table_offset) = enclosing_block_entry(expression.syntax()) else {
1970 continue;
1971 };
1972 let table = allocate_table_name(file, &expression, &mut identifiers);
1973 let entries = arms
1974 .iter()
1975 .map(|arm| {
1976 let id = stable_id(file, "branch", arm.syntax().text_range(), "match-arm");
1977 format!(
1978 "{:?}, {:?}",
1979 format!("{id}:missed"),
1980 format!("{id}:selected")
1981 )
1982 })
1983 .collect::<Vec<_>>()
1984 .join(", ");
1985 push_direct(
1986 &mut insertions,
1987 table_offset,
1988 format!("\nconst {table}: &[&str] = &[{entries}];"),
1989 );
1990 for (index, arm) in arms.iter().enumerate() {
1991 let Some(body) = arm.expr() else {
1992 continue;
1993 };
1994 let call = format!("{runtime_path}::arms({table}, {index});");
1995 match &body {
1996 ast::Expr::BlockExpr(block) if plain_block(block) => {
1997 if let Some(offset) = block_entry_offset(block) {
1998 push_direct(&mut insertions, offset, format!("\n{call}"));
1999 }
2000 }
2001 _ => push_wrapper(
2002 &mut insertions,
2003 body.syntax().text_range(),
2004 arm.syntax().text_range(),
2005 0,
2006 format!("{{ {call} ("),
2007 ") }".into(),
2008 ),
2009 }
2010 }
2011 }
2012
2013 for binary in root.descendants().filter_map(ast::BinExpr::cast) {
2017 let short_circuits_when = match binary.op_kind() {
2018 Some(BinaryOp::LogicOp(LogicOp::And)) => false,
2019 Some(BinaryOp::LogicOp(LogicOp::Or)) => true,
2020 _ => continue,
2021 };
2022 if cannot_carry_probe(binary.syntax()) {
2023 continue;
2024 }
2025 let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
2026 continue;
2027 };
2028 if has_let(&left) {
2032 continue;
2033 }
2034 let kind = if short_circuits_when {
2035 "logical-or"
2036 } else {
2037 "logical-and"
2038 };
2039 let id = stable_id(file, "branch", right.syntax().text_range(), kind);
2040 push_wrapper(
2041 &mut insertions,
2042 left.syntax().text_range(),
2043 binary.syntax().text_range(),
2044 2,
2045 format!("{runtime_path}::logical(("),
2046 format!(
2047 "), {short_circuits_when}, {:?}, {:?})",
2048 format!("{id}:short-circuit"),
2049 format!("{id}:evaluated")
2050 ),
2051 );
2052 }
2053
2054 for expression in root.descendants().filter_map(ast::ForExpr::cast) {
2058 if cannot_carry_probe(expression.syntax()) {
2059 continue;
2060 }
2061 let Some(iterable) = expression.iterable() else {
2062 continue;
2063 };
2064 let id = stable_id(file, "branch", expression.syntax().text_range(), "for-loop");
2065 push_wrapper(
2069 &mut insertions,
2070 iterable.syntax().text_range(),
2071 iterable.syntax().text_range(),
2072 0,
2073 format!("{runtime_path}::for_loop(("),
2074 format!(
2075 "), {:?}, {:?})",
2076 format!("{id}:zero"),
2077 format!("{id}:entered")
2078 ),
2079 );
2080 }
2081
2082 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
2086 if cannot_carry_probe(expression.syntax()) {
2087 continue;
2088 }
2089 let Some(offset) = expression.loop_body().as_ref().and_then(block_entry_offset) else {
2090 continue;
2091 };
2092 let id = stable_id(
2093 file,
2094 "branch",
2095 expression.syntax().text_range(),
2096 "while-loop",
2097 );
2098 let flag = allocate_flag_name(file, &expression, &mut identifiers);
2099 let range = range_after_attributes(&expression);
2100 push_wrapper(
2101 &mut insertions,
2102 range,
2103 range,
2104 0,
2105 format!("{{ let mut {flag} = true; "),
2106 format!(
2107 " {runtime_path}::zero_iterations({flag}, {:?}) }}",
2108 format!("{id}:zero")
2109 ),
2110 );
2111 push_direct(
2112 &mut insertions,
2113 offset,
2114 format!(
2115 "\n{runtime_path}::entered(&mut {flag}, {:?});",
2116 format!("{id}:entered")
2117 ),
2118 );
2119 }
2120
2121 for expression in root.descendants().filter_map(ast::TryExpr::cast) {
2125 if cannot_carry_probe(expression.syntax()) {
2126 continue;
2127 }
2128 let Some(operand) = expression.expr() else {
2129 continue;
2130 };
2131 let id = stable_id(
2132 file,
2133 "branch",
2134 expression.syntax().text_range(),
2135 "try-operator",
2136 );
2137 push_wrapper(
2140 &mut insertions,
2141 operand.syntax().text_range(),
2142 operand.syntax().text_range(),
2143 0,
2144 format!("{runtime_path}::TryProbe::probe(("),
2145 format!(
2146 "), {:?}, {:?})",
2147 format!("{id}:continued"),
2148 format!("{id}:returned")
2149 ),
2150 );
2151 }
2152
2153 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
2154 let Some(condition) = expression.condition() else {
2155 continue;
2156 };
2157 if has_let(&condition) {
2158 if !cannot_carry_probe(condition.syntax()) {
2159 instrument_let_chain(
2160 &mut insertions,
2161 runtime_path,
2162 file,
2163 &condition,
2164 ChainHost::If(&expression),
2165 &mut identifiers,
2166 );
2167 }
2168 continue;
2169 }
2170 let frame_name = allocate_frame_name(file, &condition, "if", &mut identifiers);
2171 instrument_decision(
2172 &mut insertions,
2173 runtime_path,
2174 file,
2175 &condition,
2176 "if",
2177 &frame_name,
2178 );
2179 }
2180 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
2181 let Some(condition) = expression.condition() else {
2182 continue;
2183 };
2184 if has_let(&condition) {
2185 if !cannot_carry_probe(condition.syntax()) {
2186 instrument_let_chain(
2187 &mut insertions,
2188 runtime_path,
2189 file,
2190 &condition,
2191 ChainHost::While(&expression),
2192 &mut identifiers,
2193 );
2194 }
2195 continue;
2196 }
2197 let frame_name = allocate_frame_name(file, &condition, "while", &mut identifiers);
2198 instrument_decision(
2199 &mut insertions,
2200 runtime_path,
2201 file,
2202 &condition,
2203 "while",
2204 &frame_name,
2205 );
2206 }
2207 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
2208 if let Some(condition) = guard.condition() {
2209 let frame_name = allocate_frame_name(file, &condition, "match-guard", &mut identifiers);
2210 instrument_decision(
2211 &mut insertions,
2212 runtime_path,
2213 file,
2214 &condition,
2215 "match-guard",
2216 &frame_name,
2217 );
2218 }
2219 }
2220 for arguments in &assertions {
2227 let Some(condition) = assertion_condition(root, *arguments) else {
2228 continue;
2229 };
2230 let frame_name = allocate_frame_name(file, &condition, "assert", &mut identifiers);
2231 if !instrument_decision(
2232 &mut insertions,
2233 runtime_path,
2234 file,
2235 &condition,
2236 "assert",
2237 &frame_name,
2238 ) {
2239 continue;
2240 }
2241 if assertion_argument_count(root, *arguments) == 1 {
2242 let range = condition.syntax().text_range();
2243 let original = &source[usize::from(range.start())..usize::from(range.end())];
2244 push_direct(
2245 &mut insertions,
2246 usize::from(range.end()),
2247 format!(", \"assertion failed: {{}}\", stringify!({original})"),
2248 );
2249 }
2250 }
2251 for expression in standalone_matches(root, &matches, &assertions) {
2254 let frame_name = allocate_frame_name(file, &expression, "matches", &mut identifiers);
2255 instrument_decision(
2256 &mut insertions,
2257 runtime_path,
2258 file,
2259 &expression,
2260 "matches",
2261 &frame_name,
2262 );
2263 }
2264
2265 if skipped_attributed_statement {
2266 add_manifest_limitation(
2267 &mut manifest,
2268 file,
2269 "rust-attributed-statement-probes-not-injected",
2270 "A `let` without an initializer that carries outer attributes has no expression to hold a probe",
2271 );
2272 }
2273 manifest.limitations.sort_by(|left, right| {
2274 left.get("id")
2275 .and_then(|value| value.as_str())
2276 .cmp(&right.get("id").and_then(|value| value.as_str()))
2277 });
2278
2279 let code = apply_insertions(source, insertions)?;
2280 let transformed = SourceFile::parse(&code, edition);
2281 let errors = transformed
2282 .errors()
2283 .into_iter()
2284 .map(|error| error.to_string())
2285 .collect::<Vec<_>>();
2286 if !errors.is_empty() {
2287 if let Some(directory) = std::env::var_os(FAILED_TRANSFORM_DUMP_ENV) {
2291 let name = file.replace(['/', '\\'], "__");
2292 let _ = std::fs::create_dir_all(&directory);
2293 let _ = std::fs::write(std::path::Path::new(&directory).join(name), &code);
2294 }
2295 return Err(RustInstrumenterError::Parse(errors));
2296 }
2297 Ok(RustInstrumentedSource { code, manifest })
2298}
2299
2300#[cfg(test)]
2301mod tests {
2302 use std::{
2303 fs,
2304 process::Command,
2305 time::{SystemTime, UNIX_EPOCH},
2306 };
2307
2308 use super::*;
2309
2310 fn limitation_kind_of(limitation: &serde_json::Value) -> Option<&str> {
2312 limitation.get("id")?.as_str()?.split('#').next()
2313 }
2314
2315 const NOOP_RUNTIME: &str = r#"
2316#[doc(hidden)]
2317mod __supercov_runtime_v1 {
2318 pub struct DecisionFrame;
2319 impl DecisionFrame {
2320 pub fn new(_: &'static str, _: usize) -> Self { Self }
2321 }
2322 pub fn hit(_: &'static str) {}
2323 pub fn arms(_: &[&'static str], _: usize) {}
2324 pub fn logical(left: bool, _: bool, _: &'static str, _: &'static str) -> bool { left }
2325 pub fn for_loop<I: IntoIterator>(iterable: I, _: &'static str, _: &'static str) -> I::IntoIter {
2326 iterable.into_iter()
2327 }
2328 pub fn entered(_: &mut bool, _: &'static str) {}
2329 pub fn zero_iterations(_: bool, _: &'static str) {}
2330 pub trait TryProbe: Sized {
2331 fn probe(self, _: &'static str, _: &'static str) -> Self { self }
2332 }
2333 impl<T> TryProbe for T {}
2334 pub fn condition<V: std::ops::Not<Output = bool>>(value: V, _: &mut DecisionFrame, _: usize) -> bool { !!value }
2335 pub fn decision(value: bool, _: &mut DecisionFrame) -> bool { value }
2336 pub fn reached(_: &mut DecisionFrame, _: usize) -> bool { true }
2337 pub fn decision_chain(_: &mut DecisionFrame, _: bool, _: &[(usize, &'static str, &'static str)]) {}
2338 pub fn assertion(_: &'static str) {}
2339}
2340"#;
2341
2342 fn compile_and_run(source: &str, name: &str) -> std::process::Output {
2343 compile_and_run_edition(source, name, "2024")
2344 }
2345
2346 fn compile_and_run_edition(source: &str, name: &str, edition: &str) -> std::process::Output {
2347 let nonce = SystemTime::now()
2348 .duration_since(UNIX_EPOCH)
2349 .unwrap()
2350 .as_nanos();
2351 let directory = std::env::temp_dir().join(format!(
2352 "supercov-rust-transform-{}-{nonce}-{name}",
2353 std::process::id()
2354 ));
2355 fs::create_dir(&directory).unwrap();
2356 let input = directory.join("main.rs");
2357 let binary = directory.join("program");
2358 fs::write(&input, source).unwrap();
2359 let compile = Command::new("rustc")
2360 .arg(format!("--edition={edition}"))
2361 .arg(&input)
2362 .arg("-o")
2363 .arg(&binary)
2364 .output()
2365 .unwrap();
2366 assert!(
2367 compile.status.success(),
2368 "rustc failed:\n{}\nsource:\n{source}",
2369 String::from_utf8_lossy(&compile.stderr)
2370 );
2371 let output = Command::new(&binary).output().unwrap();
2372 fs::remove_dir_all(directory).unwrap();
2373 output
2374 }
2375
2376 #[test]
2377 fn discovers_rust_obligations_with_exact_ranges_and_stable_ids() {
2378 let source = r#"fn classify<T>(values: &[T], first: bool, second: bool, third: bool) -> Option<&T> {
2379 let picked = if first && (second || third) {
2380 values.first()?
2381 } else {
2382 None
2383 };
2384 for value in values {
2385 if first || second {
2386 return Some(value);
2387 }
2388 }
2389 match picked {
2390 Some(value) if second && third => Some(value),
2391 _ => None,
2392 }
2393}
2394
2395fn closure(value: i32) -> bool {
2396 (|candidate| candidate > 0)(value)
2397}
2398"#;
2399 let first = build_rust_manifest("src/lib.rs", source).unwrap();
2400 let second = build_rust_manifest("src/lib.rs", source).unwrap();
2401 assert_eq!(first, second);
2402 assert!(first.points.iter().any(|point| {
2403 point.kind == PointKind::Function && point.label.as_deref() == Some("classify")
2404 }));
2405 assert!(first.points.iter().any(|point| {
2406 point.kind == PointKind::Function && point.label.as_deref() == Some("<closure>")
2407 }));
2408 let first_if = first
2409 .decisions
2410 .iter()
2411 .find(|decision| decision.line == 2)
2412 .unwrap();
2413 assert_eq!(first_if.conditions, ["first", "second", "third"]);
2414 assert_eq!(first_if.column, 20);
2415 assert!(
2416 first
2417 .branches
2418 .iter()
2419 .any(|branch| branch.kind == "for-loop")
2420 );
2421 let mut arms = first
2422 .branches
2423 .iter()
2424 .filter(|branch| branch.kind == "match-arm")
2425 .collect::<Vec<_>>();
2426 arms.sort_by_key(|branch| branch.line);
2427 assert_eq!(arms.len(), 2);
2428 assert_eq!(
2429 arms[0]
2430 .alternatives
2431 .iter()
2432 .map(|alternative| alternative.label.as_str())
2433 .collect::<Vec<_>>(),
2434 ["not selected", "selected"]
2435 );
2436 assert_eq!(
2439 arms[1]
2440 .alternatives
2441 .iter()
2442 .map(|alternative| alternative.label.as_str())
2443 .collect::<Vec<_>>(),
2444 ["selected"]
2445 );
2446 assert!(
2447 first
2448 .branches
2449 .iter()
2450 .any(|branch| branch.kind == "try-operator")
2451 );
2452 assert!(first.decisions.iter().all(|decision| {
2453 decision.id.starts_with("rs:decision:") && decision.conditions.len() >= 2
2454 }));
2455 assert!(first.limitations.is_empty());
2456 }
2457
2458 #[test]
2459 fn declares_macro_and_const_boundaries_instead_of_hiding_them() {
2460 let source = r#"const fn doubled(value: usize) -> usize { value * 2 }
2461
2462macro_rules! noop {
2463 () => {};
2464}
2465
2466fn checked(value: bool) -> bool {
2467 assert!(value);
2468 noop!();
2469 const { doubled(2) == 4 }
2470}
2471"#;
2472 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
2473 assert!(manifest.decisions.iter().any(|decision| {
2476 decision.line == 8 && decision.source == "value" && decision.conditions == ["value"]
2477 }));
2478 let ids = manifest
2479 .limitations
2480 .iter()
2481 .filter_map(limitation_kind_of)
2482 .collect::<BTreeSet<_>>();
2483 assert_eq!(
2484 ids,
2485 BTreeSet::from([
2486 "rust-const-context-not-instrumented",
2487 "rust-macro-expansion-not-instrumented"
2488 ])
2489 );
2490 assert!(!manifest.points.iter().any(|point| {
2491 point.kind == PointKind::Function && point.label.as_deref() == Some("doubled")
2492 }));
2493 }
2494
2495 #[test]
2496 fn transforms_points_and_nested_decisions_without_changing_behavior() {
2497 let source = r#"use std::sync::atomic::{AtomicUsize, Ordering};
2498
2499static CALLS: AtomicUsize = AtomicUsize::new(0);
2500
2501fn observed(name: &str, value: bool) -> bool {
2502 let order = CALLS.fetch_add(1, Ordering::SeqCst);
2503 println!("{order}:{name}:{value}");
2504 value
2505}
2506
2507fn classify(first: bool, second: bool, third: bool) -> i32 {
2508 if observed("a", first) && (observed("b", second) || observed("c", third)) {
2509 7
2510 } else {
2511 3
2512 }
2513}
2514
2515fn main() {
2516 let closure = |value: i32| value + 1;
2517 println!("result={}", closure(classify(true, false, true)));
2518}
2519"#;
2520 let transformed =
2521 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2522 assert!(transformed.code.contains("::condition("));
2523 assert!(transformed.code.contains("::decision("));
2524 assert!(transformed.code.contains("::hit("));
2525 let original = compile_and_run(source, "original");
2526 let instrumented = compile_and_run(
2527 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2528 "instrumented",
2529 );
2530 assert_eq!(instrumented.status, original.status);
2531 assert_eq!(instrumented.stdout, original.stdout);
2532 assert_eq!(instrumented.stderr, original.stderr);
2533 }
2534
2535 #[test]
2536 fn let_chains_take_derived_condition_probes_and_const_contexts_stay_declared() {
2537 let source = r#"const fn enabled(value: bool) -> bool {
2538 if value { true } else { false }
2539}
2540
2541fn classify(value: Option<bool>, fallback: bool) -> bool {
2542 if let Some(inner) = value && inner && fallback { true } else { false }
2543}
2544"#;
2545 let transformed =
2546 instrument_rust_source("src/lib.rs", source, "crate::__supercov_runtime_v1").unwrap();
2547 let ids = transformed
2548 .manifest
2549 .limitations
2550 .iter()
2551 .filter_map(limitation_kind_of)
2552 .collect::<BTreeSet<_>>();
2553 assert!(ids.contains("rust-const-context-not-instrumented"));
2554 assert!(!ids.contains("rust-let-chain-probes-not-injected"));
2555 assert!(
2558 transformed
2559 .code
2560 .contains("::reached(&mut __supercov_decision_")
2561 );
2562 assert!(
2563 transformed
2564 .code
2565 .contains("::condition((inner), &mut __supercov_decision_")
2566 );
2567 assert!(
2568 transformed
2569 .code
2570 .contains("::decision_chain(&mut __supercov_decision_")
2571 );
2572 assert!(transformed.code.contains("&& let Some(inner) = value &&"));
2573 assert!(!transformed.code.contains("condition((let"));
2574 }
2575
2576 #[test]
2577 fn std_macro_arguments_take_probes_and_assertions_are_decisions() {
2578 let source = r#"use std::fmt::Write as _;
2579
2580fn classify(values: &[i32], strict: bool) -> String {
2581 let mut out = String::new();
2582 assert!(values.len() < 10 && (strict || !values.is_empty()), "bad input {:?}", values);
2583 debug_assert!(values.iter().all(|v| *v > -100));
2584 let doubled = vec![values.iter().map(|v| v * 2).sum::<i32>(), if strict { 1 } else { 2 }];
2585 let repeated = vec![if strict { 1 } else { 0 }; values.len()];
2586 let small = matches!(values.first(), Some(v) if *v < 3);
2587 if matches!(values.len(), 1 | 2) && small {
2588 println!("small");
2589 }
2590 println!("{}", repeated.len() + small as usize);
2591 write!(out, "{}", doubled.iter().map(|d| if *d > 4 { "big" } else { "small" }).collect::<Vec<_>>().join(",")).unwrap();
2592 println!("{} {}", format!("{:?}", doubled), if values.first().copied().unwrap_or(0) > 0 && strict { "positive" } else { "other" });
2593 assert_eq!(doubled.len(), if strict { 2 } else { 2 }, "length for strict={strict}");
2594 out
2595}
2596
2597fn main() {
2598 println!("{}", classify(&[1, 2], true));
2599 println!("{}", classify(&[3], false));
2600 println!("{}", classify(&[], true));
2601 let total: i32 = dbg!(vec![1, 2, 3]).into_iter().sum();
2602 println!("{total}");
2603}
2604"#;
2605 let transformed =
2606 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2607 assert!(transformed.code.contains(
2610 r#", "assertion failed: {}", stringify!(values.iter().all(|v| *v > -100)))"#
2611 ));
2612 assert!(
2614 transformed
2615 .code
2616 .contains(r#"), "bad input {:?}", values);"#)
2617 );
2618 let assertion = transformed
2622 .manifest
2623 .decisions
2624 .iter()
2625 .find(|decision| decision.line == 5)
2626 .expect("assert! decision");
2627 assert_eq!(
2628 assertion.conditions,
2629 ["values.len() < 10", "strict", "!values.is_empty()"]
2630 );
2631 assert!(
2632 transformed
2633 .manifest
2634 .decisions
2635 .iter()
2636 .any(|decision| decision.line == 7)
2637 );
2638 assert!(
2639 transformed
2640 .manifest
2641 .decisions
2642 .iter()
2643 .any(|decision| decision.line == 9)
2644 );
2645 assert!(
2646 transformed
2647 .code
2648 .contains("assert!(({ let mut __supercov_decision_")
2649 );
2650 assert!(
2651 transformed
2652 .code
2653 .contains(", if ({ let mut __supercov_decision_")
2654 );
2655 assert!(
2657 transformed
2658 .code
2659 .contains("vec![if ({ let mut __supercov_decision_")
2660 );
2661 assert!(
2664 transformed
2665 .code
2666 .contains("let small = ({ let mut __supercov_decision_")
2667 );
2668 assert_eq!(
2669 transformed
2670 .manifest
2671 .decisions
2672 .iter()
2673 .filter(|decision| decision.line == 10)
2674 .count(),
2675 1
2676 );
2677 assert_eq!(
2678 transformed
2679 .manifest
2680 .decisions
2681 .iter()
2682 .filter(|decision| decision.line == 9)
2683 .count(),
2684 1
2685 );
2686 assert!(
2687 transformed
2688 .code
2689 .contains("vec![values.iter().map(|v| { crate::__supercov_runtime_v1::hit(")
2690 );
2691 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
2692 limitation.get("id").and_then(|id| id.as_str())
2693 == Some("rust-macro-expansion-not-instrumented")
2694 }));
2695 let original = compile_and_run(source, "original-macros");
2696 let instrumented = compile_and_run(
2697 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2698 "instrumented-macros",
2699 );
2700 assert_eq!(instrumented.status, original.status);
2701 assert_eq!(instrumented.stdout, original.stdout);
2702 let after_location = |stderr: &[u8]| {
2705 String::from_utf8_lossy(stderr)
2706 .lines()
2707 .map(|line| {
2708 line.split_once("] ")
2709 .map_or(line, |(_, rest)| rest)
2710 .to_owned()
2711 })
2712 .collect::<Vec<_>>()
2713 };
2714 assert_eq!(
2715 after_location(&instrumented.stderr),
2716 after_location(&original.stderr)
2717 );
2718 }
2719
2720 #[test]
2721 fn assertion_panic_messages_survive_instrumentation() {
2722 let source = r#"fn grow(len: usize, new_capacity: usize) {
2725 assert!(new_capacity >= len);
2726}
2727
2728fn check(value: i32) {
2729 assert!(value > 0 && value < 10, "value {value} out of range");
2730}
2731
2732// tokio: `assert!` only negates its operand, so a `&bool` is accepted.
2733fn all_seen(seen: &[bool]) {
2734 for was_seen in seen {
2735 assert!(was_seen);
2736 debug_assert!(was_seen, "seen");
2737 }
2738}
2739
2740fn main() {
2741 std::panic::set_hook(Box::new(|_| {}));
2742 all_seen(&[true, true]);
2743 match std::panic::catch_unwind(|| all_seen(&[true, false])) {
2744 Ok(()) => println!("ok"),
2745 Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
2746 }
2747 for (len, capacity) in [(3, 5), (8, 5)] {
2748 match std::panic::catch_unwind(|| grow(len, capacity)) {
2749 Ok(()) => println!("ok"),
2750 Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
2751 }
2752 }
2753 match std::panic::catch_unwind(|| check(12)) {
2754 Ok(()) => println!("ok"),
2755 Err(payload) => println!("{}", payload.downcast_ref::<String>().cloned().unwrap_or_default()),
2756 }
2757}
2758"#;
2759 let transformed =
2760 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2761 let original = compile_and_run(source, "original-assert-message");
2762 let instrumented = compile_and_run(
2763 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2764 "instrumented-assert-message",
2765 );
2766 assert_eq!(instrumented.status, original.status);
2767 assert_eq!(instrumented.stdout, original.stdout);
2768 assert!(
2769 String::from_utf8_lossy(&instrumented.stdout)
2770 .contains("assertion failed: new_capacity >= len")
2771 );
2772 assert!(
2773 String::from_utf8_lossy(&instrumented.stdout).contains("assertion failed: was_seen")
2774 );
2775 }
2776
2777 #[test]
2778 fn files_that_predate_a_reserved_word_still_instrument() {
2779 let source = r#"struct Rng(u64);
2781impl Rng {
2782 fn gen(&mut self) -> u64 {
2783 self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1);
2784 self.0 >> 33
2785 }
2786}
2787
2788fn main() {
2789 let mut rng = Rng(7);
2790 let mut odd = 0;
2791 for _ in 0..10 {
2792 if rng.gen() % 2 == 1 {
2793 odd += 1;
2794 }
2795 }
2796 println!("{odd}");
2797}
2798"#;
2799 let transformed =
2800 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2801 assert!(
2802 transformed
2803 .manifest
2804 .decisions
2805 .iter()
2806 .any(|decision| decision.line == 13)
2807 );
2808 let original = compile_and_run_edition(source, "original-gen", "2021");
2809 let instrumented = compile_and_run_edition(
2810 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2811 "instrumented-gen",
2812 "2021",
2813 );
2814 assert_eq!(instrumented.status, original.status);
2815 assert_eq!(instrumented.stdout, original.stdout);
2816 }
2817
2818 #[test]
2819 fn lone_let_conditions_compile_before_edition_2024_and_record_breaks() {
2820 let source = r#"fn first_even(values: &[i32]) -> Option<i32> {
2824 let mut it = values.iter();
2825 'scan: while let Some(value) = it.next() {
2826 if *value < 0 {
2827 break;
2828 }
2829 for _ in 0..1 {
2830 if *value == 99 {
2831 break 'scan;
2832 }
2833 if *value == 98 {
2834 break;
2835 }
2836 }
2837 if *value % 2 == 0 {
2838 return Some(*value);
2839 }
2840 }
2841 None
2842}
2843
2844fn describe(value: Option<i32>) -> &'static str {
2845 if let Some(inner) = value {
2846 if inner > 0 { "positive" } else { "non-positive" }
2847 } else if let None = value {
2848 "none"
2849 } else {
2850 "unreachable"
2851 }
2852}
2853
2854fn count(values: &[Option<i32>]) -> usize {
2855 let mut total = 0;
2856 for value in values {
2857 if let Some(_) = value {
2858 total += 1;
2859 }
2860 }
2861 total
2862}
2863
2864fn main() {
2865 println!("{:?} {:?} {:?} {:?}", first_even(&[1, 3, 4]), first_even(&[1, -1, 4]), first_even(&[99, 4]), first_even(&[98, 3, 6]));
2866 println!("{} {} {}", describe(Some(2)), describe(Some(-2)), describe(None));
2867 println!("{}", count(&[Some(1), None, Some(3)]));
2868}
2869"#;
2870 let transformed =
2871 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2872 assert!(!transformed.code.contains("&& let"));
2873 assert!(transformed.code.contains("__supercov_broke_"));
2874 assert_eq!(transformed.code.matches("= true; break").count(), 2);
2875 for edition in ["2021", "2024"] {
2876 let original =
2877 compile_and_run_edition(source, &format!("original-lone-let-{edition}"), edition);
2878 let instrumented = compile_and_run_edition(
2879 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2880 &format!("instrumented-lone-let-{edition}"),
2881 edition,
2882 );
2883 assert_eq!(instrumented.status, original.status);
2884 assert_eq!(instrumented.stdout, original.stdout);
2885 assert_eq!(instrumented.stderr, original.stderr);
2886 }
2887 }
2888
2889 #[test]
2890 fn let_chains_keep_their_behavior() {
2891 let source = r#"fn describe(value: Option<i32>, flag: bool) -> &'static str {
2892 if let Some(inner) = value && inner > 0 && flag {
2893 "positive"
2894 } else if let Some(inner) = value && (inner < 0 || flag) {
2895 "negative-or-flagged"
2896 } else {
2897 "other"
2898 }
2899}
2900
2901fn count_pairs(values: &[(Option<i32>, i32)]) -> i32 {
2902 let mut total = 0;
2903 let mut it = values.iter();
2904 while let Some((first, second)) = it.next() && let Some(inner) = first && *second > 0 {
2905 total += inner * second;
2906 if total > 100 {
2907 break;
2908 }
2909 }
2910 total
2911}
2912
2913fn tail(value: Option<&str>) -> usize {
2914 let pick = |v: Option<&str>| if let Some(text) = v && !text.is_empty() { text.len() } else { 0 };
2915 if let Some(text) = value && text.starts_with('x') {
2916 println!("x-prefixed");
2917 }
2918 pick(value)
2919}
2920
2921fn main() {
2922 for value in [Some(3), Some(-3), Some(0), None] {
2923 for flag in [true, false] {
2924 println!("{value:?} {flag} {}", describe(value, flag));
2925 }
2926 }
2927 println!("{}", count_pairs(&[(Some(2), 3), (Some(4), 5), (None, 1), (Some(9), 9)]));
2928 println!("{}", count_pairs(&[(Some(50), 3), (Some(4), 5)]));
2929 println!("{} {} {}", tail(Some("xyz")), tail(Some("")), tail(None));
2930}
2931"#;
2932 let transformed =
2933 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2934 assert_eq!(
2935 transformed.code.matches("const __SUPERCOV_CHAIN_").count(),
2936 5
2937 );
2938 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
2939 limitation.get("id").and_then(|id| id.as_str())
2940 == Some("rust-let-chain-probes-not-injected")
2941 }));
2942 let original = compile_and_run(source, "original-chains");
2943 let instrumented = compile_and_run(
2944 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2945 "instrumented-chains",
2946 );
2947 assert_eq!(instrumented.status, original.status);
2948 assert_eq!(instrumented.stdout, original.stdout);
2949 assert_eq!(instrumented.stderr, original.stderr);
2950 }
2951
2952 #[test]
2953 fn instrumented_const_and_static_initialisers_still_compile() {
2954 let source = r#"const DIRECT: usize = if cfg!(unix) { 100 } else { 1_000 };
2960static WIDTH: usize = if cfg!(unix) { 2 } else { 4 };
2961
2962enum Mode {
2963 Narrow = if cfg!(unix) { 1 } else { 2 },
2964}
2965
2966struct Buffer([u8; if cfg!(unix) { 4 } else { 8 }]);
2967
2968impl Buffer {
2969 const SPAN: usize = if cfg!(unix) { 5 } else { 9 };
2970}
2971
2972fn scaled(flag: bool) -> usize {
2973 const LOCAL: usize = if cfg!(unix) { 3 } else { 6 };
2974 if flag { LOCAL + Buffer::SPAN } else { DIRECT + WIDTH }
2975}
2976
2977fn main() {
2978 let buffer = Buffer([0; if cfg!(unix) { 4 } else { 8 }]);
2979 println!(
2980 "{} {} {} {}",
2981 scaled(true),
2982 scaled(false),
2983 Mode::Narrow as usize,
2984 buffer.0.len()
2985 );
2986}
2987"#;
2988 let transformed =
2989 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2990 assert!(transformed.code.contains("::decision("));
2993 let ids = transformed
2994 .manifest
2995 .limitations
2996 .iter()
2997 .filter_map(limitation_kind_of)
2998 .collect::<BTreeSet<_>>();
2999 assert!(ids.contains("rust-const-context-not-instrumented"));
3000
3001 let original = compile_and_run(source, "const-original");
3002 let instrumented = compile_and_run(
3003 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3004 "const-instrumented",
3005 );
3006 assert_eq!(instrumented.status, original.status);
3007 assert_eq!(instrumented.stdout, original.stdout);
3008 assert_eq!(instrumented.stderr, original.stderr);
3009 }
3010
3011 #[test]
3012 fn a_probed_global_allocator_would_recurse_into_itself() {
3013 let source = r#"use std::alloc::{GlobalAlloc, Layout, System};
3019
3020struct Odd;
3021
3022unsafe impl GlobalAlloc for Odd {
3023 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
3024 if layout.align() == 1 && layout.size() > 0 {
3025 System.alloc(layout)
3026 } else {
3027 System.alloc(layout)
3028 }
3029 }
3030
3031 unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
3032 System.dealloc(pointer, layout);
3033 }
3034}
3035
3036#[global_allocator]
3037static ODD: Odd = Odd;
3038
3039fn classify(flag: bool) -> usize {
3040 if flag { 1 } else { 2 }
3041}
3042
3043fn main() {
3044 let held = std::vec![7u8; 32];
3045 println!("{} {}", classify(!held.is_empty()), held.len());
3046}
3047"#;
3048 let transformed =
3049 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3050 let allocator = transformed
3052 .code
3053 .split("unsafe impl GlobalAlloc for Odd")
3054 .nth(1)
3055 .and_then(|rest| rest.split("#[global_allocator]").next())
3056 .expect("the instrumented source still contains the allocator impl");
3057 assert!(
3058 !allocator.contains("__supercov_runtime_v1"),
3059 "probe injected into a GlobalAlloc impl:\n{allocator}"
3060 );
3061 assert!(transformed.code.contains("::decision("));
3063 let ids = transformed
3064 .manifest
3065 .limitations
3066 .iter()
3067 .filter_map(limitation_kind_of)
3068 .collect::<BTreeSet<_>>();
3069 assert!(ids.contains("rust-global-allocator-not-instrumented"));
3070
3071 let original = compile_and_run(source, "alloc-original");
3072 let instrumented = compile_and_run(
3073 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3074 "alloc-instrumented",
3075 );
3076 assert_eq!(instrumented.status, original.status);
3077 assert_eq!(instrumented.stdout, original.stdout);
3078 assert_eq!(instrumented.stderr, original.stderr);
3079 }
3080
3081 #[test]
3082 fn match_arms_record_selection_without_changing_behavior() {
3083 let source = r#"#[derive(Debug)]
3084enum Shape { Dot, Line(i32), Box { w: i32, h: i32 } }
3085
3086fn area(shape: &Shape) -> i32 {
3087 match shape {
3088 Shape::Dot => 0,
3089 Shape::Line(length) if *length < 0 => -length,
3090 Shape::Line(length) => *length,
3091 Shape::Box { w, h } => {
3092 let area = w * h;
3093 area
3094 }
3095 }
3096}
3097
3098fn describe(value: i32) -> &'static str {
3099 let inner = |v: i32| match v { 0 => "none", 1 => "one", _ => "many" };
3100 match value {
3101 0 => inner(value),
3102 n if n < 0 => unsafe { std::hint::unreachable_unchecked() },
3103 n => match n % 2 {
3104 0 => "even",
3105 _ => inner(n),
3106 },
3107 }
3108}
3109
3110fn main() {
3111 for shape in [Shape::Dot, Shape::Line(-3), Shape::Line(4), Shape::Box { w: 2, h: 5 }] {
3112 println!("{shape:?}={}", area(&shape));
3113 }
3114 for value in [0, 1, 3, 8] {
3115 println!("{value}:{}", describe(value));
3116 }
3117}
3118"#;
3119 let transformed =
3120 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3121 assert!(transformed.code.contains("::arms(__SUPERCOV_ARMS_"));
3122 assert_eq!(
3123 transformed.code.matches("const __SUPERCOV_ARMS_").count(),
3124 4
3125 );
3126 let arms = transformed
3127 .manifest
3128 .branches
3129 .iter()
3130 .filter(|branch| branch.kind == "match-arm")
3131 .count();
3132 assert_eq!(arms, 4 + 3 + 3 + 2);
3133 for branch in transformed
3136 .manifest
3137 .branches
3138 .iter()
3139 .filter(|branch| branch.kind == "match-arm")
3140 {
3141 for alternative in &branch.alternatives {
3142 assert!(
3143 transformed.code.contains(&format!("{:?}", alternative.id)),
3144 "{} is not in any table",
3145 alternative.id
3146 );
3147 }
3148 }
3149 let original = compile_and_run(source, "original-arms");
3150 let instrumented = compile_and_run(
3151 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3152 "instrumented-arms",
3153 );
3154 assert_eq!(instrumented.status, original.status);
3155 assert_eq!(instrumented.stdout, original.stdout);
3156 assert_eq!(instrumented.stderr, original.stderr);
3157 }
3158
3159 #[test]
3160 fn loops_logic_and_try_record_their_branches_without_changing_behavior() {
3161 let source = r#"use std::ops::ControlFlow;
3162
3163fn total(values: &[i32]) -> i32 {
3164 let mut sum = 0;
3165 for value in values {
3166 sum += value;
3167 }
3168 'outer: for row in 0..3 {
3169 for column in 0..3 {
3170 if column > row {
3171 continue 'outer;
3172 }
3173 sum += row * column;
3174 }
3175 }
3176 sum
3177}
3178
3179fn first_even(values: &[i32]) -> Option<i32> {
3180 let mut index = 0;
3181 'scan: while index < values.len() {
3182 if values[index] % 2 == 0 {
3183 break 'scan;
3184 }
3185 index += 1;
3186 }
3187 let mut it = values.iter().skip(index);
3188 while let Some(value) = it.next() {
3189 return Some(*value);
3190 }
3191 None
3192}
3193
3194fn parse_twice(text: &str) -> Result<i32, String> {
3195 let value: i32 = text.trim().parse().map_err(|_| "bad".to_string())?;
3196 let doubled = Some(value).map(|v| v * 2).ok_or("none")?;
3197 Ok(doubled)
3198}
3199
3200fn halve(value: i32) -> Option<i32> {
3201 let even = (value % 2 == 0).then_some(value)?;
3202 Some(even / 2)
3203}
3204
3205fn flow(values: &[i32]) -> ControlFlow<i32, i32> {
3206 let mut sum = 0;
3207 for value in values {
3208 let step: ControlFlow<i32, i32> = if *value < 0 { ControlFlow::Break(*value) } else { ControlFlow::Continue(*value) };
3209 sum += step?;
3210 }
3211 ControlFlow::Continue(sum)
3212}
3213
3214fn gate(a: bool, b: bool, c: bool) -> bool {
3215 let both = a && b;
3216 let either = a || b || c;
3217 both || (either && !c) || (c && a && (b || !b))
3218}
3219
3220fn main() {
3221 println!("{} {}", total(&[]), total(&[1, 2, 3]));
3222 println!("{:?} {:?} {:?}", first_even(&[]), first_even(&[1, 3]), first_even(&[1, 4, 6]));
3223 println!("{:?} {:?}", parse_twice(" 21 "), parse_twice("x"));
3224 println!("{:?} {:?}", halve(8), halve(7));
3225 println!("{:?} {:?}", flow(&[1, 2]), flow(&[1, -5, 2]));
3226 for a in [false, true] {
3227 for b in [false, true] {
3228 for c in [false, true] {
3229 print!("{}", gate(a, b, c) as u8);
3230 }
3231 }
3232 }
3233 println!();
3234}
3235"#;
3236 let transformed =
3237 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3238 for marker in [
3239 "::logical((",
3240 "::for_loop((",
3241 "::entered(&mut __supercov_loop_",
3242 "::zero_iterations(__supercov_loop_",
3243 "::TryProbe::probe((",
3244 ] {
3245 assert!(transformed.code.contains(marker), "{marker} missing");
3246 }
3247 let kinds = |kind: &str| {
3248 transformed
3249 .manifest
3250 .branches
3251 .iter()
3252 .filter(|branch| branch.kind == kind)
3253 .count()
3254 };
3255 assert_eq!(kinds("for-loop"), 3 + 1 + 3);
3256 assert_eq!(kinds("while-loop"), 2);
3257 assert_eq!(kinds("try-operator"), 4);
3258 assert_eq!(kinds("logical-and"), 4);
3259 assert_eq!(kinds("logical-or"), 5);
3260 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
3261 limitation.get("id").and_then(|id| id.as_str())
3262 == Some("rust-structural-branch-probes-not-yet-injected")
3263 }));
3264 let original = compile_and_run(source, "original-structural");
3265 let instrumented = compile_and_run(
3266 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3267 "instrumented-structural",
3268 );
3269 assert_eq!(instrumented.status, original.status);
3270 assert_eq!(instrumented.stdout, original.stdout);
3271 assert_eq!(instrumented.stderr, original.stderr);
3272 }
3273
3274 #[test]
3275 fn cfg_gated_sibling_blocks_keep_their_tail_position() {
3276 let source = r#"pub fn is_available() -> bool {
3282 #[cfg(target_endian = "little")]
3283 {
3284 true
3285 }
3286 #[cfg(not(target_endian = "little"))]
3287 {
3288 false
3289 }
3290}
3291
3292fn main() {
3293 println!("{}", is_available());
3294}
3295"#;
3296 let transformed =
3297 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3298 let original = compile_and_run(source, "cfg-original");
3299 let instrumented = compile_and_run(
3300 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3301 "cfg-instrumented",
3302 );
3303 assert_eq!(instrumented.status, original.status);
3304 assert_eq!(instrumented.stdout, original.stdout);
3305 assert!(
3307 transformed
3308 .code
3309 .contains("{\n\ncrate::__supercov_runtime_v1::hit(")
3310 || transformed
3311 .code
3312 .contains("{\ncrate::__supercov_runtime_v1::hit(")
3313 );
3314
3315 let attributed_let = r#"fn main() {
3319 #[cfg(target_endian = "little")]
3320 let value = 1;
3321 #[cfg(not(target_endian = "little"))]
3322 let value = 2;
3323 #[cfg(target_endian = "little")]
3324 let borrowed: &String = &String::from("little");
3325 #[cfg(not(target_endian = "little"))]
3326 let borrowed: &String = &String::from("big");
3327 #[cfg(target_endian = "little")]
3328 print!("le ");
3329 #[cfg(not(target_endian = "little"))]
3330 print!("be ");
3331 #[allow(unused_assignments)]
3332 let mut later;
3333 later = value + 1;
3334 println!("{value} {borrowed} {later}");
3335}
3336"#;
3337 let transformed = instrument_rust_source(
3338 "src/main.rs",
3339 attributed_let,
3340 "crate::__supercov_runtime_v1",
3341 )
3342 .unwrap();
3343 let ids = transformed
3344 .manifest
3345 .limitations
3346 .iter()
3347 .filter_map(limitation_kind_of)
3348 .collect::<BTreeSet<_>>();
3349 assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
3351 assert!(
3352 transformed
3353 .code
3354 .contains("let value = { crate::__supercov_runtime_v1::hit(")
3355 );
3356 assert!(
3357 transformed
3358 .code
3359 .contains("let borrowed: &String = { crate::__supercov_runtime_v1::hit(")
3360 );
3361 assert!(
3362 transformed
3363 .code
3364 .contains("] { crate::__supercov_runtime_v1::hit(")
3365 );
3366 let original = compile_and_run(attributed_let, "cfg-let-original");
3367 let instrumented = compile_and_run(
3368 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3369 "cfg-let-instrumented",
3370 );
3371 assert_eq!(instrumented.status, original.status);
3372 assert_eq!(instrumented.stdout, original.stdout);
3373 assert_eq!(instrumented.stderr, original.stderr);
3374
3375 let trailing_macro = r#"macro_rules! pick { ($e:expr) => { $e } }
3380fn value() -> i32 {
3381 let base = 20;
3382 #[rustfmt::skip]
3383 pick! { base + 1 }
3384}
3385fn effect() {
3386 #[rustfmt::skip]
3387 println! { "effect" }
3388}
3389fn gated() {
3390 #[cfg(target_endian = "little")]
3391 println! { "little" }
3392}
3393fn main() {
3394 effect();
3395 gated();
3396 println!("{}", value());
3397}
3398"#;
3399 let transformed = instrument_rust_source(
3400 "src/main.rs",
3401 trailing_macro,
3402 "crate::__supercov_runtime_v1",
3403 )
3404 .unwrap();
3405 assert!(
3406 transformed
3407 .code
3408 .contains("{ crate::__supercov_runtime_v1::hit(\"rs:statement:")
3409 );
3410 assert!(
3411 transformed
3412 .code
3413 .contains("); #[rustfmt::skip]\n pick! { base + 1 } }")
3414 );
3415 assert!(
3416 transformed
3417 .code
3418 .contains("); #[rustfmt::skip]\n println! { \"effect\" } }")
3419 );
3420 assert!(
3421 transformed.code.contains(
3422 "\n #[cfg(target_endian = \"little\")]\n println! { \"little\" }\n"
3423 )
3424 );
3425 let ids = transformed
3426 .manifest
3427 .limitations
3428 .iter()
3429 .filter_map(limitation_kind_of)
3430 .collect::<BTreeSet<_>>();
3431 assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
3432 let original = compile_and_run(trailing_macro, "trailing-macro-original");
3433 let instrumented = compile_and_run(
3434 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3435 "trailing-macro-instrumented",
3436 );
3437 assert_eq!(
3438 instrumented.status,
3439 original.status,
3440 "{}",
3441 String::from_utf8_lossy(&instrumented.stderr)
3442 );
3443 assert_eq!(instrumented.stdout, original.stdout);
3444 assert_eq!(instrumented.stderr, original.stderr);
3445 }
3446
3447 #[test]
3448 fn an_attributed_macro_statement_keeps_its_macro_in_statement_position() {
3449 let source = r#"macro_rules! trace {
3454 ($($arg:tt)*) => {
3455 #[cfg(target_endian = "little")]
3456 {
3457 println!($($arg)+);
3458 }
3459 }
3460}
3461fn manual() {
3462 #[cfg(any(target_endian = "little", target_endian = "big"))]
3463 trace!("manual");
3464 let _ = 1;
3465}
3466fn main() {
3467 manual();
3468}
3469"#;
3470 let transformed =
3471 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3472 assert!(
3475 transformed.code.contains(r#"trace!("manual"); }"#),
3476 "{}",
3477 transformed.code
3478 );
3479 let original = compile_and_run(source, "attributed-macro-statement-original");
3480 let instrumented = compile_and_run(
3481 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3482 "attributed-macro-statement-instrumented",
3483 );
3484 assert_eq!(
3485 instrumented.status,
3486 original.status,
3487 "{}",
3488 String::from_utf8_lossy(&instrumented.stderr)
3489 );
3490 assert_eq!(instrumented.stdout, original.stdout);
3491 assert_eq!(instrumented.stderr, original.stderr);
3492 }
3493
3494 #[test]
3495 fn an_attributed_lets_macro_initialiser_keeps_its_value() {
3496 let source = r#"macro_rules! first {
3502 ($e:expr) => { $e }
3503}
3504fn value(flag: bool) -> i32 {
3505 #[cfg(any(target_endian = "little", target_endian = "big"))]
3506 let chosen = first!(if flag { 7 } else { 3 });
3507 chosen + 1
3508}
3509fn main() {
3510 println!("{}", value(true));
3511}
3512"#;
3513 let transformed =
3514 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3515 assert!(
3518 !transformed
3519 .code
3520 .contains("first!(if flag { 7 } else { 3 }); }"),
3521 "{}",
3522 transformed.code
3523 );
3524 let original = compile_and_run(source, "attributed-let-macro-original");
3525 let instrumented = compile_and_run(
3526 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3527 "attributed-let-macro-instrumented",
3528 );
3529 assert_eq!(
3530 instrumented.status,
3531 original.status,
3532 "{}",
3533 String::from_utf8_lossy(&instrumented.stderr)
3534 );
3535 assert_eq!(instrumented.stdout, original.stdout);
3536 assert_eq!(instrumented.stdout, b"8\n");
3537 }
3538
3539 #[test]
3540 fn obligations_no_probe_can_reach_are_declined() {
3541 let source = r#"pub struct Tagged(usize);
3547impl Tagged {
3548 pub const fn new(len: usize, on_heap: bool) -> Self {
3549 Self(if on_heap { len << 1 } else { len })
3550 }
3551 pub fn plain(len: usize) -> Self {
3552 Self(if len > 0 { len } else { 0 })
3553 }
3554}
3555"#;
3556 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
3557 let declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
3558 assert!(!declined.is_empty(), "the const fn was not declined");
3559
3560 let line_of = |id: &str| {
3562 manifest
3563 .points
3564 .iter()
3565 .find(|point| point.id == id)
3566 .map(|point| point.line)
3567 .or_else(|| {
3568 manifest
3569 .decisions
3570 .iter()
3571 .find(|decision| decision.id == id)
3572 .map(|decision| decision.line)
3573 })
3574 .or_else(|| {
3575 manifest
3576 .branches
3577 .iter()
3578 .find(|branch| branch.id == id)
3579 .map(|branch| branch.line)
3580 })
3581 };
3582 for id in &declined {
3583 let line = line_of(id).unwrap_or_else(|| panic!("no obligation {id}"));
3584 assert!(
3585 (3..=5).contains(&line),
3586 "declined an obligation outside the const fn, at line {line}"
3587 );
3588 }
3589 let measured = manifest
3590 .points
3591 .iter()
3592 .map(|point| (point.id.clone(), point.line))
3593 .filter(|(id, _)| !declined.contains(id))
3594 .collect::<Vec<_>>();
3595 assert!(
3596 measured.iter().any(|(_, line)| (6..=8).contains(line)),
3597 "the plain fn must stay measured: {measured:?}"
3598 );
3599 }
3600
3601 #[test]
3602 fn a_documented_function_is_reported_where_it_starts() {
3603 let source = r#"/// Documented.
3606/// Twice.
3607#[inline]
3608pub fn documented(value: i32) -> i32 {
3609 value + 1
3610}
3611"#;
3612 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
3613 let point = manifest
3614 .points
3615 .iter()
3616 .find(|point| {
3617 point.kind == PointKind::Function && point.label.as_deref() == Some("documented")
3618 })
3619 .unwrap();
3620 assert_eq!(point.line, 4, "{point:?}");
3621 assert!(
3622 point.source.starts_with("pub fn documented"),
3623 "{}",
3624 point.source
3625 );
3626 }
3627
3628 #[test]
3629 fn rejects_non_crate_local_runtime_paths() {
3630 assert_eq!(
3631 instrument_rust_source("src/lib.rs", "fn okay() {}", "supercov::runtime"),
3632 Err(RustInstrumenterError::InvalidRuntimePath)
3633 );
3634 }
3635
3636 #[test]
3637 fn rejects_invalid_rust_without_partial_obligations() {
3638 assert!(matches!(
3639 build_rust_manifest("src/lib.rs", "fn broken( {\n"),
3640 Err(RustInstrumenterError::Parse(_))
3641 ));
3642 }
3643}