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()
132 .any(|ancestor| in_global_allocator_impl(&ancestor))
133}
134
135fn in_global_allocator_impl(node: &ra_ap_syntax::SyntaxNode) -> bool {
137 ast::Impl::cast(node.clone()).is_some_and(|block| {
138 block.trait_().is_some_and(|implemented| {
139 implemented
140 .syntax()
141 .descendants_with_tokens()
142 .filter_map(|element| element.into_token())
143 .any(|token| token.kind() == SyntaxKind::IDENT && token.text() == "GlobalAlloc")
144 })
145 })
146}
147
148fn cannot_carry_probe(node: &ra_ap_syntax::SyntaxNode) -> bool {
150 in_const_context(node) || in_global_allocator(node)
151}
152
153fn range_offsets(range: TextRange) -> (usize, usize) {
154 (usize::from(range.start()), usize::from(range.end()))
155}
156
157fn push_wrapper(
158 insertions: &mut Vec<Insertion>,
159 range: TextRange,
160 scope: TextRange,
161 rank: usize,
162 prefix: String,
163 suffix: String,
164) {
165 let (start, end) = range_offsets(range);
166 let (scope_start, scope_end) = range_offsets(scope);
167 let scope_len = scope_end - scope_start;
168 insertions.push(Insertion {
169 offset: start,
170 kind: InsertionKind::Start,
171 scope_len,
172 rank,
173 text: prefix,
174 });
175 insertions.push(Insertion {
176 offset: end,
177 kind: InsertionKind::End,
178 scope_len,
179 rank,
180 text: suffix,
181 });
182}
183
184fn push_direct(insertions: &mut Vec<Insertion>, offset: usize, text: String) {
185 insertions.push(Insertion {
186 offset,
187 kind: InsertionKind::Direct,
188 scope_len: 0,
189 rank: 0,
190 text,
191 });
192}
193
194fn apply_insertions(
195 source: &str,
196 mut insertions: Vec<Insertion>,
197) -> Result<String, RustInstrumenterError> {
198 if insertions
199 .iter()
200 .any(|edit| edit.offset > source.len() || !source.is_char_boundary(edit.offset))
201 {
202 return Err(RustInstrumenterError::InvalidRange);
203 }
204 insertions.sort_by(|left, right| {
205 left.offset.cmp(&right.offset).then_with(|| {
206 let kind_order = |kind: InsertionKind| match kind {
207 InsertionKind::End => 0,
208 InsertionKind::Direct => 1,
209 InsertionKind::Start => 2,
210 };
211 kind_order(left.kind)
212 .cmp(&kind_order(right.kind))
213 .then_with(|| match left.kind {
214 InsertionKind::End => left
215 .scope_len
216 .cmp(&right.scope_len)
217 .then_with(|| right.rank.cmp(&left.rank)),
218 InsertionKind::Direct => std::cmp::Ordering::Equal,
219 InsertionKind::Start => right
220 .scope_len
221 .cmp(&left.scope_len)
222 .then_with(|| left.rank.cmp(&right.rank)),
223 })
224 })
225 });
226
227 let mut output = source.to_owned();
228 let mut index = insertions.len();
229 while index > 0 {
230 let offset = insertions[index - 1].offset;
231 let start = insertions[..index].partition_point(|insertion| insertion.offset < offset);
232 let text = insertions[start..index]
233 .iter()
234 .map(|insertion| insertion.text.as_str())
235 .collect::<String>();
236 output.insert_str(offset, &text);
237 index = start;
238 }
239 Ok(output)
240}
241
242fn limitation_id(kind: &str, file: &str) -> String {
246 format!("{kind}#{file}")
247}
248
249fn add_manifest_limitation(manifest: &mut CoverageManifest, file: &str, id: &str, reason: &str) {
250 let id = limitation_id(id, file);
251 if manifest.limitations.iter().any(|limitation| {
252 limitation.get("id").and_then(|value| value.as_str()) == Some(id.as_str())
253 }) {
254 return;
255 }
256 manifest.limitations.push(json!({
257 "id": id,
258 "kind": "source-scope",
259 "file": file,
260 "line": 1,
261 "column": 0,
262 "source": "",
263 "reason": reason,
264 "blocking": false
267 }));
268}
269
270fn allocate_frame_name(
271 file: &str,
272 condition: &ast::Expr,
273 kind: &str,
274 identifiers: &mut BTreeSet<String>,
275) -> String {
276 let id = stable_id(file, "decision", condition.syntax().text_range(), kind);
277 let suffix = id.rsplit(':').next().unwrap_or("decision");
278 let base = format!("__supercov_decision_{suffix}");
279 let mut candidate = base.clone();
280 let mut attempt = 0_usize;
281 while !identifiers.insert(candidate.clone()) {
282 attempt += 1;
283 candidate = format!("{base}_{attempt}");
284 }
285 candidate
286}
287
288fn allocate_table_name(
291 file: &str,
292 expression: &ast::MatchExpr,
293 identifiers: &mut BTreeSet<String>,
294) -> String {
295 let id = stable_id(file, "match", expression.syntax().text_range(), "arms");
296 let suffix = id
297 .rsplit(':')
298 .next()
299 .unwrap_or("match")
300 .to_ascii_uppercase();
301 let base = format!("__SUPERCOV_ARMS_{suffix}");
302 let mut candidate = base.clone();
303 let mut attempt = 0_usize;
304 while !identifiers.insert(candidate.clone()) {
305 attempt += 1;
306 candidate = format!("{base}_{attempt}");
307 }
308 candidate
309}
310
311fn allocate_flag_name(
313 file: &str,
314 expression: &ast::WhileExpr,
315 identifiers: &mut BTreeSet<String>,
316) -> String {
317 let id = stable_id(file, "loop", expression.syntax().text_range(), "flag");
318 let suffix = id.rsplit(':').next().unwrap_or("loop");
319 let base = format!("__supercov_loop_{suffix}");
320 let mut candidate = base.clone();
321 let mut attempt = 0_usize;
322 while !identifiers.insert(candidate.clone()) {
323 attempt += 1;
324 candidate = format!("{base}_{attempt}");
325 }
326 candidate
327}
328
329impl std::error::Error for RustInstrumenterError {}
330
331struct SourceLocations<'a> {
332 source: &'a str,
333 line_starts: Vec<usize>,
334}
335
336impl<'a> SourceLocations<'a> {
337 fn new(source: &'a str) -> Self {
338 let mut line_starts = vec![0];
339 line_starts.extend(
340 source
341 .bytes()
342 .enumerate()
343 .filter_map(|(index, byte)| (byte == b'\n').then_some(index + 1)),
344 );
345 Self {
346 source,
347 line_starts,
348 }
349 }
350
351 fn range(&self, range: TextRange) -> Result<(usize, usize), RustInstrumenterError> {
352 let start = usize::from(range.start());
353 let end = usize::from(range.end());
354 if start > end
355 || end > self.source.len()
356 || !self.source.is_char_boundary(start)
357 || !self.source.is_char_boundary(end)
358 {
359 return Err(RustInstrumenterError::InvalidRange);
360 }
361 Ok((start, end))
362 }
363
364 fn line_column(&self, offset: usize) -> (usize, usize) {
365 let line_index = self.line_starts.partition_point(|start| *start <= offset) - 1;
366 (line_index + 1, offset - self.line_starts[line_index])
367 }
368
369 fn text(&self, range: TextRange) -> Result<String, RustInstrumenterError> {
370 let (start, end) = self.range(range)?;
371 Ok(self.source[start..end].trim().to_owned())
372 }
373}
374
375fn stable_id(file: &str, kind: &str, range: TextRange, suffix: &str) -> String {
376 let mut hash = Sha256::new();
377 let start = usize::from(range.start()).to_string();
378 let end = usize::from(range.end()).to_string();
379 for value in [file, kind, &start, &end, suffix] {
380 hash.update(value.as_bytes());
381 hash.update([0]);
382 }
383 let digest = hash.finalize();
384 let mut encoded = String::with_capacity(24);
385 for byte in &digest[..12] {
386 use std::fmt::Write as _;
387 write!(&mut encoded, "{byte:02x}").expect("writing to a string cannot fail");
388 }
389 format!("rs:{kind}:{encoded}")
390}
391
392struct RustObligationCollector<'a> {
393 file: &'a str,
394 locations: SourceLocations<'a>,
395 manifest: CoverageManifest,
396 point_ids: BTreeSet<String>,
397 decision_ids: BTreeSet<String>,
398 branch_ids: BTreeSet<String>,
399 site_limitations: BTreeSet<&'static str>,
401 obligation_ranges: Vec<(String, TextRange)>,
404 tests: Vec<TextRange>,
406 compiled_out: Vec<TextRange>,
408 error: Option<RustInstrumenterError>,
409}
410
411impl<'a> RustObligationCollector<'a> {
412 fn new(file: &'a str, source: &'a str) -> Self {
413 Self {
414 file,
415 locations: SourceLocations::new(source),
416 manifest: CoverageManifest {
417 unmeasured: Vec::new(),
418 decisions: Vec::new(),
419 points: Vec::new(),
420 branches: Vec::new(),
421 limitations: Vec::new(),
422 scope: None,
423 assertion_sites: Vec::new(),
424 },
425 point_ids: BTreeSet::new(),
426 decision_ids: BTreeSet::new(),
427 branch_ids: BTreeSet::new(),
428 site_limitations: BTreeSet::new(),
429 obligation_ranges: Vec::new(),
430 tests: Vec::new(),
431 compiled_out: Vec::new(),
432 error: None,
433 }
434 }
435
436 fn leaving_out(mut self, tests: Vec<TextRange>, compiled_out: Vec<TextRange>) -> Self {
437 self.tests = tests;
438 self.compiled_out = compiled_out;
439 self
440 }
441
442 fn leave_out_test_code(&mut self) {
446 let uncompiled = self
449 .obligation_ranges
450 .iter()
451 .filter(|(_, range)| in_test_code(&self.compiled_out, *range))
452 .map(|(id, _)| id.clone())
453 .collect::<BTreeSet<_>>();
454 let inside = self
455 .obligation_ranges
456 .iter()
457 .filter(|(_, range)| in_test_code(&self.tests, *range))
458 .map(|(id, _)| id.clone())
459 .chain(uncompiled.iter().cloned())
460 .collect::<BTreeSet<_>>();
461 if inside.is_empty() {
462 return;
463 }
464 for point in std::mem::take(&mut self.manifest.points) {
465 if !inside.contains(&point.id) {
466 self.manifest.points.push(point);
467 } else if point.kind == PointKind::Statement && !uncompiled.contains(&point.id) {
468 self.manifest.assertion_sites.push(point);
469 }
470 }
471 self.manifest
472 .decisions
473 .retain(|decision| !inside.contains(&decision.id));
474 self.manifest
475 .branches
476 .retain(|branch| !inside.contains(&branch.id));
477 self.manifest.unmeasured.retain(|id| !inside.contains(id));
478 }
479
480 fn location_source(&mut self, range: TextRange) -> Option<(usize, usize, String)> {
481 let result = self.locations.range(range).map(|(start, _)| {
482 let (line, column) = self.locations.line_column(start);
483 (line, column, self.locations.text(range))
484 });
485 match result {
486 Ok((line, column, Ok(source))) => Some((line, column, source)),
487 Ok((_, _, Err(error))) | Err(error) => {
488 self.error.get_or_insert(error);
489 None
490 }
491 }
492 }
493
494 fn point(&mut self, range: TextRange, kind: PointKind, label: Option<String>) {
495 self.point_located(range, range, kind, label);
496 }
497
498 fn point_located(
502 &mut self,
503 range: TextRange,
504 location: TextRange,
505 kind: PointKind,
506 label: Option<String>,
507 ) {
508 let kind_name = match kind {
509 PointKind::Statement => "statement",
510 PointKind::Function => "function",
511 };
512 let id = stable_id(self.file, kind_name, range, label.as_deref().unwrap_or(""));
513 self.obligation_ranges.push((id.clone(), range));
514 if !self.point_ids.insert(id.clone()) {
515 return;
516 }
517 let Some((line, column, source)) = self.location_source(location) else {
518 return;
519 };
520 self.manifest.points.push(PointMeta {
521 id,
522 kind,
523 file: self.file.into(),
524 line,
525 column,
526 source,
527 label,
528 });
529 }
530
531 fn atomic_condition_ranges(expression: &ast::Expr, ranges: &mut Vec<TextRange>) {
532 match expression {
533 ast::Expr::ParenExpr(paren) => {
534 if let Some(inner) = paren.expr() {
535 Self::atomic_condition_ranges(&inner, ranges);
536 } else {
537 ranges.push(expression.syntax().text_range());
538 }
539 }
540 ast::Expr::BinExpr(binary)
541 if matches!(
542 binary.op_kind(),
543 Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))
544 ) =>
545 {
546 if let Some(left) = binary.lhs() {
547 Self::atomic_condition_ranges(&left, ranges);
548 }
549 if let Some(right) = binary.rhs() {
550 Self::atomic_condition_ranges(&right, ranges);
551 }
552 }
553 _ => ranges.push(expression.syntax().text_range()),
554 }
555 }
556
557 fn decision(&mut self, test: &ast::Expr, kind: &str) {
558 let range = test.syntax().text_range();
559 let id = stable_id(self.file, "decision", range, kind);
560 self.obligation_ranges.push((id.clone(), range));
561 if !self.decision_ids.insert(id.clone()) {
562 return;
563 }
564 let Some((line, column, source)) = self.location_source(range) else {
565 return;
566 };
567 let mut condition_ranges = Vec::new();
568 Self::atomic_condition_ranges(test, &mut condition_ranges);
569 let mut conditions = Vec::with_capacity(condition_ranges.len());
570 for condition in condition_ranges {
571 match self.locations.text(condition) {
572 Ok(source) => conditions.push(source),
573 Err(error) => {
574 self.error.get_or_insert(error);
575 return;
576 }
577 }
578 }
579 self.manifest.decisions.push(DecisionMeta {
580 id: id.clone(),
581 file: self.file.into(),
582 line,
583 column,
584 source: source.clone(),
585 conditions,
586 kind: kind.into(),
587 });
588 self.branch_with_id(
589 format!("{id}:outcome"),
590 range,
591 kind,
592 source,
593 [("true", "true"), ("false", "false")],
594 );
595 }
596
597 fn branch<const N: usize>(
598 &mut self,
599 range: TextRange,
600 kind: &str,
601 alternatives: [(&str, &str); N],
602 ) {
603 let id = stable_id(self.file, "branch", range, kind);
604 let Some((_, _, source)) = self.location_source(range) else {
605 return;
606 };
607 self.branch_with_id(id, range, kind, source, alternatives);
608 }
609
610 fn branch_with_id<const N: usize>(
611 &mut self,
612 id: String,
613 range: TextRange,
614 kind: &str,
615 source: String,
616 alternatives: [(&str, &str); N],
617 ) {
618 self.obligation_ranges.push((id.clone(), range));
619 if !self.branch_ids.insert(id.clone()) {
620 return;
621 }
622 let Some((line, column, _)) = self.location_source(range) else {
623 return;
624 };
625 self.manifest.branches.push(BranchMeta {
626 id: id.clone(),
627 kind: kind.into(),
628 file: self.file.into(),
629 line,
630 column,
631 source,
632 alternatives: alternatives
633 .into_iter()
634 .map(|(suffix, label)| BranchAlternativeMeta {
635 id: format!("{id}:{suffix}"),
636 label: label.into(),
637 })
638 .collect(),
639 });
640 }
641
642 fn decline_unreachable_obligations(&mut self, root: &ra_ap_syntax::SyntaxNode) {
647 let mut declined = self
651 .manifest
652 .unmeasured
653 .iter()
654 .cloned()
655 .collect::<BTreeSet<_>>();
656 for (id, range) in &self.obligation_ranges {
657 let node = match root.covering_element(*range) {
658 ra_ap_syntax::NodeOrToken::Node(node) => Some(node),
659 ra_ap_syntax::NodeOrToken::Token(token) => token.parent(),
660 };
661 if node.is_some_and(|node| cannot_carry_probe(&node)) {
662 declined.insert(id.clone());
663 }
664 }
665 self.manifest.unmeasured = declined.into_iter().collect();
666 }
667
668 fn site_limitation(&mut self, id: &'static str, range: TextRange, reason: String) {
674 if self.site_limitations.contains(id)
675 || in_test_code(&self.tests, range)
676 || in_test_code(&self.compiled_out, range)
677 {
678 return;
679 }
680 let Some((line, column, source)) = self.location_source(range) else {
681 return;
682 };
683 self.site_limitations.insert(id);
684 let source = source.lines().next().unwrap_or("").trim();
687 let source = if source.chars().count() > 120 {
688 format!("{}...", source.chars().take(117).collect::<String>())
689 } else {
690 source.to_owned()
691 };
692 self.manifest.limitations.push(json!({
693 "id": limitation_id(id, self.file),
694 "kind": "source-scope",
695 "file": self.file,
696 "line": line,
697 "column": column,
698 "source": source,
699 "blocking": false,
703 "reason": reason
704 }));
705 }
706
707 fn collect(
708 mut self,
709 file: &SourceFile,
710 assertions: &[TextRange],
711 matches: &[TextRange],
712 ) -> Result<CoverageManifest, RustInstrumenterError> {
713 let root = file.syntax();
714
715 for list in root.descendants().filter_map(ast::StmtList::cast) {
716 for statement in list.statements() {
717 match statement {
718 ast::Stmt::ExprStmt(statement) => {
719 self.point(statement.syntax().text_range(), PointKind::Statement, None);
720 }
721 ast::Stmt::LetStmt(statement) => {
722 self.point(statement.syntax().text_range(), PointKind::Statement, None);
723 }
724 ast::Stmt::Item(_) => {}
725 }
726 }
727 if let Some(tail) = list.tail_expr() {
728 self.point(tail.syntax().text_range(), PointKind::Statement, None);
729 }
730 }
731
732 for function in root.descendants().filter_map(ast::Fn::cast) {
733 if function.body().is_none() {
734 continue;
735 }
736 if function.const_token().is_some() {
737 self.site_limitation(
738 "rust-const-context-not-instrumented",
739 function.syntax().text_range(),
740 "Runtime probes cannot execute in const fn or compile-time evaluation".into(),
741 );
742 continue;
743 }
744 let label = function.name().map(|name| name.text().to_string());
745 self.point_located(
746 function.syntax().text_range(),
747 item_range(function.syntax()),
748 PointKind::Function,
749 label,
750 );
751 }
752
753 for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
754 self.point(
755 closure.syntax().text_range(),
756 PointKind::Function,
757 Some("<closure>".into()),
758 );
759 }
760
761 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
762 if let Some(condition) = expression.condition() {
763 self.decision(&condition, "if");
764 }
765 }
766 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
767 if let Some(condition) = expression.condition() {
768 self.decision(&condition, "while");
769 }
770 self.branch(
771 expression.syntax().text_range(),
772 "while-loop",
773 [("zero", "zero iterations"), ("entered", "entered")],
774 );
775 }
776 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
777 if let Some(condition) = guard.condition() {
778 self.decision(&condition, "match-guard");
779 }
780 }
781 for arguments in assertions {
782 if let Some(condition) = assertion_condition(root, *arguments) {
783 self.decision(&condition, "assert");
784 }
785 }
786 for expression in standalone_matches(root, matches, assertions) {
787 self.decision(&expression, "matches");
788 }
789 for expression in value_decisions(root, assertions) {
790 self.decision(&expression, "value");
791 }
792
793 for binary in root.descendants().filter_map(ast::BinExpr::cast) {
794 let kind = match binary.op_kind() {
795 Some(BinaryOp::LogicOp(LogicOp::And)) => "logical-and",
796 Some(BinaryOp::LogicOp(LogicOp::Or)) => "logical-or",
797 _ => continue,
798 };
799 let range = binary.rhs().map_or_else(
800 || binary.syntax().text_range(),
801 |right| right.syntax().text_range(),
802 );
803 self.branch(
804 range,
805 kind,
806 [
807 ("short-circuit", "short-circuited"),
808 ("evaluated", "right operand evaluated"),
809 ],
810 );
811 }
812
813 for expression in root.descendants().filter_map(ast::ForExpr::cast) {
814 self.branch(
815 expression.syntax().text_range(),
816 "for-loop",
817 [("zero", "zero iterations"), ("entered", "entered")],
818 );
819 }
820 for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
821 let Some(list) = expression.match_arm_list() else {
822 continue;
823 };
824 let arms = list.arms().collect::<Vec<_>>();
825 let last = arms.len().saturating_sub(1);
826 for (index, arm) in arms.iter().enumerate() {
827 let range = arm.syntax().text_range();
828 if index == last {
829 self.branch(range, "match-arm", [("selected", "selected")]);
833 } else {
834 self.branch(
835 range,
836 "match-arm",
837 [("missed", "not selected"), ("selected", "selected")],
838 );
839 }
840 }
841 }
842 for expression in root.descendants().filter_map(ast::TryExpr::cast) {
843 self.branch(
844 expression.syntax().text_range(),
845 "try-operator",
846 [("continued", "continued"), ("returned", "early return")],
847 );
848 }
849
850 let mut macro_names = BTreeSet::new();
856 let mut first_macro = None;
857 let mut macro_sites = 0;
858 for call in root.descendants().filter_map(ast::MacroCall::cast) {
859 macro_names.insert(
860 call.path()
861 .map(|path| path.syntax().text().to_string())
862 .unwrap_or_else(|| "?".into()),
863 );
864 first_macro.get_or_insert_with(|| call.syntax().text_range());
865 macro_sites += 1;
866 }
867 if let Some(range) = first_macro {
868 let named = macro_names
869 .iter()
870 .take(4)
871 .map(|name| format!("`{name}!`"))
872 .collect::<Vec<_>>()
873 .join(", ");
874 let rest = macro_names.len().saturating_sub(4);
875 self.site_limitation(
876 "rust-macro-expansion-not-instrumented",
877 range,
878 format!(
879 "{named}{} expand in the compiler: their arguments and expansions are outside the owned source denominator ({macro_sites} call site{} in this file)",
880 if rest == 0 {
881 String::new()
882 } else {
883 format!(" and {rest} other macro{}", if rest == 1 { "" } else { "s" })
884 },
885 if macro_sites == 1 { "" } else { "s" }
886 ),
887 );
888 }
889
890 let bears_obligation = |node: &ra_ap_syntax::SyntaxNode| {
895 ast::StmtList::cast(node.clone()).is_some_and(|list| {
896 list.statements().next().is_some() || list.tail_expr().is_some()
897 }) || ast::IfExpr::can_cast(node.kind())
898 || ast::WhileExpr::can_cast(node.kind())
899 || ast::MatchGuard::can_cast(node.kind())
900 || ast::ForExpr::can_cast(node.kind())
901 || ast::MatchArm::can_cast(node.kind())
902 || ast::TryExpr::can_cast(node.kind())
903 || ast::ClosureExpr::can_cast(node.kind())
904 || ast::BinExpr::cast(node.clone()).is_some_and(|binary| {
905 matches!(
906 binary.op_kind(),
907 Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))
908 )
909 })
910 };
911 if let Some(node) = root
912 .descendants()
913 .find(|node| bears_obligation(node) && in_const_context(node))
914 {
915 self.site_limitation(
916 "rust-const-context-not-instrumented",
917 node.text_range(),
918 "Runtime probes cannot execute in const fn or compile-time evaluation; this and any later const context in the file stay declared".into(),
919 );
920 }
921 if let Some(node) = root
922 .descendants()
923 .find(|node| bears_obligation(node) && in_global_allocator(node))
924 {
925 self.site_limitation(
926 "rust-global-allocator-not-instrumented",
927 node.text_range(),
928 "Probing a GlobalAlloc implementation recurses into itself, because the runtime allocates".into(),
929 );
930 }
931
932 self.decline_unreachable_obligations(root);
933 self.leave_out_test_code();
934
935 if let Some(error) = self.error {
936 return Err(error);
937 }
938 self.manifest
939 .decisions
940 .sort_by(|left, right| left.id.cmp(&right.id));
941 self.manifest
942 .points
943 .sort_by(|left, right| left.id.cmp(&right.id));
944 self.manifest
945 .branches
946 .sort_by(|left, right| left.id.cmp(&right.id));
947 self.manifest.limitations.sort_by(|left, right| {
948 left.get("id")
949 .and_then(|value| value.as_str())
950 .cmp(&right.get("id").and_then(|value| value.as_str()))
951 });
952 Ok(self.manifest)
953 }
954}
955
956pub const FAILED_TRANSFORM_DUMP_ENV: &str = "SUPERCOV_RUST_DUMP_FAILED_INSTRUMENTATION";
959
960const EXPRESSION_MACROS: &[&str] = &[
969 "assert",
970 "debug_assert",
971 "assert_eq",
972 "assert_ne",
973 "debug_assert_eq",
974 "debug_assert_ne",
975 "println",
976 "print",
977 "eprintln",
978 "eprint",
979 "format",
980 "format_args",
981 "write",
982 "writeln",
983 "panic",
984 "unreachable",
985 "todo",
986 "unimplemented",
987 "vec",
988 "dbg",
989 "matches",
990];
991
992const ASSERTION_MACROS: &[&str] = &["assert", "debug_assert"];
994
995const ASSERTION_STATEMENT_MACROS: &[&str] = &[
998 "assert",
999 "assert_eq",
1000 "assert_ne",
1001 "debug_assert",
1002 "debug_assert_eq",
1003 "debug_assert_ne",
1004];
1005
1006struct ExpressionView {
1010 text: String,
1011 assertions: Vec<TextRange>,
1012 assertion_calls: Vec<TextRange>,
1015 matches: Vec<TextRange>,
1017}
1018
1019const EDITIONS: [Edition; 4] = [
1025 Edition::Edition2024,
1026 Edition::Edition2021,
1027 Edition::Edition2018,
1028 Edition::Edition2015,
1029];
1030
1031fn parse_any_edition(source: &str) -> Result<(SourceFile, Edition), Vec<String>> {
1032 let mut newest_errors = None;
1033 for edition in EDITIONS {
1034 let parsed = SourceFile::parse(source, edition);
1035 let errors = parsed.errors();
1036 if errors.is_empty() {
1037 return Ok((parsed.tree(), edition));
1038 }
1039 newest_errors.get_or_insert_with(|| {
1040 errors
1041 .into_iter()
1042 .map(|error| error.to_string())
1043 .collect::<Vec<_>>()
1044 });
1045 }
1046 Err(newest_errors.unwrap_or_default())
1047}
1048
1049fn expression_view(source: &str, edition: Edition) -> ExpressionView {
1050 let mut text = source.to_owned();
1051 let mut assertions = Vec::new();
1052 let mut assertion_calls = Vec::new();
1053 let mut matches = Vec::new();
1054 for _ in 0..16 {
1057 let tree = SourceFile::parse(&text, edition).tree();
1058 let Some(next) = rewrite_expression_macros(
1059 &text,
1060 &tree,
1061 edition,
1062 &mut assertions,
1063 &mut assertion_calls,
1064 &mut matches,
1065 ) else {
1066 break;
1067 };
1068 text = next;
1069 }
1070 ExpressionView {
1071 text,
1072 assertions,
1073 assertion_calls,
1074 matches,
1075 }
1076}
1077
1078fn is_statement_macro(call: &ast::MacroCall) -> bool {
1081 call.syntax().parent().is_some_and(|parent| {
1082 ast::MacroExpr::can_cast(parent.kind())
1083 && parent.parent().is_some_and(|grandparent| {
1084 ast::ExprStmt::can_cast(grandparent.kind())
1085 || ast::StmtList::can_cast(grandparent.kind())
1086 })
1087 })
1088}
1089
1090fn first_top_level_comma_end(arguments: &ast::TokenTree) -> Option<usize> {
1092 arguments
1093 .syntax()
1094 .children_with_tokens()
1095 .filter_map(|element| element.into_token())
1096 .find(|token| token.kind() == SyntaxKind::COMMA)
1097 .map(|token| usize::from(token.text_range().end()))
1098}
1099
1100fn rewrite_expression_macros(
1103 source: &str,
1104 tree: &SourceFile,
1105 edition: Edition,
1106 assertions: &mut Vec<TextRange>,
1107 assertion_calls: &mut Vec<TextRange>,
1108 matches: &mut Vec<TextRange>,
1109) -> Option<String> {
1110 let mut text = source.as_bytes().to_vec();
1111 let mut changed = false;
1112 for call in tree.syntax().descendants().filter_map(ast::MacroCall::cast) {
1113 let Some(name) = call
1114 .path()
1115 .and_then(|path| path.segment())
1116 .and_then(|segment| segment.name_ref())
1117 .map(|name| name.text().to_string())
1118 else {
1119 continue;
1120 };
1121 if !EXPRESSION_MACROS.contains(&name.as_str()) {
1122 continue;
1123 }
1124 let (Some(bang), Some(arguments)) = (call.excl_token(), call.token_tree()) else {
1125 continue;
1126 };
1127 let parenthesised = arguments.l_paren_token().is_some();
1128 if !parenthesised && arguments.l_brack_token().is_none() {
1129 continue;
1130 }
1131 let range = arguments.syntax().text_range();
1132 let (start, end) = (usize::from(range.start()), usize::from(range.end()));
1133 let mut rewritten = source.as_bytes()[start..end].to_vec();
1134 if !parenthesised {
1135 rewritten[0] = b'(';
1136 *rewritten
1137 .last_mut()
1138 .expect("a token tree has a closing delimiter") = b')';
1139 }
1140 if name == "matches" {
1141 let Some(comma_end) = first_top_level_comma_end(&arguments) else {
1144 continue;
1145 };
1146 for byte in &mut rewritten[comma_end - start..end - start - 1] {
1147 if *byte != b'\n' {
1148 *byte = b' ';
1149 }
1150 }
1151 }
1152 let probe = format!(
1154 "fn __supercov() {{ let _ = __f{}; }}",
1155 String::from_utf8_lossy(&rewritten)
1156 );
1157 if !SourceFile::parse(&probe, edition).errors().is_empty() {
1158 if !parenthesised && !is_statement_macro(&call) {
1162 let array = source.as_bytes()[start..end].to_vec();
1163 let array_probe = format!(
1164 "fn __supercov() {{ let _ = {}; }}",
1165 String::from_utf8_lossy(&array)
1166 );
1167 if SourceFile::parse(&array_probe, edition).errors().is_empty() {
1168 let prefix_start = usize::from(call.syntax().text_range().start());
1169 let prefix_start = call.attrs().last().map_or(prefix_start, |attribute| {
1170 usize::from(attribute.syntax().text_range().end())
1171 });
1172 for byte in &mut text[prefix_start..start] {
1173 if *byte != b'\n' {
1174 *byte = b' ';
1175 }
1176 }
1177 text[prefix_start] = b'&';
1182 changed = true;
1183 }
1184 }
1185 continue;
1186 }
1187 text[usize::from(bang.text_range().start())] = b'_';
1188 text[start..end].copy_from_slice(&rewritten);
1189 changed = true;
1190 if ASSERTION_MACROS.contains(&name.as_str()) {
1191 assertions.push(range);
1192 }
1193 if ASSERTION_STATEMENT_MACROS.contains(&name.as_str()) {
1194 assertion_calls.push(call.syntax().text_range());
1195 }
1196 if name == "matches" {
1197 matches.push(call.syntax().text_range());
1198 }
1199 }
1200 changed.then(|| String::from_utf8(text).expect("rewriting ASCII keeps the source UTF-8"))
1201}
1202
1203fn assertion_argument_count(root: &ra_ap_syntax::SyntaxNode, arguments: TextRange) -> usize {
1206 root.descendants()
1207 .find(|node| node.text_range() == arguments && ast::ArgList::can_cast(node.kind()))
1208 .and_then(ast::ArgList::cast)
1209 .map_or(0, |list| list.args().count())
1210}
1211
1212fn assertion_condition(root: &ra_ap_syntax::SyntaxNode, arguments: TextRange) -> Option<ast::Expr> {
1215 root.descendants()
1216 .find(|node| node.text_range() == arguments && ast::ArgList::can_cast(node.kind()))
1217 .and_then(ast::ArgList::cast)?
1218 .args()
1219 .next()
1220}
1221
1222struct ParsedSource {
1227 tree: SourceFile,
1228 assertions: Vec<TextRange>,
1229 assertion_calls: Vec<TextRange>,
1230 matches: Vec<TextRange>,
1231 edition: Edition,
1232}
1233
1234fn parse_for_instrumentation(source: &str) -> Result<ParsedSource, RustInstrumenterError> {
1235 if source.len() > u32::MAX as usize {
1236 return Err(RustInstrumenterError::SourceTooLarge);
1237 }
1238 let (tree, edition) = parse_any_edition(source).map_err(RustInstrumenterError::Parse)?;
1239 let view = expression_view(source, edition);
1240 let parsed_view = SourceFile::parse(&view.text, edition);
1241 if parsed_view.errors().is_empty() {
1242 Ok(ParsedSource {
1243 tree: parsed_view.tree(),
1244 assertions: view.assertions,
1245 assertion_calls: view.assertion_calls,
1246 matches: view.matches,
1247 edition,
1248 })
1249 } else {
1250 Ok(ParsedSource {
1251 tree,
1252 assertions: Vec::new(),
1253 assertion_calls: Vec::new(),
1254 matches: Vec::new(),
1255 edition,
1256 })
1257 }
1258}
1259
1260fn condition_expressions(
1263 root: &ra_ap_syntax::SyntaxNode,
1264 assertions: &[TextRange],
1265) -> Vec<ast::Expr> {
1266 let mut conditions = Vec::new();
1267 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
1268 conditions.extend(expression.condition());
1269 }
1270 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
1271 conditions.extend(expression.condition());
1272 }
1273 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
1274 conditions.extend(guard.condition());
1275 }
1276 for arguments in assertions {
1277 conditions.extend(assertion_condition(root, *arguments));
1278 }
1279 conditions
1280}
1281
1282fn is_logical(expression: &ast::Expr) -> bool {
1283 matches!(expression, ast::Expr::BinExpr(binary)
1284 if matches!(binary.op_kind(), Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))))
1285}
1286
1287fn value_decisions(root: &ra_ap_syntax::SyntaxNode, assertions: &[TextRange]) -> Vec<ast::Expr> {
1293 let conditions = condition_expressions(root, assertions)
1294 .iter()
1295 .map(|condition| unparenthesized(condition).syntax().text_range())
1296 .collect::<Vec<_>>();
1297 root.descendants()
1298 .filter_map(ast::Expr::cast)
1299 .filter(is_logical)
1300 .filter(|expression| {
1301 let mut parent = expression.syntax().parent();
1302 while let Some(node) = parent
1303 .clone()
1304 .filter(|node| ast::ParenExpr::can_cast(node.kind()))
1305 {
1306 parent = node.parent();
1307 }
1308 !parent
1309 .and_then(ast::Expr::cast)
1310 .is_some_and(|parent| is_logical(&parent))
1311 })
1312 .filter(|expression| !conditions.contains(&expression.syntax().text_range()))
1313 .collect()
1314}
1315
1316fn unparenthesized(expression: &ast::Expr) -> ast::Expr {
1317 match expression {
1318 ast::Expr::ParenExpr(paren) => paren
1319 .expr()
1320 .map_or_else(|| expression.clone(), |inner| unparenthesized(&inner)),
1321 _ => expression.clone(),
1322 }
1323}
1324
1325fn standalone_matches(
1329 root: &ra_ap_syntax::SyntaxNode,
1330 matches: &[TextRange],
1331 assertions: &[TextRange],
1332) -> Vec<ast::Expr> {
1333 let mut atoms = Vec::new();
1334 let mut conditions = condition_expressions(root, assertions);
1335 conditions.extend(value_decisions(root, assertions));
1336 for condition in &conditions {
1337 RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
1338 }
1339 matches
1340 .iter()
1341 .filter(|range| !atoms.contains(range))
1342 .filter_map(|range| {
1343 root.descendants()
1344 .find(|node| node.text_range() == *range && ast::CallExpr::can_cast(node.kind()))
1345 .and_then(ast::Expr::cast)
1346 })
1347 .collect()
1348}
1349
1350#[derive(Debug, Clone, Default, PartialEq)]
1356pub struct TargetCfg {
1357 names: BTreeSet<String>,
1358 pairs: BTreeSet<(String, String)>,
1359}
1360
1361impl TargetCfg {
1362 pub fn new(values: &[cargo_platform::Cfg]) -> Self {
1363 let mut cfg = Self::default();
1364 for value in values {
1365 match value {
1366 cargo_platform::Cfg::Name(name) => {
1367 cfg.names.insert(name.name.clone());
1368 }
1369 cargo_platform::Cfg::KeyPair(key, value) => {
1370 cfg.pairs.insert((key.name.clone(), value.clone()));
1371 }
1372 }
1373 }
1374 cfg
1375 }
1376
1377 fn decides(key: &str) -> bool {
1379 matches!(key, "unix" | "windows" | "panic") || key.starts_with("target_")
1380 }
1381
1382 fn evaluate(&self, expression: &cargo_platform::CfgExpr) -> Option<bool> {
1383 use cargo_platform::{Cfg, CfgExpr};
1384 match expression {
1385 CfgExpr::True => Some(true),
1386 CfgExpr::False => Some(false),
1387 CfgExpr::Not(inner) => self.evaluate(inner).map(|value| !value),
1388 CfgExpr::All(items) => {
1389 let values = items
1390 .iter()
1391 .map(|item| self.evaluate(item))
1392 .collect::<Vec<_>>();
1393 if values.contains(&Some(false)) {
1394 Some(false)
1395 } else {
1396 values.iter().all(Option::is_some).then_some(true)
1397 }
1398 }
1399 CfgExpr::Any(items) => {
1400 let values = items
1401 .iter()
1402 .map(|item| self.evaluate(item))
1403 .collect::<Vec<_>>();
1404 if values.contains(&Some(true)) {
1405 Some(true)
1406 } else {
1407 values.iter().all(Option::is_some).then_some(false)
1408 }
1409 }
1410 CfgExpr::Value(Cfg::Name(name)) => {
1411 Self::decides(&name.name).then(|| self.names.contains(&name.name))
1412 }
1413 CfgExpr::Value(Cfg::KeyPair(key, value)) => Self::decides(&key.name)
1414 .then(|| self.pairs.contains(&(key.name.clone(), value.clone()))),
1415 }
1416 }
1417
1418 fn compiles_out(&self, node: &ra_ap_syntax::SyntaxNode) -> bool {
1420 item_attributes(node).iter().any(|text| {
1421 text.strip_prefix("cfg(")
1422 .and_then(|inner| inner.strip_suffix(')'))
1423 .and_then(|inner| inner.parse::<cargo_platform::CfgExpr>().ok())
1424 .is_some_and(|expression| self.evaluate(&expression) == Some(false))
1425 })
1426 }
1427}
1428
1429fn item_attributes(node: &ra_ap_syntax::SyntaxNode) -> Vec<String> {
1432 let attributes = if let Some(item) = ast::Item::cast(node.clone()) {
1433 item.attrs().collect::<Vec<_>>()
1434 } else if let Some(item) = ast::AssocItem::cast(node.clone()) {
1435 item.attrs().collect::<Vec<_>>()
1436 } else {
1437 return Vec::new();
1438 };
1439 attributes
1440 .iter()
1441 .filter_map(|attribute| attribute.meta())
1442 .map(|meta| {
1443 meta.syntax()
1444 .text()
1445 .to_string()
1446 .chars()
1447 .filter(|character| !character.is_whitespace())
1448 .collect()
1449 })
1450 .collect()
1451}
1452
1453pub fn test_code_path(file: &str) -> bool {
1457 let mut parts = file.split(['/', '\\']).collect::<Vec<_>>();
1458 let name = parts.pop().unwrap_or_default();
1459 parts
1460 .iter()
1461 .any(|directory| matches!(*directory, "tests" | "examples" | "benches"))
1462 || name == "tests.rs"
1463 || name
1464 .strip_suffix("tests.rs")
1465 .and_then(|stem| stem.strip_suffix(['_', '-']))
1466 .is_some_and(|stem| {
1467 !stem.is_empty()
1468 && stem.chars().all(|character| {
1469 character.is_ascii_alphanumeric() || "_-".contains(character)
1470 })
1471 })
1472}
1473
1474pub(crate) fn test_item(node: &ra_ap_syntax::SyntaxNode) -> bool {
1477 item_attributes(node).iter().any(|text| {
1478 let path = text
1479 .chars()
1480 .take_while(|character| character.is_alphanumeric() || "_:".contains(*character))
1481 .collect::<String>();
1482 matches!(path.rsplit("::").next(), Some("test" | "bench"))
1483 || text == "cfg(test)"
1484 || text.starts_with("cfg(all(test,")
1485 || text.starts_with("cfg(all(test)")
1486 })
1487}
1488
1489fn compiled_out_ranges(root: &ra_ap_syntax::SyntaxNode, cfg: Option<&TargetCfg>) -> Vec<TextRange> {
1491 let Some(cfg) = cfg else {
1492 return Vec::new();
1493 };
1494 root.descendants()
1495 .filter(|node| cfg.compiles_out(node))
1496 .map(|node| node.text_range())
1497 .collect()
1498}
1499
1500fn test_code_ranges(root: &ra_ap_syntax::SyntaxNode, test_file: bool) -> Vec<TextRange> {
1502 if test_file {
1503 return vec![root.text_range()];
1504 }
1505 root.descendants()
1506 .filter(test_item)
1507 .map(|node| node.text_range())
1508 .collect()
1509}
1510
1511fn in_test_code(tests: &[TextRange], range: TextRange) -> bool {
1512 tests.iter().any(|test| test.contains_range(range))
1513}
1514
1515pub fn build_rust_manifest(
1516 file: &str,
1517 source: &str,
1518) -> Result<CoverageManifest, RustInstrumenterError> {
1519 let parsed = parse_for_instrumentation(source)?;
1520 let tests = test_code_ranges(parsed.tree.syntax(), test_code_path(file));
1521 RustObligationCollector::new(file, source)
1522 .leaving_out(tests, Vec::new())
1523 .collect(&parsed.tree, &parsed.assertions, &parsed.matches)
1524}
1525
1526fn before_its_line(root: &ra_ap_syntax::SyntaxNode, source: &str, offset: usize) -> usize {
1537 let line = source[..offset].rfind('\n').map_or(0, |at| at + 1);
1538 if line == 0 || !source[line..offset].chars().all(char::is_whitespace) {
1539 return offset;
1540 }
1541 let Ok(at) = u32::try_from(offset) else {
1542 return offset;
1543 };
1544 let mut before = root
1545 .token_at_offset(ra_ap_syntax::TextSize::from(at))
1546 .right_biased()
1547 .and_then(|token| token.prev_token());
1548 while let Some(token) = &before {
1549 if !token.kind().is_trivia() {
1550 return usize::from(token.text_range().end());
1551 }
1552 before = token.prev_token();
1553 }
1554 offset
1555}
1556
1557fn block_entry_offset(block: &ast::BlockExpr) -> Option<usize> {
1558 let list = block.stmt_list()?;
1559 list.attrs()
1560 .last()
1561 .map(|attribute| usize::from(attribute.syntax().text_range().end()))
1562 .or_else(|| {
1563 list.l_curly_token()
1564 .map(|token| usize::from(token.text_range().end()))
1565 })
1566}
1567
1568fn item_range(node: &ra_ap_syntax::SyntaxNode) -> TextRange {
1573 let range = node.text_range();
1574 node.children_with_tokens()
1575 .find(|element| match element {
1576 ra_ap_syntax::NodeOrToken::Token(token) => !token.kind().is_trivia(),
1577 ra_ap_syntax::NodeOrToken::Node(child) => !ast::Attr::can_cast(child.kind()),
1578 })
1579 .map_or(range, |element| {
1580 TextRange::new(element.text_range().start(), range.end())
1581 })
1582}
1583
1584fn range_after_attributes(node: &impl HasAttrs) -> TextRange {
1585 let range = node.syntax().text_range();
1586 node.attrs().last().map_or(range, |attribute| {
1587 TextRange::new(attribute.syntax().text_range().end(), range.end())
1588 })
1589}
1590
1591fn has_let(expression: &ast::Expr) -> bool {
1592 expression
1593 .syntax()
1594 .descendants()
1595 .any(|node| ast::LetExpr::can_cast(node.kind()))
1596}
1597
1598enum ChainHost<'a> {
1600 If(&'a ast::IfExpr),
1601 While(&'a ast::WhileExpr),
1602}
1603
1604fn allocate_chain_table_name(
1606 file: &str,
1607 condition: &ast::Expr,
1608 identifiers: &mut BTreeSet<String>,
1609) -> String {
1610 let id = stable_id(file, "chain", condition.syntax().text_range(), "operators");
1611 let suffix = id
1612 .rsplit(':')
1613 .next()
1614 .unwrap_or("chain")
1615 .to_ascii_uppercase();
1616 let base = format!("__SUPERCOV_CHAIN_{suffix}");
1617 let mut candidate = base.clone();
1618 let mut attempt = 0_usize;
1619 while !identifiers.insert(candidate.clone()) {
1620 attempt += 1;
1621 candidate = format!("{base}_{attempt}");
1622 }
1623 candidate
1624}
1625
1626fn allocate_identifier(
1628 file: &str,
1629 range: TextRange,
1630 kind: &str,
1631 identifiers: &mut BTreeSet<String>,
1632) -> String {
1633 let id = stable_id(file, kind, range, "");
1634 let suffix = id.rsplit(':').next().unwrap_or(kind);
1635 let base = format!("__supercov_{kind}_{suffix}");
1636 let mut candidate = base.clone();
1637 let mut attempt = 0_usize;
1638 while !identifiers.insert(candidate.clone()) {
1639 attempt += 1;
1640 candidate = format!("{base}_{attempt}");
1641 }
1642 candidate
1643}
1644
1645fn own_breaks(body: &ast::BlockExpr, label: Option<ast::Label>) -> Vec<TextRange> {
1649 let own_label = label
1650 .and_then(|label| label.lifetime())
1651 .map(|lifetime| lifetime.text().to_string());
1652 body.syntax()
1653 .descendants()
1654 .filter_map(ast::BreakExpr::cast)
1655 .filter(|expression| match expression.lifetime() {
1656 Some(lifetime) => own_label.as_deref() == Some(lifetime.text().to_string().as_str()),
1657 None => !expression
1658 .syntax()
1659 .ancestors()
1660 .skip(1)
1661 .take_while(|ancestor| ancestor != body.syntax())
1662 .any(|ancestor| {
1663 ast::LoopExpr::can_cast(ancestor.kind())
1664 || ast::WhileExpr::can_cast(ancestor.kind())
1665 || ast::ForExpr::can_cast(ancestor.kind())
1666 || ast::ClosureExpr::can_cast(ancestor.kind())
1667 }),
1668 })
1669 .map(|expression| expression.syntax().text_range())
1670 .collect()
1671}
1672
1673fn instrument_let_chain(
1689 insertions: &mut Vec<Insertion>,
1690 runtime_path: &str,
1691 file: &str,
1692 condition: &ast::Expr,
1693 host: ChainHost<'_>,
1694 identifiers: &mut BTreeSet<String>,
1695) {
1696 let (kind, host_range, body, label) = match &host {
1699 ChainHost::If(expression) => (
1700 "if",
1701 range_after_attributes(*expression),
1702 expression.then_branch(),
1703 None,
1704 ),
1705 ChainHost::While(expression) => (
1706 "while",
1707 range_after_attributes(*expression),
1708 expression.loop_body(),
1709 expression.label(),
1710 ),
1711 };
1712 let Some(body) = body else {
1713 return;
1714 };
1715 let Some(body_offset) = block_entry_offset(&body) else {
1716 return;
1717 };
1718 let range = condition.syntax().text_range();
1719 let id = stable_id(file, "decision", range, kind);
1720 let mut atoms = Vec::new();
1721 RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
1722 let lets = condition
1723 .syntax()
1724 .descendants()
1725 .filter_map(ast::LetExpr::cast)
1726 .map(|expression| expression.syntax().text_range())
1727 .collect::<Vec<_>>();
1728 let frame = allocate_frame_name(file, condition, kind, identifiers);
1729 let table = allocate_chain_table_name(file, condition, identifiers);
1730 let single_let = atoms.len() == 1;
1731 let broke = match &host {
1732 ChainHost::While(_) if single_let => {
1733 Some(allocate_identifier(file, range, "broke", identifiers))
1734 }
1735 _ => None,
1736 };
1737
1738 let mut operators = Vec::new();
1739 for binary in condition
1740 .syntax()
1741 .descendants()
1742 .filter_map(ast::BinExpr::cast)
1743 {
1744 if !matches!(binary.op_kind(), Some(BinaryOp::LogicOp(LogicOp::And))) {
1747 continue;
1748 }
1749 let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
1750 continue;
1751 };
1752 if !has_let(&left) {
1753 continue;
1754 }
1755 let branch = stable_id(file, "branch", right.syntax().text_range(), "logical-and");
1756 let right_range = right.syntax().text_range();
1757 let Some(first) = atoms
1758 .iter()
1759 .position(|atom| right_range.contains_range(*atom))
1760 else {
1761 continue;
1762 };
1763 operators.push(format!(
1764 "({first}, {:?}, {:?})",
1765 format!("{branch}:short-circuit"),
1766 format!("{branch}:evaluated")
1767 ));
1768 }
1769
1770 let mark = format!("{runtime_path}::reached(&mut {frame}, 0);");
1771 let record_false = format!("{runtime_path}::decision_chain(&mut {frame}, false, {table});");
1772 let mut prefix = format!(
1773 "{{ const {table}: &[(usize, &str, &str)] = &[{}]; let mut {frame} = {runtime_path}::DecisionFrame::new({id:?}, {}); ",
1774 operators.join(", "),
1775 atoms.len()
1776 );
1777 if single_let {
1778 prefix.push_str(&mark);
1779 prefix.push(' ');
1780 }
1781 if let Some(broke) = &broke {
1782 prefix.push_str(&format!("let mut {broke} = false; "));
1783 }
1784 let suffix = match &host {
1785 ChainHost::If(expression) => match expression.else_branch() {
1786 Some(_) => " }".to_owned(),
1787 None => format!(" else {{ {record_false} }} }}"),
1788 },
1789 ChainHost::While(_) => match &broke {
1790 Some(broke) => format!(" if !{broke} {{ {mark} {record_false} }} }}"),
1791 None => format!(" {record_false} }}"),
1792 },
1793 };
1794 push_wrapper(insertions, host_range, host_range, 1, prefix, suffix);
1795 if !single_let {
1796 push_direct(
1797 insertions,
1798 usize::from(range.start()),
1799 format!("{runtime_path}::reached(&mut {frame}, 0) && "),
1800 );
1801 }
1802 for (index, atom) in atoms.iter().enumerate() {
1803 if lets.contains(atom) {
1804 if index > 0 {
1805 push_direct(
1806 insertions,
1807 usize::from(atom.start()),
1808 format!("{runtime_path}::reached(&mut {frame}, {index}) && "),
1809 );
1810 }
1811 } else {
1812 push_wrapper(
1813 insertions,
1814 *atom,
1815 *atom,
1816 1,
1817 format!("{runtime_path}::condition(("),
1818 format!("), &mut {frame}, {index})"),
1819 );
1820 }
1821 }
1822 let mut entry = String::new();
1823 if broke.is_some() {
1824 entry.push_str(&format!(" {mark}"));
1825 }
1826 entry.push_str(&format!(
1827 " {runtime_path}::decision_chain(&mut {frame}, true, {table});"
1828 ));
1829 push_direct(insertions, body_offset, entry);
1830 if let Some(broke) = &broke {
1831 for break_range in own_breaks(&body, label) {
1832 push_wrapper(
1833 insertions,
1834 break_range,
1835 break_range,
1836 0,
1837 format!("{{ {broke} = true; "),
1838 " }".into(),
1839 );
1840 }
1841 }
1842 if let ChainHost::If(expression) = &host {
1843 match expression.else_branch() {
1844 Some(ast::ElseBranch::Block(block)) => {
1845 if let Some(offset) = block_entry_offset(&block) {
1846 push_direct(insertions, offset, format!(" {record_false}"));
1847 }
1848 }
1849 Some(ast::ElseBranch::IfExpr(nested)) => {
1850 let nested_range = nested.syntax().text_range();
1851 push_wrapper(
1852 insertions,
1853 nested_range,
1854 nested_range,
1855 0,
1856 format!("{{ {record_false} "),
1857 " }".into(),
1858 );
1859 }
1860 None => {}
1861 }
1862 }
1863}
1864
1865fn enclosing_block_entry(node: &ra_ap_syntax::SyntaxNode) -> Option<usize> {
1869 node.ancestors()
1870 .skip(1)
1871 .find_map(ast::BlockExpr::cast)
1872 .and_then(|block| block_entry_offset(&block))
1873}
1874
1875fn plain_block(block: &ast::BlockExpr) -> bool {
1879 block
1880 .syntax()
1881 .first_token()
1882 .is_some_and(|token| token.kind() == SyntaxKind::L_CURLY)
1883}
1884
1885fn instrument_decision(
1886 insertions: &mut Vec<Insertion>,
1887 runtime_path: &str,
1888 file: &str,
1889 condition: &ast::Expr,
1890 kind: &str,
1891 frame_name: &str,
1892) -> bool {
1893 if cannot_carry_probe(condition.syntax())
1894 || condition
1895 .syntax()
1896 .descendants()
1897 .any(|node| ast::LetExpr::can_cast(node.kind()))
1898 {
1899 return false;
1900 }
1901 let range = condition.syntax().text_range();
1902 let id = stable_id(file, "decision", range, kind);
1903 let mut condition_ranges = Vec::new();
1904 RustObligationCollector::atomic_condition_ranges(condition, &mut condition_ranges);
1905 push_wrapper(
1906 insertions,
1907 range,
1908 range,
1909 0,
1910 format!(
1911 "({{ let mut {frame_name} = {runtime_path}::DecisionFrame::new({id:?}, {}); {runtime_path}::decision((",
1912 condition_ranges.len()
1913 ),
1914 format!("), &mut {frame_name}) }})"),
1915 );
1916 for (index, atomic_range) in condition_ranges.into_iter().enumerate() {
1920 push_wrapper(
1921 insertions,
1922 atomic_range,
1923 atomic_range,
1924 1,
1925 format!("{runtime_path}::condition(("),
1926 format!("), &mut {frame_name}, {index})"),
1927 );
1928 }
1929 true
1930}
1931
1932pub fn instrument_rust_source(
1942 file: &str,
1943 source: &str,
1944 runtime_path: &str,
1945) -> Result<RustInstrumentedSource, RustInstrumenterError> {
1946 instrument_rust_file(file, source, runtime_path, test_code_path(file), None)
1947}
1948
1949pub fn instrument_rust_file(
1954 file: &str,
1955 source: &str,
1956 runtime_path: &str,
1957 test_file: bool,
1958 cfg: Option<&TargetCfg>,
1959) -> Result<RustInstrumentedSource, RustInstrumenterError> {
1960 if !valid_runtime_path(runtime_path) {
1961 return Err(RustInstrumenterError::InvalidRuntimePath);
1962 }
1963 let parsed = parse_for_instrumentation(source)?;
1969 let tests = test_code_ranges(parsed.tree.syntax(), test_file);
1970 let compiled_out = compiled_out_ranges(parsed.tree.syntax(), cfg);
1971 let mut manifest = RustObligationCollector::new(file, source)
1972 .leaving_out(tests.clone(), compiled_out.clone())
1973 .collect(&parsed.tree, &parsed.assertions, &parsed.matches)?;
1974 let ParsedSource {
1975 tree,
1976 assertions,
1977 assertion_calls,
1978 matches,
1979 edition,
1980 } = parsed;
1981 let root = tree.syntax();
1982 let mut insertions = Vec::new();
1983 let mut identifiers = root
1984 .descendants_with_tokens()
1985 .filter_map(|element| element.into_token())
1986 .filter(|token| token.kind() == SyntaxKind::IDENT)
1987 .map(|token| token.text().to_string())
1988 .collect::<BTreeSet<_>>();
1989
1990 let mut skipped_attributed_statement = false;
1991 let attributed_probe = |insertions: &mut Vec<Insertion>,
2002 skipped: &mut bool,
2003 expression: Option<ast::Expr>,
2004 has_attrs: bool,
2005 statement: bool,
2009 trailing: bool,
2010 range: TextRange,
2011 id: String| {
2012 if !has_attrs {
2013 push_direct(
2014 insertions,
2015 before_its_line(root, source, usize::from(range.start())),
2016 format!("{runtime_path}::hit({id:?});"),
2017 );
2018 return;
2019 }
2020 let Some(expression) = expression else {
2021 *skipped = true;
2022 return;
2023 };
2024 if let ast::Expr::BlockExpr(block) = &expression
2025 && let Some(offset) = block_entry_offset(block)
2026 {
2027 push_direct(insertions, offset, format!("{runtime_path}::hit({id:?});"));
2028 return;
2029 }
2030 if trailing && let ast::Expr::MacroExpr(_) = &expression {
2041 if expression.attrs().any(|attribute| {
2042 attribute.meta().is_some_and(|meta| {
2045 let text = meta.syntax().text().to_string();
2046 let name = text
2047 .chars()
2048 .take_while(|character| character.is_alphanumeric() || *character == '_')
2049 .collect::<String>();
2050 matches!(name.as_str(), "cfg" | "cfg_attr")
2051 })
2052 }) {
2053 *skipped = true;
2054 return;
2055 }
2056 let range = expression.syntax().text_range();
2057 push_wrapper(
2058 insertions,
2059 range,
2060 range,
2061 0,
2062 format!("{{ {runtime_path}::hit({id:?}); "),
2063 " }".into(),
2064 );
2065 return;
2066 }
2067 if statement && let ast::Expr::MacroExpr(_) = &expression {
2074 let start = expression.attrs().last().map_or_else(
2075 || expression.syntax().text_range().start(),
2076 |attribute| attribute.syntax().text_range().end(),
2077 );
2078 let wrapped = TextRange::new(start, expression.syntax().text_range().end());
2079 push_wrapper(
2080 insertions,
2081 wrapped,
2082 wrapped,
2083 0,
2084 format!(" {{ {runtime_path}::hit({id:?}); "),
2085 "; }".into(),
2086 );
2087 return;
2088 }
2089 let start = expression.attrs().last().map_or_else(
2095 || expression.syntax().text_range().start(),
2096 |attribute| attribute.syntax().text_range().end(),
2097 );
2098 let wrapped = TextRange::new(start, expression.syntax().text_range().end());
2099 push_wrapper(
2100 insertions,
2101 wrapped,
2102 wrapped,
2103 0,
2104 format!(" {{ {runtime_path}::hit({id:?}); ("),
2105 ") }".into(),
2106 );
2107 };
2108 for list in root.descendants().filter_map(ast::StmtList::cast) {
2109 let last_statement = list.statements().last();
2110 for statement in list.statements() {
2111 let (range, expression, has_attrs, statement_expression, trailing) = match &statement {
2112 ast::Stmt::ExprStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
2113 let expression = statement.expr();
2114 let has_attrs = expression
2117 .as_ref()
2118 .is_some_and(|expression| expression.attrs().next().is_some());
2119 let trailing = statement.semicolon_token().is_none()
2122 && list.tail_expr().is_none()
2123 && last_statement.as_ref() == Some(&ast::Stmt::ExprStmt(statement.clone()));
2124 (
2125 statement.syntax().text_range(),
2126 expression,
2127 has_attrs,
2128 true,
2129 trailing,
2130 )
2131 }
2132 ast::Stmt::LetStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
2133 let has_attrs = statement.attrs().next().is_some();
2134 let initializer = has_attrs.then(|| statement.initializer()).flatten();
2137 (
2138 statement.syntax().text_range(),
2139 initializer,
2140 has_attrs,
2141 false,
2142 false,
2143 )
2144 }
2145 _ => continue,
2146 };
2147 let id = stable_id(file, "statement", range, "");
2148 if !has_attrs
2155 && !trailing
2156 && assertion_calls
2157 .iter()
2158 .any(|call| range.contains_range(*call))
2159 {
2160 push_direct(
2161 &mut insertions,
2162 usize::from(range.end()),
2163 format!("{runtime_path}::assertion({id:?});"),
2164 );
2165 }
2166 attributed_probe(
2167 &mut insertions,
2168 &mut skipped_attributed_statement,
2169 expression,
2170 has_attrs,
2171 statement_expression,
2172 trailing,
2173 range,
2174 id,
2175 );
2176 }
2177 if let Some(tail) = list
2178 .tail_expr()
2179 .filter(|tail| !cannot_carry_probe(tail.syntax()))
2180 {
2181 let range = tail.syntax().text_range();
2182 let id = stable_id(file, "statement", range, "");
2183 let has_attrs = tail.attrs().next().is_some();
2184 attributed_probe(
2185 &mut insertions,
2186 &mut skipped_attributed_statement,
2187 Some(tail),
2188 has_attrs,
2189 true,
2190 true,
2191 range,
2192 id,
2193 );
2194 }
2195 }
2196
2197 for function in root.descendants().filter_map(ast::Fn::cast) {
2198 if cannot_carry_probe(function.syntax()) {
2201 continue;
2202 }
2203 let Some(body) = function.body() else {
2204 continue;
2205 };
2206 let label = function.name().map(|name| name.text().to_string());
2207 let id = stable_id(
2208 file,
2209 "function",
2210 function.syntax().text_range(),
2211 label.as_deref().unwrap_or(""),
2212 );
2213 if let Some(offset) = block_entry_offset(&body) {
2214 push_direct(
2215 &mut insertions,
2216 offset,
2217 format!("{runtime_path}::hit({id:?});"),
2218 );
2219 }
2220 }
2221
2222 for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
2223 let Some(body) = closure.body() else {
2224 continue;
2225 };
2226 if cannot_carry_probe(body.syntax()) {
2227 continue;
2228 }
2229 let id = stable_id(file, "function", closure.syntax().text_range(), "<closure>");
2230 if let ast::Expr::BlockExpr(block) = &body {
2231 if let Some(offset) = block_entry_offset(block) {
2232 push_direct(
2233 &mut insertions,
2234 offset,
2235 format!("{runtime_path}::hit({id:?});"),
2236 );
2237 }
2238 } else {
2239 let range = body.syntax().text_range();
2240 push_wrapper(
2241 &mut insertions,
2242 range,
2243 closure.syntax().text_range(),
2244 0,
2245 format!("{{ {runtime_path}::hit({id:?}); ("),
2246 ") }".into(),
2247 );
2248 }
2249 }
2250
2251 for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
2258 if cannot_carry_probe(expression.syntax()) {
2259 continue;
2260 }
2261 let Some(list) = expression.match_arm_list() else {
2262 continue;
2263 };
2264 let arms = list.arms().collect::<Vec<_>>();
2265 if arms.is_empty() {
2266 continue;
2267 }
2268 let Some(table_offset) = enclosing_block_entry(expression.syntax()) else {
2269 continue;
2270 };
2271 let table = allocate_table_name(file, &expression, &mut identifiers);
2272 let entries = arms
2273 .iter()
2274 .map(|arm| {
2275 let id = stable_id(file, "branch", arm.syntax().text_range(), "match-arm");
2276 format!(
2277 "{:?}, {:?}",
2278 format!("{id}:missed"),
2279 format!("{id}:selected")
2280 )
2281 })
2282 .collect::<Vec<_>>()
2283 .join(", ");
2284 push_direct(
2285 &mut insertions,
2286 table_offset,
2287 format!(" const {table}: &[&str] = &[{entries}];"),
2291 );
2292 for (index, arm) in arms.iter().enumerate() {
2293 let Some(body) = arm.expr() else {
2294 continue;
2295 };
2296 let call = format!("{runtime_path}::arms({table}, {index});");
2297 match &body {
2298 ast::Expr::BlockExpr(block) if plain_block(block) => {
2299 if let Some(offset) = block_entry_offset(block) {
2300 push_direct(&mut insertions, offset, format!(" {call}"));
2301 }
2302 }
2303 _ => push_wrapper(
2304 &mut insertions,
2305 body.syntax().text_range(),
2306 arm.syntax().text_range(),
2307 0,
2308 format!("{{ {call} ("),
2309 ") }".into(),
2310 ),
2311 }
2312 }
2313 }
2314
2315 for binary in root.descendants().filter_map(ast::BinExpr::cast) {
2319 let short_circuits_when = match binary.op_kind() {
2320 Some(BinaryOp::LogicOp(LogicOp::And)) => false,
2321 Some(BinaryOp::LogicOp(LogicOp::Or)) => true,
2322 _ => continue,
2323 };
2324 if cannot_carry_probe(binary.syntax()) {
2325 continue;
2326 }
2327 let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
2328 continue;
2329 };
2330 if has_let(&left) {
2334 continue;
2335 }
2336 let kind = if short_circuits_when {
2337 "logical-or"
2338 } else {
2339 "logical-and"
2340 };
2341 let id = stable_id(file, "branch", right.syntax().text_range(), kind);
2342 push_wrapper(
2343 &mut insertions,
2344 left.syntax().text_range(),
2345 binary.syntax().text_range(),
2346 2,
2347 format!("{runtime_path}::logical(("),
2348 format!(
2349 "), {short_circuits_when}, {:?}, {:?})",
2350 format!("{id}:short-circuit"),
2351 format!("{id}:evaluated")
2352 ),
2353 );
2354 }
2355
2356 for expression in root.descendants().filter_map(ast::ForExpr::cast) {
2360 if cannot_carry_probe(expression.syntax()) {
2361 continue;
2362 }
2363 let Some(iterable) = expression.iterable() else {
2364 continue;
2365 };
2366 let id = stable_id(file, "branch", expression.syntax().text_range(), "for-loop");
2367 push_wrapper(
2371 &mut insertions,
2372 iterable.syntax().text_range(),
2373 iterable.syntax().text_range(),
2374 0,
2375 format!("{runtime_path}::for_loop(("),
2376 format!(
2377 "), {:?}, {:?})",
2378 format!("{id}:zero"),
2379 format!("{id}:entered")
2380 ),
2381 );
2382 }
2383
2384 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
2388 if cannot_carry_probe(expression.syntax()) {
2389 continue;
2390 }
2391 let Some(offset) = expression.loop_body().as_ref().and_then(block_entry_offset) else {
2392 continue;
2393 };
2394 let id = stable_id(
2395 file,
2396 "branch",
2397 expression.syntax().text_range(),
2398 "while-loop",
2399 );
2400 let flag = allocate_flag_name(file, &expression, &mut identifiers);
2401 let range = range_after_attributes(&expression);
2402 push_wrapper(
2403 &mut insertions,
2404 range,
2405 range,
2406 0,
2407 format!("{{ let mut {flag} = true; "),
2408 format!(
2409 " {runtime_path}::zero_iterations({flag}, {:?}) }}",
2410 format!("{id}:zero")
2411 ),
2412 );
2413 push_direct(
2414 &mut insertions,
2415 offset,
2416 format!(
2417 " {runtime_path}::entered(&mut {flag}, {:?});",
2418 format!("{id}:entered")
2419 ),
2420 );
2421 }
2422
2423 for expression in root.descendants().filter_map(ast::TryExpr::cast) {
2427 if cannot_carry_probe(expression.syntax()) {
2428 continue;
2429 }
2430 let Some(operand) = expression.expr() else {
2431 continue;
2432 };
2433 let id = stable_id(
2434 file,
2435 "branch",
2436 expression.syntax().text_range(),
2437 "try-operator",
2438 );
2439 push_wrapper(
2442 &mut insertions,
2443 operand.syntax().text_range(),
2444 operand.syntax().text_range(),
2445 0,
2446 format!("{runtime_path}::TryProbe::probe(("),
2447 format!(
2448 "), {:?}, {:?})",
2449 format!("{id}:continued"),
2450 format!("{id}:returned")
2451 ),
2452 );
2453 }
2454
2455 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
2456 let Some(condition) = expression.condition() else {
2457 continue;
2458 };
2459 if has_let(&condition) {
2460 if !cannot_carry_probe(condition.syntax()) {
2461 instrument_let_chain(
2462 &mut insertions,
2463 runtime_path,
2464 file,
2465 &condition,
2466 ChainHost::If(&expression),
2467 &mut identifiers,
2468 );
2469 }
2470 continue;
2471 }
2472 let frame_name = allocate_frame_name(file, &condition, "if", &mut identifiers);
2473 instrument_decision(
2474 &mut insertions,
2475 runtime_path,
2476 file,
2477 &condition,
2478 "if",
2479 &frame_name,
2480 );
2481 }
2482 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
2483 let Some(condition) = expression.condition() else {
2484 continue;
2485 };
2486 if has_let(&condition) {
2487 if !cannot_carry_probe(condition.syntax()) {
2488 instrument_let_chain(
2489 &mut insertions,
2490 runtime_path,
2491 file,
2492 &condition,
2493 ChainHost::While(&expression),
2494 &mut identifiers,
2495 );
2496 }
2497 continue;
2498 }
2499 let frame_name = allocate_frame_name(file, &condition, "while", &mut identifiers);
2500 instrument_decision(
2501 &mut insertions,
2502 runtime_path,
2503 file,
2504 &condition,
2505 "while",
2506 &frame_name,
2507 );
2508 }
2509 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
2510 if let Some(condition) = guard.condition() {
2511 let frame_name = allocate_frame_name(file, &condition, "match-guard", &mut identifiers);
2512 instrument_decision(
2513 &mut insertions,
2514 runtime_path,
2515 file,
2516 &condition,
2517 "match-guard",
2518 &frame_name,
2519 );
2520 }
2521 }
2522 for arguments in &assertions {
2529 let Some(condition) = assertion_condition(root, *arguments) else {
2530 continue;
2531 };
2532 let frame_name = allocate_frame_name(file, &condition, "assert", &mut identifiers);
2533 if !instrument_decision(
2534 &mut insertions,
2535 runtime_path,
2536 file,
2537 &condition,
2538 "assert",
2539 &frame_name,
2540 ) {
2541 continue;
2542 }
2543 if assertion_argument_count(root, *arguments) == 1 {
2544 let range = condition.syntax().text_range();
2545 let original = &source[usize::from(range.start())..usize::from(range.end())];
2546 push_direct(
2547 &mut insertions,
2548 usize::from(range.end()),
2549 format!(", \"assertion failed: {{}}\", stringify!({original})"),
2550 );
2551 }
2552 }
2553 for expression in standalone_matches(root, &matches, &assertions) {
2556 let frame_name = allocate_frame_name(file, &expression, "matches", &mut identifiers);
2557 instrument_decision(
2558 &mut insertions,
2559 runtime_path,
2560 file,
2561 &expression,
2562 "matches",
2563 &frame_name,
2564 );
2565 }
2566 for expression in value_decisions(root, &assertions) {
2567 let frame_name = allocate_frame_name(file, &expression, "value", &mut identifiers);
2568 instrument_decision(
2569 &mut insertions,
2570 runtime_path,
2571 file,
2572 &expression,
2573 "value",
2574 &frame_name,
2575 );
2576 }
2577
2578 if !compiled_out.is_empty() {
2581 insertions.retain(|insertion| {
2582 !compiled_out.iter().any(|range| {
2583 usize::from(range.start()) <= insertion.offset
2584 && insertion.offset <= usize::from(range.end())
2585 })
2586 });
2587 }
2588 if !tests.is_empty() {
2591 let inside = |offset: usize| {
2592 tests.iter().any(|test| {
2593 usize::from(test.start()) <= offset && offset <= usize::from(test.end())
2594 })
2595 };
2596 let markers = manifest
2597 .assertion_sites
2598 .iter()
2599 .map(|site| {
2600 (
2601 format!("{runtime_path}::assertion({:?});", site.id),
2602 site.id.clone(),
2603 )
2604 })
2605 .collect::<std::collections::BTreeMap<_, _>>();
2606 let mut marked = BTreeSet::new();
2607 insertions.retain(|insertion| {
2608 if !inside(insertion.offset) {
2609 return true;
2610 }
2611 match markers.get(&insertion.text) {
2612 Some(id) => {
2613 marked.insert(id.clone());
2614 true
2615 }
2616 None => false,
2617 }
2618 });
2619 manifest
2620 .assertion_sites
2621 .retain(|site| marked.contains(&site.id));
2622 }
2623
2624 if skipped_attributed_statement && !test_file {
2625 add_manifest_limitation(
2626 &mut manifest,
2627 file,
2628 "rust-attributed-statement-probes-not-injected",
2629 "A `let` without an initializer that carries outer attributes has no expression to hold a probe",
2630 );
2631 }
2632 manifest.limitations.sort_by(|left, right| {
2633 left.get("id")
2634 .and_then(|value| value.as_str())
2635 .cmp(&right.get("id").and_then(|value| value.as_str()))
2636 });
2637
2638 let code = apply_insertions(source, insertions)?;
2639 let transformed = SourceFile::parse(&code, edition);
2640 let errors = transformed
2641 .errors()
2642 .into_iter()
2643 .map(|error| error.to_string())
2644 .collect::<Vec<_>>();
2645 if !errors.is_empty() {
2646 if let Some(directory) = std::env::var_os(FAILED_TRANSFORM_DUMP_ENV) {
2650 let name = file.replace(['/', '\\'], "__");
2651 let _ = std::fs::create_dir_all(&directory);
2652 let _ = std::fs::write(std::path::Path::new(&directory).join(name), &code);
2653 }
2654 return Err(RustInstrumenterError::Parse(errors));
2655 }
2656 Ok(RustInstrumentedSource { code, manifest })
2657}
2658
2659#[cfg(test)]
2660mod tests {
2661 use std::{
2662 fs,
2663 process::Command,
2664 time::{SystemTime, UNIX_EPOCH},
2665 };
2666
2667 use super::*;
2668
2669 fn limitation_kind_of(limitation: &serde_json::Value) -> Option<&str> {
2671 limitation.get("id")?.as_str()?.split('#').next()
2672 }
2673
2674 const NOOP_RUNTIME: &str = r#"
2675#[doc(hidden)]
2676mod __supercov_runtime_v1 {
2677 pub struct DecisionFrame;
2678 impl DecisionFrame {
2679 pub fn new(_: &'static str, _: usize) -> Self { Self }
2680 }
2681 pub fn hit(_: &'static str) {}
2682 pub fn arms(_: &[&'static str], _: usize) {}
2683 pub fn logical(left: bool, _: bool, _: &'static str, _: &'static str) -> bool { left }
2684 pub fn for_loop<I: IntoIterator>(iterable: I, _: &'static str, _: &'static str) -> I::IntoIter {
2685 iterable.into_iter()
2686 }
2687 pub fn entered(_: &mut bool, _: &'static str) {}
2688 pub fn zero_iterations(_: bool, _: &'static str) {}
2689 pub trait TryProbe: Sized {
2690 fn probe(self, _: &'static str, _: &'static str) -> Self { self }
2691 }
2692 impl<T> TryProbe for T {}
2693 pub fn condition<V: std::ops::Not<Output = bool>>(value: V, _: &mut DecisionFrame, _: usize) -> bool { !!value }
2694 pub fn decision(value: bool, _: &mut DecisionFrame) -> bool { value }
2695 pub fn reached(_: &mut DecisionFrame, _: usize) -> bool { true }
2696 pub fn decision_chain(_: &mut DecisionFrame, _: bool, _: &[(usize, &'static str, &'static str)]) {}
2697 pub fn assertion(_: &'static str) {}
2698}
2699"#;
2700
2701 fn compile_and_run(source: &str, name: &str) -> std::process::Output {
2702 compile_and_run_edition(source, name, "2024")
2703 }
2704
2705 fn compile_and_run_edition(source: &str, name: &str, edition: &str) -> std::process::Output {
2706 let nonce = SystemTime::now()
2707 .duration_since(UNIX_EPOCH)
2708 .unwrap()
2709 .as_nanos();
2710 let directory = std::env::temp_dir().join(format!(
2711 "supercov-rust-transform-{}-{nonce}-{name}",
2712 std::process::id()
2713 ));
2714 fs::create_dir(&directory).unwrap();
2715 let input = directory.join("main.rs");
2716 let binary = directory.join("program");
2717 fs::write(&input, source).unwrap();
2718 let compile = Command::new("rustc")
2719 .arg(format!("--edition={edition}"))
2720 .arg(&input)
2721 .arg("-o")
2722 .arg(&binary)
2723 .output()
2724 .unwrap();
2725 assert!(
2726 compile.status.success(),
2727 "rustc failed:\n{}\nsource:\n{source}",
2728 String::from_utf8_lossy(&compile.stderr)
2729 );
2730 let output = Command::new(&binary).output().unwrap();
2731 fs::remove_dir_all(directory).unwrap();
2732 output
2733 }
2734
2735 #[test]
2736 fn a_match_and_a_loop_take_no_line_of_their_own() {
2737 let source = r#"fn kind(key: &str) -> u8 {
2742 match key.len() {
2743 0 => 0,
2744 n if n < 4 => {
2745 1
2746 }
2747 _ => 2,
2748 }
2749}
2750
2751fn scan(key: &str, limit: usize) -> usize {
2752 let mut seen = 0;
2753 while seen < limit && seen < key.len() {
2754 if key.as_bytes()[seen] == b'-' || seen > 8 {
2755 break;
2756 } else {
2757 seen += 1;
2758 }
2759 }
2760 for _ in 0..limit {
2761 seen += 0;
2762 }
2763 seen
2764}
2765"#;
2766 let transformed =
2767 instrument_rust_source("src/scan.rs", source, "crate::__supercov_runtime_v1").unwrap();
2768 assert_eq!(
2769 transformed.code.lines().count(),
2770 source.lines().count(),
2771 "{}",
2772 transformed.code
2773 );
2774 for (written, rewritten) in source.lines().zip(transformed.code.lines()) {
2776 let end = written.trim().trim_end_matches(['{', ';', ',']).trim();
2777 let last = end.rsplit(' ').next().unwrap_or_default();
2778 assert!(rewritten.contains(last), "{written:?} became {rewritten:?}");
2779 }
2780 }
2781
2782 #[test]
2783 fn a_panic_is_reported_where_its_author_wrote_it() {
2784 let source = r#"fn check(key: &str) -> usize {
2790 if key.len() < 4 {
2791 panic!("key too short");
2792 }
2793 key.len()
2794}
2795
2796fn later(values: &[usize]) -> usize {
2797 let first = values[0];
2798 let total: usize = values.iter().sum();
2799 assert!(total > first, "nothing but the first");
2800 total
2801}
2802
2803fn main() {
2804 let mode = std::env::args().nth(1).unwrap_or_default();
2805 if mode == "later" {
2806 later(&[3]);
2807 }
2808 let closure = |key: &str| {
2809 check(key)
2810 };
2811 closure("ab");
2812}
2813"#;
2814 let transformed =
2815 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2816 assert_eq!(
2817 transformed.code.lines().count(),
2818 source.lines().count(),
2819 "{}",
2820 transformed.code
2821 );
2822 for (number, (written, rewritten)) in
2824 source.lines().zip(transformed.code.lines()).enumerate()
2825 {
2826 let code = written.trim_start();
2827 let word = code.split(['(', ' ']).next().unwrap_or(code);
2829 if matches!(word, "panic!" | "assert!" | "let") {
2830 assert_eq!(
2831 rewritten.find(word),
2832 written.find(word),
2833 "line {}: {rewritten}",
2834 number + 1
2835 );
2836 }
2837 }
2838 let location = |output: &std::process::Output| {
2839 String::from_utf8_lossy(&output.stderr)
2840 .lines()
2841 .find(|line| line.contains("panicked at"))
2842 .map(|line| line[line.find("main.rs").unwrap()..].to_owned())
2843 .expect("a panic")
2844 };
2845 let original = compile_and_run(source, "panic-original");
2846 let instrumented = compile_and_run(
2847 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2848 "panic-instrumented",
2849 );
2850 assert_eq!(location(&original), "main.rs:3:9:");
2851 assert_eq!(location(&instrumented), location(&original));
2852 }
2853
2854 #[test]
2855 fn a_target_cfg_decides_only_what_the_target_settles() {
2856 let cfg = TargetCfg::new(
2857 &[
2858 "unix",
2859 "target_os=\"linux\"",
2860 "target_family=\"unix\"",
2861 "target_pointer_width=\"64\"",
2862 "debug_assertions",
2863 ]
2864 .map(|value| value.parse::<cargo_platform::Cfg>().unwrap()),
2865 );
2866 let decide = |expression: &str| cfg.evaluate(&expression.parse().unwrap());
2867 assert_eq!(decide("unix"), Some(true));
2868 assert_eq!(decide("windows"), Some(false));
2869 assert_eq!(decide("target_os = \"macos\""), Some(false));
2870 assert_eq!(decide("not(target_os = \"linux\")"), Some(false));
2871 assert_eq!(
2872 decide("all(unix, target_pointer_width = \"64\")"),
2873 Some(true)
2874 );
2875 assert_eq!(decide("any(windows, target_os = \"wasi\")"), Some(false));
2876 assert_eq!(decide("feature = \"extra\""), None);
2878 assert_eq!(decide("not(feature = \"extra\")"), None);
2879 assert_eq!(decide("debug_assertions"), None);
2880 assert_eq!(decide("all(windows, feature = \"extra\")"), Some(false));
2881 assert_eq!(decide("any(unix, feature = \"extra\")"), Some(true));
2882 assert_eq!(decide("any(windows, feature = \"extra\")"), None);
2883
2884 let source = "pub fn a() -> u8 { b() }\n#[cfg(windows)]\nfn b() -> u8 { 1 }\n#[cfg(unix)]\nfn b() -> u8 { 2 }\n#[cfg(feature = \"x\")]\nfn c() -> u8 { 3 }\n";
2885 let instrumented = instrument_rust_file(
2886 "src/lib.rs",
2887 source,
2888 "crate::__supercov_runtime_v1",
2889 false,
2890 Some(&cfg),
2891 )
2892 .unwrap();
2893 let lines = instrumented
2894 .manifest
2895 .points
2896 .iter()
2897 .map(|point| point.line)
2898 .collect::<BTreeSet<_>>();
2899 assert!(
2900 !lines.contains(&3),
2901 "the windows item is left out: {lines:?}"
2902 );
2903 assert!(lines.contains(&5) && lines.contains(&7), "{lines:?}");
2904 let windows = &instrumented.code[instrumented.code.find("#[cfg(windows)]").unwrap()
2905 ..instrumented.code.find("#[cfg(unix)]").unwrap()];
2906 assert!(!windows.contains("__supercov_runtime_v1"), "{windows}");
2907 }
2908
2909 #[test]
2910 fn test_code_is_known_by_where_it_sits() {
2911 for file in [
2912 "tests/it.rs",
2913 "tests/common/mod.rs",
2914 "crates/a/tests/it.rs",
2915 "examples/demo.rs",
2916 "benches/speed.rs",
2917 "src/tests.rs",
2918 "src/parser_tests.rs",
2919 "src/parser-tests.rs",
2920 ] {
2921 assert!(test_code_path(file), "{file}");
2922 }
2923 for file in [
2924 "src/lib.rs",
2925 "src/attests.rs",
2926 "src/_tests.rs",
2927 "crates/foo-tests/src/lib.rs",
2928 "src/testing.rs",
2929 ] {
2930 assert!(!test_code_path(file), "{file}");
2931 }
2932 }
2933
2934 #[test]
2935 fn test_items_keep_only_their_assertion_markers() {
2936 let source = r#"pub fn add(a: i32, b: i32) -> i32 {
2937 if a > 0 && b > 0 { a + b } else { 0 }
2938}
2939
2940#[cfg(test)]
2941mod tests {
2942 use super::*;
2943 #[test]
2944 fn adds() {
2945 let total = add(1, 2);
2946 assert_eq!(total, 3);
2947 }
2948}
2949
2950#[cfg(all(test, feature = "slow"))]
2951fn slow_helper() -> bool { true || false }
2952
2953#[tokio::test]
2954async fn async_adds() {
2955 assert!(add(2, 2) == 4);
2956}
2957"#;
2958 let transformed =
2959 instrument_rust_source("src/lib.rs", source, "crate::__supercov_runtime_v1").unwrap();
2960 let manifest = &transformed.manifest;
2961 assert!(
2962 manifest.points.iter().all(|point| point.line <= 3),
2963 "{:?}",
2964 manifest.points
2965 );
2966 assert!(manifest.decisions.iter().all(|decision| decision.line == 2));
2967 assert!(manifest.branches.iter().all(|branch| branch.line == 2));
2968 assert_eq!(manifest.decisions.len(), 1);
2969 let sites = manifest
2970 .assertion_sites
2971 .iter()
2972 .map(|site| site.source.as_str())
2973 .collect::<BTreeSet<_>>();
2974 assert_eq!(
2975 sites,
2976 BTreeSet::from(["assert!(add(2, 2) == 4);", "assert_eq!(total, 3);"])
2977 );
2978 let tests = &transformed.code[transformed.code.find("#[cfg(test)]").unwrap()..];
2979 assert_eq!(tests.matches("::assertion(").count(), 2, "{tests}");
2980 assert!(!tests.contains("::hit("), "{tests}");
2981 assert!(!tests.contains("::logical("), "{tests}");
2982
2983 let file = instrument_rust_source(
2985 "tests/it.rs",
2986 "#[test]\nfn it() {\n let x = 1;\n assert_eq!(x, 1);\n}\n",
2987 "crate::__supercov_runtime_v1",
2988 )
2989 .unwrap();
2990 assert!(file.manifest.points.is_empty());
2991 assert_eq!(file.manifest.assertion_sites.len(), 1);
2992 assert!(!file.code.contains("::hit("));
2993 }
2994
2995 #[test]
2996 fn discovers_rust_obligations_with_exact_ranges_and_stable_ids() {
2997 let source = r#"fn classify<T>(values: &[T], first: bool, second: bool, third: bool) -> Option<&T> {
2998 let picked = if first && (second || third) {
2999 values.first()?
3000 } else {
3001 None
3002 };
3003 for value in values {
3004 if first || second {
3005 return Some(value);
3006 }
3007 }
3008 match picked {
3009 Some(value) if second && third => Some(value),
3010 _ => None,
3011 }
3012}
3013
3014fn closure(value: i32) -> bool {
3015 (|candidate| candidate > 0)(value)
3016}
3017"#;
3018 let first = build_rust_manifest("src/lib.rs", source).unwrap();
3019 let second = build_rust_manifest("src/lib.rs", source).unwrap();
3020 assert_eq!(first, second);
3021 assert!(first.points.iter().any(|point| {
3022 point.kind == PointKind::Function && point.label.as_deref() == Some("classify")
3023 }));
3024 assert!(first.points.iter().any(|point| {
3025 point.kind == PointKind::Function && point.label.as_deref() == Some("<closure>")
3026 }));
3027 let first_if = first
3028 .decisions
3029 .iter()
3030 .find(|decision| decision.line == 2)
3031 .unwrap();
3032 assert_eq!(first_if.conditions, ["first", "second", "third"]);
3033 assert_eq!(first_if.column, 20);
3034 assert!(
3035 first
3036 .branches
3037 .iter()
3038 .any(|branch| branch.kind == "for-loop")
3039 );
3040 let mut arms = first
3041 .branches
3042 .iter()
3043 .filter(|branch| branch.kind == "match-arm")
3044 .collect::<Vec<_>>();
3045 arms.sort_by_key(|branch| branch.line);
3046 assert_eq!(arms.len(), 2);
3047 assert_eq!(
3048 arms[0]
3049 .alternatives
3050 .iter()
3051 .map(|alternative| alternative.label.as_str())
3052 .collect::<Vec<_>>(),
3053 ["not selected", "selected"]
3054 );
3055 assert_eq!(
3058 arms[1]
3059 .alternatives
3060 .iter()
3061 .map(|alternative| alternative.label.as_str())
3062 .collect::<Vec<_>>(),
3063 ["selected"]
3064 );
3065 assert!(
3066 first
3067 .branches
3068 .iter()
3069 .any(|branch| branch.kind == "try-operator")
3070 );
3071 assert!(first.decisions.iter().all(|decision| {
3072 decision.id.starts_with("rs:decision:") && decision.conditions.len() >= 2
3073 }));
3074 assert!(first.limitations.is_empty());
3075 }
3076
3077 #[test]
3078 fn declares_macro_and_const_boundaries_instead_of_hiding_them() {
3079 let source = r#"const fn doubled(value: usize) -> usize { value * 2 }
3080
3081macro_rules! noop {
3082 () => {};
3083}
3084
3085fn checked(value: bool) -> bool {
3086 assert!(value);
3087 noop!();
3088 const { doubled(2) == 4 }
3089}
3090"#;
3091 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
3092 assert!(manifest.decisions.iter().any(|decision| {
3095 decision.line == 8 && decision.source == "value" && decision.conditions == ["value"]
3096 }));
3097 let ids = manifest
3098 .limitations
3099 .iter()
3100 .filter_map(limitation_kind_of)
3101 .collect::<BTreeSet<_>>();
3102 assert_eq!(
3103 ids,
3104 BTreeSet::from([
3105 "rust-const-context-not-instrumented",
3106 "rust-macro-expansion-not-instrumented"
3107 ])
3108 );
3109 assert!(!manifest.points.iter().any(|point| {
3110 point.kind == PointKind::Function && point.label.as_deref() == Some("doubled")
3111 }));
3112 }
3113
3114 #[test]
3115 fn a_vec_macro_that_starts_a_larger_expression_keeps_its_probe_outside() {
3116 let source = "pub fn grown(n: usize) -> Vec<u8> {\n if n > 0 {\n vec![0; n].into()\n } else {\n vec![1; 2] == vec![1, 1] && n == 0;\n Vec::new()\n }\n}\n";
3120 let transformed =
3121 instrument_rust_source("src/lib.rs", source, "crate::__supercov_runtime_v1").unwrap();
3122 assert!(
3123 !transformed.code.contains("vec!crate"),
3124 "{}",
3125 transformed.code
3126 );
3127 assert!(
3128 transformed.code.contains("vec![0; n].into()"),
3129 "{}",
3130 transformed.code
3131 );
3132 }
3133
3134 #[test]
3135 fn transforms_points_and_nested_decisions_without_changing_behavior() {
3136 let source = r#"use std::sync::atomic::{AtomicUsize, Ordering};
3137
3138static CALLS: AtomicUsize = AtomicUsize::new(0);
3139
3140fn observed(name: &str, value: bool) -> bool {
3141 let order = CALLS.fetch_add(1, Ordering::SeqCst);
3142 println!("{order}:{name}:{value}");
3143 value
3144}
3145
3146fn classify(first: bool, second: bool, third: bool) -> i32 {
3147 if observed("a", first) && (observed("b", second) || observed("c", third)) {
3148 7
3149 } else {
3150 3
3151 }
3152}
3153
3154fn main() {
3155 let closure = |value: i32| value + 1;
3156 println!("result={}", closure(classify(true, false, true)));
3157}
3158"#;
3159 let transformed =
3160 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3161 assert!(transformed.code.contains("::condition("));
3162 assert!(transformed.code.contains("::decision("));
3163 assert!(transformed.code.contains("::hit("));
3164 let original = compile_and_run(source, "original");
3165 let instrumented = compile_and_run(
3166 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3167 "instrumented",
3168 );
3169 assert_eq!(instrumented.status, original.status);
3170 assert_eq!(instrumented.stdout, original.stdout);
3171 assert_eq!(instrumented.stderr, original.stderr);
3172 }
3173
3174 #[test]
3175 fn let_chains_take_derived_condition_probes_and_const_contexts_stay_declared() {
3176 let source = r#"const fn enabled(value: bool) -> bool {
3177 if value { true } else { false }
3178}
3179
3180fn classify(value: Option<bool>, fallback: bool) -> bool {
3181 if let Some(inner) = value && inner && fallback { true } else { false }
3182}
3183"#;
3184 let transformed =
3185 instrument_rust_source("src/lib.rs", source, "crate::__supercov_runtime_v1").unwrap();
3186 let ids = transformed
3187 .manifest
3188 .limitations
3189 .iter()
3190 .filter_map(limitation_kind_of)
3191 .collect::<BTreeSet<_>>();
3192 assert!(ids.contains("rust-const-context-not-instrumented"));
3193 assert!(!ids.contains("rust-let-chain-probes-not-injected"));
3194 assert!(
3197 transformed
3198 .code
3199 .contains("::reached(&mut __supercov_decision_")
3200 );
3201 assert!(
3202 transformed
3203 .code
3204 .contains("::condition((inner), &mut __supercov_decision_")
3205 );
3206 assert!(
3207 transformed
3208 .code
3209 .contains("::decision_chain(&mut __supercov_decision_")
3210 );
3211 assert!(transformed.code.contains("&& let Some(inner) = value &&"));
3212 assert!(!transformed.code.contains("condition((let"));
3213 }
3214
3215 #[test]
3216 fn std_macro_arguments_take_probes_and_assertions_are_decisions() {
3217 let source = r#"use std::fmt::Write as _;
3218
3219fn classify(values: &[i32], strict: bool) -> String {
3220 let mut out = String::new();
3221 assert!(values.len() < 10 && (strict || !values.is_empty()), "bad input {:?}", values);
3222 debug_assert!(values.iter().all(|v| *v > -100));
3223 let doubled = vec![values.iter().map(|v| v * 2).sum::<i32>(), if strict { 1 } else { 2 }];
3224 let repeated = vec![if strict { 1 } else { 0 }; values.len()];
3225 let small = matches!(values.first(), Some(v) if *v < 3);
3226 if matches!(values.len(), 1 | 2) && small {
3227 println!("small");
3228 }
3229 println!("{}", repeated.len() + small as usize);
3230 write!(out, "{}", doubled.iter().map(|d| if *d > 4 { "big" } else { "small" }).collect::<Vec<_>>().join(",")).unwrap();
3231 println!("{} {}", format!("{:?}", doubled), if values.first().copied().unwrap_or(0) > 0 && strict { "positive" } else { "other" });
3232 assert_eq!(doubled.len(), if strict { 2 } else { 2 }, "length for strict={strict}");
3233 out
3234}
3235
3236fn main() {
3237 println!("{}", classify(&[1, 2], true));
3238 println!("{}", classify(&[3], false));
3239 println!("{}", classify(&[], true));
3240 let total: i32 = dbg!(vec![1, 2, 3]).into_iter().sum();
3241 println!("{total}");
3242}
3243"#;
3244 let transformed =
3245 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3246 assert!(transformed.code.contains(
3249 r#", "assertion failed: {}", stringify!(values.iter().all(|v| *v > -100)))"#
3250 ));
3251 assert!(
3253 transformed
3254 .code
3255 .contains(r#"), "bad input {:?}", values);"#)
3256 );
3257 let assertion = transformed
3261 .manifest
3262 .decisions
3263 .iter()
3264 .find(|decision| decision.line == 5)
3265 .expect("assert! decision");
3266 assert_eq!(
3267 assertion.conditions,
3268 ["values.len() < 10", "strict", "!values.is_empty()"]
3269 );
3270 assert!(
3271 transformed
3272 .manifest
3273 .decisions
3274 .iter()
3275 .any(|decision| decision.line == 7)
3276 );
3277 assert!(
3278 transformed
3279 .manifest
3280 .decisions
3281 .iter()
3282 .any(|decision| decision.line == 9)
3283 );
3284 assert!(
3285 transformed
3286 .code
3287 .contains("assert!(({ let mut __supercov_decision_")
3288 );
3289 assert!(
3290 transformed
3291 .code
3292 .contains(", if ({ let mut __supercov_decision_")
3293 );
3294 assert!(
3296 transformed
3297 .code
3298 .contains("vec![if ({ let mut __supercov_decision_")
3299 );
3300 assert!(
3303 transformed
3304 .code
3305 .contains("let small = ({ let mut __supercov_decision_")
3306 );
3307 assert_eq!(
3308 transformed
3309 .manifest
3310 .decisions
3311 .iter()
3312 .filter(|decision| decision.line == 10)
3313 .count(),
3314 1
3315 );
3316 assert_eq!(
3317 transformed
3318 .manifest
3319 .decisions
3320 .iter()
3321 .filter(|decision| decision.line == 9)
3322 .count(),
3323 1
3324 );
3325 assert!(
3326 transformed
3327 .code
3328 .contains("vec![values.iter().map(|v| { crate::__supercov_runtime_v1::hit(")
3329 );
3330 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
3331 limitation.get("id").and_then(|id| id.as_str())
3332 == Some("rust-macro-expansion-not-instrumented")
3333 }));
3334 let original = compile_and_run(source, "original-macros");
3335 let instrumented = compile_and_run(
3336 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3337 "instrumented-macros",
3338 );
3339 assert_eq!(instrumented.status, original.status);
3340 assert_eq!(instrumented.stdout, original.stdout);
3341 let after_location = |stderr: &[u8]| {
3344 String::from_utf8_lossy(stderr)
3345 .lines()
3346 .map(|line| {
3347 line.split_once("] ")
3348 .map_or(line, |(_, rest)| rest)
3349 .to_owned()
3350 })
3351 .collect::<Vec<_>>()
3352 };
3353 assert_eq!(
3354 after_location(&instrumented.stderr),
3355 after_location(&original.stderr)
3356 );
3357 }
3358
3359 #[test]
3360 fn assertion_panic_messages_survive_instrumentation() {
3361 let source = r#"fn grow(len: usize, new_capacity: usize) {
3364 assert!(new_capacity >= len);
3365}
3366
3367fn check(value: i32) {
3368 assert!(value > 0 && value < 10, "value {value} out of range");
3369}
3370
3371// tokio: `assert!` only negates its operand, so a `&bool` is accepted.
3372fn all_seen(seen: &[bool]) {
3373 for was_seen in seen {
3374 assert!(was_seen);
3375 debug_assert!(was_seen, "seen");
3376 }
3377}
3378
3379fn main() {
3380 std::panic::set_hook(Box::new(|_| {}));
3381 all_seen(&[true, true]);
3382 match std::panic::catch_unwind(|| all_seen(&[true, false])) {
3383 Ok(()) => println!("ok"),
3384 Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
3385 }
3386 for (len, capacity) in [(3, 5), (8, 5)] {
3387 match std::panic::catch_unwind(|| grow(len, capacity)) {
3388 Ok(()) => println!("ok"),
3389 Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
3390 }
3391 }
3392 match std::panic::catch_unwind(|| check(12)) {
3393 Ok(()) => println!("ok"),
3394 Err(payload) => println!("{}", payload.downcast_ref::<String>().cloned().unwrap_or_default()),
3395 }
3396}
3397"#;
3398 let transformed =
3399 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3400 let original = compile_and_run(source, "original-assert-message");
3401 let instrumented = compile_and_run(
3402 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3403 "instrumented-assert-message",
3404 );
3405 assert_eq!(instrumented.status, original.status);
3406 assert_eq!(instrumented.stdout, original.stdout);
3407 assert!(
3408 String::from_utf8_lossy(&instrumented.stdout)
3409 .contains("assertion failed: new_capacity >= len")
3410 );
3411 assert!(
3412 String::from_utf8_lossy(&instrumented.stdout).contains("assertion failed: was_seen")
3413 );
3414 }
3415
3416 #[test]
3417 fn files_that_predate_a_reserved_word_still_instrument() {
3418 let source = r#"struct Rng(u64);
3420impl Rng {
3421 fn gen(&mut self) -> u64 {
3422 self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1);
3423 self.0 >> 33
3424 }
3425}
3426
3427fn main() {
3428 let mut rng = Rng(7);
3429 let mut odd = 0;
3430 for _ in 0..10 {
3431 if rng.gen() % 2 == 1 {
3432 odd += 1;
3433 }
3434 }
3435 println!("{odd}");
3436}
3437"#;
3438 let transformed =
3439 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3440 assert!(
3441 transformed
3442 .manifest
3443 .decisions
3444 .iter()
3445 .any(|decision| decision.line == 13)
3446 );
3447 let original = compile_and_run_edition(source, "original-gen", "2021");
3448 let instrumented = compile_and_run_edition(
3449 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3450 "instrumented-gen",
3451 "2021",
3452 );
3453 assert_eq!(instrumented.status, original.status);
3454 assert_eq!(instrumented.stdout, original.stdout);
3455 }
3456
3457 #[test]
3458 fn lone_let_conditions_compile_before_edition_2024_and_record_breaks() {
3459 let source = r#"fn first_even(values: &[i32]) -> Option<i32> {
3463 let mut it = values.iter();
3464 'scan: while let Some(value) = it.next() {
3465 if *value < 0 {
3466 break;
3467 }
3468 for _ in 0..1 {
3469 if *value == 99 {
3470 break 'scan;
3471 }
3472 if *value == 98 {
3473 break;
3474 }
3475 }
3476 if *value % 2 == 0 {
3477 return Some(*value);
3478 }
3479 }
3480 None
3481}
3482
3483fn describe(value: Option<i32>) -> &'static str {
3484 if let Some(inner) = value {
3485 if inner > 0 { "positive" } else { "non-positive" }
3486 } else if let None = value {
3487 "none"
3488 } else {
3489 "unreachable"
3490 }
3491}
3492
3493fn count(values: &[Option<i32>]) -> usize {
3494 let mut total = 0;
3495 for value in values {
3496 if let Some(_) = value {
3497 total += 1;
3498 }
3499 }
3500 total
3501}
3502
3503fn main() {
3504 println!("{:?} {:?} {:?} {:?}", first_even(&[1, 3, 4]), first_even(&[1, -1, 4]), first_even(&[99, 4]), first_even(&[98, 3, 6]));
3505 println!("{} {} {}", describe(Some(2)), describe(Some(-2)), describe(None));
3506 println!("{}", count(&[Some(1), None, Some(3)]));
3507}
3508"#;
3509 let transformed =
3510 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3511 assert!(!transformed.code.contains("&& let"));
3512 assert!(transformed.code.contains("__supercov_broke_"));
3513 assert_eq!(transformed.code.matches("= true; break").count(), 2);
3514 for edition in ["2021", "2024"] {
3515 let original =
3516 compile_and_run_edition(source, &format!("original-lone-let-{edition}"), edition);
3517 let instrumented = compile_and_run_edition(
3518 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3519 &format!("instrumented-lone-let-{edition}"),
3520 edition,
3521 );
3522 assert_eq!(instrumented.status, original.status);
3523 assert_eq!(instrumented.stdout, original.stdout);
3524 assert_eq!(instrumented.stderr, original.stderr);
3525 }
3526 }
3527
3528 #[test]
3529 fn let_chains_keep_their_behavior() {
3530 let source = r#"fn describe(value: Option<i32>, flag: bool) -> &'static str {
3531 if let Some(inner) = value && inner > 0 && flag {
3532 "positive"
3533 } else if let Some(inner) = value && (inner < 0 || flag) {
3534 "negative-or-flagged"
3535 } else {
3536 "other"
3537 }
3538}
3539
3540fn count_pairs(values: &[(Option<i32>, i32)]) -> i32 {
3541 let mut total = 0;
3542 let mut it = values.iter();
3543 while let Some((first, second)) = it.next() && let Some(inner) = first && *second > 0 {
3544 total += inner * second;
3545 if total > 100 {
3546 break;
3547 }
3548 }
3549 total
3550}
3551
3552fn tail(value: Option<&str>) -> usize {
3553 let pick = |v: Option<&str>| if let Some(text) = v && !text.is_empty() { text.len() } else { 0 };
3554 if let Some(text) = value && text.starts_with('x') {
3555 println!("x-prefixed");
3556 }
3557 pick(value)
3558}
3559
3560fn main() {
3561 for value in [Some(3), Some(-3), Some(0), None] {
3562 for flag in [true, false] {
3563 println!("{value:?} {flag} {}", describe(value, flag));
3564 }
3565 }
3566 println!("{}", count_pairs(&[(Some(2), 3), (Some(4), 5), (None, 1), (Some(9), 9)]));
3567 println!("{}", count_pairs(&[(Some(50), 3), (Some(4), 5)]));
3568 println!("{} {} {}", tail(Some("xyz")), tail(Some("")), tail(None));
3569}
3570"#;
3571 let transformed =
3572 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3573 assert_eq!(
3574 transformed.code.matches("const __SUPERCOV_CHAIN_").count(),
3575 5
3576 );
3577 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
3578 limitation.get("id").and_then(|id| id.as_str())
3579 == Some("rust-let-chain-probes-not-injected")
3580 }));
3581 let original = compile_and_run(source, "original-chains");
3582 let instrumented = compile_and_run(
3583 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3584 "instrumented-chains",
3585 );
3586 assert_eq!(instrumented.status, original.status);
3587 assert_eq!(instrumented.stdout, original.stdout);
3588 assert_eq!(instrumented.stderr, original.stderr);
3589 }
3590
3591 #[test]
3592 fn instrumented_const_and_static_initialisers_still_compile() {
3593 let source = r#"const DIRECT: usize = if cfg!(unix) { 100 } else { 1_000 };
3599static WIDTH: usize = if cfg!(unix) { 2 } else { 4 };
3600
3601enum Mode {
3602 Narrow = if cfg!(unix) { 1 } else { 2 },
3603}
3604
3605struct Buffer([u8; if cfg!(unix) { 4 } else { 8 }]);
3606
3607impl Buffer {
3608 const SPAN: usize = if cfg!(unix) { 5 } else { 9 };
3609}
3610
3611fn scaled(flag: bool) -> usize {
3612 const LOCAL: usize = if cfg!(unix) { 3 } else { 6 };
3613 if flag { LOCAL + Buffer::SPAN } else { DIRECT + WIDTH }
3614}
3615
3616fn main() {
3617 let buffer = Buffer([0; if cfg!(unix) { 4 } else { 8 }]);
3618 println!(
3619 "{} {} {} {}",
3620 scaled(true),
3621 scaled(false),
3622 Mode::Narrow as usize,
3623 buffer.0.len()
3624 );
3625}
3626"#;
3627 let transformed =
3628 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3629 assert!(transformed.code.contains("::decision("));
3632 let ids = transformed
3633 .manifest
3634 .limitations
3635 .iter()
3636 .filter_map(limitation_kind_of)
3637 .collect::<BTreeSet<_>>();
3638 assert!(ids.contains("rust-const-context-not-instrumented"));
3639
3640 let original = compile_and_run(source, "const-original");
3641 let instrumented = compile_and_run(
3642 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3643 "const-instrumented",
3644 );
3645 assert_eq!(instrumented.status, original.status);
3646 assert_eq!(instrumented.stdout, original.stdout);
3647 assert_eq!(instrumented.stderr, original.stderr);
3648 }
3649
3650 #[test]
3651 fn a_probed_global_allocator_would_recurse_into_itself() {
3652 let source = r#"use std::alloc::{GlobalAlloc, Layout, System};
3658
3659struct Odd;
3660
3661unsafe impl GlobalAlloc for Odd {
3662 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
3663 if layout.align() == 1 && layout.size() > 0 {
3664 System.alloc(layout)
3665 } else {
3666 System.alloc(layout)
3667 }
3668 }
3669
3670 unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
3671 System.dealloc(pointer, layout);
3672 }
3673}
3674
3675#[global_allocator]
3676static ODD: Odd = Odd;
3677
3678fn classify(flag: bool) -> usize {
3679 if flag { 1 } else { 2 }
3680}
3681
3682fn main() {
3683 let held = std::vec![7u8; 32];
3684 println!("{} {}", classify(!held.is_empty()), held.len());
3685}
3686"#;
3687 let transformed =
3688 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3689 let allocator = transformed
3691 .code
3692 .split("unsafe impl GlobalAlloc for Odd")
3693 .nth(1)
3694 .and_then(|rest| rest.split("#[global_allocator]").next())
3695 .expect("the instrumented source still contains the allocator impl");
3696 assert!(
3697 !allocator.contains("__supercov_runtime_v1"),
3698 "probe injected into a GlobalAlloc impl:\n{allocator}"
3699 );
3700 assert!(transformed.code.contains("::decision("));
3702 let ids = transformed
3703 .manifest
3704 .limitations
3705 .iter()
3706 .filter_map(limitation_kind_of)
3707 .collect::<BTreeSet<_>>();
3708 assert!(ids.contains("rust-global-allocator-not-instrumented"));
3709
3710 let original = compile_and_run(source, "alloc-original");
3711 let instrumented = compile_and_run(
3712 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3713 "alloc-instrumented",
3714 );
3715 assert_eq!(instrumented.status, original.status);
3716 assert_eq!(instrumented.stdout, original.stdout);
3717 assert_eq!(instrumented.stderr, original.stderr);
3718 }
3719
3720 #[test]
3721 fn match_arms_record_selection_without_changing_behavior() {
3722 let source = r#"#[derive(Debug)]
3723enum Shape { Dot, Line(i32), Box { w: i32, h: i32 } }
3724
3725fn area(shape: &Shape) -> i32 {
3726 match shape {
3727 Shape::Dot => 0,
3728 Shape::Line(length) if *length < 0 => -length,
3729 Shape::Line(length) => *length,
3730 Shape::Box { w, h } => {
3731 let area = w * h;
3732 area
3733 }
3734 }
3735}
3736
3737fn describe(value: i32) -> &'static str {
3738 let inner = |v: i32| match v { 0 => "none", 1 => "one", _ => "many" };
3739 match value {
3740 0 => inner(value),
3741 n if n < 0 => unsafe { std::hint::unreachable_unchecked() },
3742 n => match n % 2 {
3743 0 => "even",
3744 _ => inner(n),
3745 },
3746 }
3747}
3748
3749fn main() {
3750 for shape in [Shape::Dot, Shape::Line(-3), Shape::Line(4), Shape::Box { w: 2, h: 5 }] {
3751 println!("{shape:?}={}", area(&shape));
3752 }
3753 for value in [0, 1, 3, 8] {
3754 println!("{value}:{}", describe(value));
3755 }
3756}
3757"#;
3758 let transformed =
3759 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3760 assert!(transformed.code.contains("::arms(__SUPERCOV_ARMS_"));
3761 assert_eq!(
3762 transformed.code.matches("const __SUPERCOV_ARMS_").count(),
3763 4
3764 );
3765 let arms = transformed
3766 .manifest
3767 .branches
3768 .iter()
3769 .filter(|branch| branch.kind == "match-arm")
3770 .count();
3771 assert_eq!(arms, 4 + 3 + 3 + 2);
3772 for branch in transformed
3775 .manifest
3776 .branches
3777 .iter()
3778 .filter(|branch| branch.kind == "match-arm")
3779 {
3780 for alternative in &branch.alternatives {
3781 assert!(
3782 transformed.code.contains(&format!("{:?}", alternative.id)),
3783 "{} is not in any table",
3784 alternative.id
3785 );
3786 }
3787 }
3788 let original = compile_and_run(source, "original-arms");
3789 let instrumented = compile_and_run(
3790 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3791 "instrumented-arms",
3792 );
3793 assert_eq!(instrumented.status, original.status);
3794 assert_eq!(instrumented.stdout, original.stdout);
3795 assert_eq!(instrumented.stderr, original.stderr);
3796 }
3797
3798 #[test]
3799 fn loops_logic_and_try_record_their_branches_without_changing_behavior() {
3800 let source = r#"use std::ops::ControlFlow;
3801
3802fn total(values: &[i32]) -> i32 {
3803 let mut sum = 0;
3804 for value in values {
3805 sum += value;
3806 }
3807 'outer: for row in 0..3 {
3808 for column in 0..3 {
3809 if column > row {
3810 continue 'outer;
3811 }
3812 sum += row * column;
3813 }
3814 }
3815 sum
3816}
3817
3818fn first_even(values: &[i32]) -> Option<i32> {
3819 let mut index = 0;
3820 'scan: while index < values.len() {
3821 if values[index] % 2 == 0 {
3822 break 'scan;
3823 }
3824 index += 1;
3825 }
3826 let mut it = values.iter().skip(index);
3827 while let Some(value) = it.next() {
3828 return Some(*value);
3829 }
3830 None
3831}
3832
3833fn parse_twice(text: &str) -> Result<i32, String> {
3834 let value: i32 = text.trim().parse().map_err(|_| "bad".to_string())?;
3835 let doubled = Some(value).map(|v| v * 2).ok_or("none")?;
3836 Ok(doubled)
3837}
3838
3839fn halve(value: i32) -> Option<i32> {
3840 let even = (value % 2 == 0).then_some(value)?;
3841 Some(even / 2)
3842}
3843
3844fn flow(values: &[i32]) -> ControlFlow<i32, i32> {
3845 let mut sum = 0;
3846 for value in values {
3847 let step: ControlFlow<i32, i32> = if *value < 0 { ControlFlow::Break(*value) } else { ControlFlow::Continue(*value) };
3848 sum += step?;
3849 }
3850 ControlFlow::Continue(sum)
3851}
3852
3853fn gate(a: bool, b: bool, c: bool) -> bool {
3854 let both = a && b;
3855 let either = a || b || c;
3856 both || (either && !c) || (c && a && (b || !b))
3857}
3858
3859fn main() {
3860 println!("{} {}", total(&[]), total(&[1, 2, 3]));
3861 println!("{:?} {:?} {:?}", first_even(&[]), first_even(&[1, 3]), first_even(&[1, 4, 6]));
3862 println!("{:?} {:?}", parse_twice(" 21 "), parse_twice("x"));
3863 println!("{:?} {:?}", halve(8), halve(7));
3864 println!("{:?} {:?}", flow(&[1, 2]), flow(&[1, -5, 2]));
3865 for a in [false, true] {
3866 for b in [false, true] {
3867 for c in [false, true] {
3868 print!("{}", gate(a, b, c) as u8);
3869 }
3870 }
3871 }
3872 println!();
3873}
3874"#;
3875 let transformed =
3876 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3877 for marker in [
3878 "::logical((",
3879 "::for_loop((",
3880 "::entered(&mut __supercov_loop_",
3881 "::zero_iterations(__supercov_loop_",
3882 "::TryProbe::probe((",
3883 ] {
3884 assert!(transformed.code.contains(marker), "{marker} missing");
3885 }
3886 let kinds = |kind: &str| {
3887 transformed
3888 .manifest
3889 .branches
3890 .iter()
3891 .filter(|branch| branch.kind == kind)
3892 .count()
3893 };
3894 assert_eq!(kinds("for-loop"), 3 + 1 + 3);
3895 assert_eq!(kinds("while-loop"), 2);
3896 assert_eq!(kinds("try-operator"), 4);
3897 assert_eq!(kinds("logical-and"), 4);
3898 assert_eq!(kinds("logical-or"), 5);
3899 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
3900 limitation.get("id").and_then(|id| id.as_str())
3901 == Some("rust-structural-branch-probes-not-yet-injected")
3902 }));
3903 let original = compile_and_run(source, "original-structural");
3904 let instrumented = compile_and_run(
3905 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3906 "instrumented-structural",
3907 );
3908 assert_eq!(instrumented.status, original.status);
3909 assert_eq!(instrumented.stdout, original.stdout);
3910 assert_eq!(instrumented.stderr, original.stderr);
3911 }
3912
3913 #[test]
3914 fn cfg_gated_sibling_blocks_keep_their_tail_position() {
3915 let source = r#"pub fn is_available() -> bool {
3921 #[cfg(target_endian = "little")]
3922 {
3923 true
3924 }
3925 #[cfg(not(target_endian = "little"))]
3926 {
3927 false
3928 }
3929}
3930
3931fn main() {
3932 println!("{}", is_available());
3933}
3934"#;
3935 let transformed =
3936 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3937 let original = compile_and_run(source, "cfg-original");
3938 let instrumented = compile_and_run(
3939 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3940 "cfg-instrumented",
3941 );
3942 assert_eq!(instrumented.status, original.status);
3943 assert_eq!(instrumented.stdout, original.stdout);
3944 assert!(
3946 transformed
3947 .code
3948 .contains("{crate::__supercov_runtime_v1::hit("),
3949 "{}",
3950 transformed.code
3951 );
3952
3953 let attributed_let = r#"fn main() {
3957 #[cfg(target_endian = "little")]
3958 let value = 1;
3959 #[cfg(not(target_endian = "little"))]
3960 let value = 2;
3961 #[cfg(target_endian = "little")]
3962 let borrowed: &String = &String::from("little");
3963 #[cfg(not(target_endian = "little"))]
3964 let borrowed: &String = &String::from("big");
3965 #[cfg(target_endian = "little")]
3966 print!("le ");
3967 #[cfg(not(target_endian = "little"))]
3968 print!("be ");
3969 #[allow(unused_assignments)]
3970 let mut later;
3971 later = value + 1;
3972 println!("{value} {borrowed} {later}");
3973}
3974"#;
3975 let transformed = instrument_rust_source(
3976 "src/main.rs",
3977 attributed_let,
3978 "crate::__supercov_runtime_v1",
3979 )
3980 .unwrap();
3981 let ids = transformed
3982 .manifest
3983 .limitations
3984 .iter()
3985 .filter_map(limitation_kind_of)
3986 .collect::<BTreeSet<_>>();
3987 assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
3989 assert!(
3990 transformed
3991 .code
3992 .contains("let value = { crate::__supercov_runtime_v1::hit(")
3993 );
3994 assert!(
3995 transformed
3996 .code
3997 .contains("let borrowed: &String = { crate::__supercov_runtime_v1::hit(")
3998 );
3999 assert!(
4000 transformed
4001 .code
4002 .contains("] { crate::__supercov_runtime_v1::hit(")
4003 );
4004 let original = compile_and_run(attributed_let, "cfg-let-original");
4005 let instrumented = compile_and_run(
4006 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4007 "cfg-let-instrumented",
4008 );
4009 assert_eq!(instrumented.status, original.status);
4010 assert_eq!(instrumented.stdout, original.stdout);
4011 assert_eq!(instrumented.stderr, original.stderr);
4012
4013 let trailing_macro = r#"macro_rules! pick { ($e:expr) => { $e } }
4018fn value() -> i32 {
4019 let base = 20;
4020 #[rustfmt::skip]
4021 pick! { base + 1 }
4022}
4023fn effect() {
4024 #[rustfmt::skip]
4025 println! { "effect" }
4026}
4027fn gated() {
4028 #[cfg(target_endian = "little")]
4029 println! { "little" }
4030}
4031fn main() {
4032 effect();
4033 gated();
4034 println!("{}", value());
4035}
4036"#;
4037 let transformed = instrument_rust_source(
4038 "src/main.rs",
4039 trailing_macro,
4040 "crate::__supercov_runtime_v1",
4041 )
4042 .unwrap();
4043 assert!(
4044 transformed
4045 .code
4046 .contains("{ crate::__supercov_runtime_v1::hit(\"rs:statement:")
4047 );
4048 assert!(
4049 transformed
4050 .code
4051 .contains("); #[rustfmt::skip]\n pick! { base + 1 } }")
4052 );
4053 assert!(
4054 transformed
4055 .code
4056 .contains("); #[rustfmt::skip]\n println! { \"effect\" } }")
4057 );
4058 assert!(
4059 transformed.code.contains(
4060 "\n #[cfg(target_endian = \"little\")]\n println! { \"little\" }\n"
4061 )
4062 );
4063 let ids = transformed
4064 .manifest
4065 .limitations
4066 .iter()
4067 .filter_map(limitation_kind_of)
4068 .collect::<BTreeSet<_>>();
4069 assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
4070 let original = compile_and_run(trailing_macro, "trailing-macro-original");
4071 let instrumented = compile_and_run(
4072 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4073 "trailing-macro-instrumented",
4074 );
4075 assert_eq!(
4076 instrumented.status,
4077 original.status,
4078 "{}",
4079 String::from_utf8_lossy(&instrumented.stderr)
4080 );
4081 assert_eq!(instrumented.stdout, original.stdout);
4082 assert_eq!(instrumented.stderr, original.stderr);
4083 }
4084
4085 #[test]
4086 fn an_attributed_macro_statement_keeps_its_macro_in_statement_position() {
4087 let source = r#"macro_rules! trace {
4092 ($($arg:tt)*) => {
4093 #[cfg(target_endian = "little")]
4094 {
4095 println!($($arg)+);
4096 }
4097 }
4098}
4099fn manual() {
4100 #[cfg(any(target_endian = "little", target_endian = "big"))]
4101 trace!("manual");
4102 let _ = 1;
4103}
4104fn main() {
4105 manual();
4106}
4107"#;
4108 let transformed =
4109 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
4110 assert!(
4113 transformed.code.contains(r#"trace!("manual"); }"#),
4114 "{}",
4115 transformed.code
4116 );
4117 let original = compile_and_run(source, "attributed-macro-statement-original");
4118 let instrumented = compile_and_run(
4119 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4120 "attributed-macro-statement-instrumented",
4121 );
4122 assert_eq!(
4123 instrumented.status,
4124 original.status,
4125 "{}",
4126 String::from_utf8_lossy(&instrumented.stderr)
4127 );
4128 assert_eq!(instrumented.stdout, original.stdout);
4129 assert_eq!(instrumented.stderr, original.stderr);
4130 }
4131
4132 #[test]
4133 fn an_attributed_lets_macro_initialiser_keeps_its_value() {
4134 let source = r#"macro_rules! first {
4140 ($e:expr) => { $e }
4141}
4142fn value(flag: bool) -> i32 {
4143 #[cfg(any(target_endian = "little", target_endian = "big"))]
4144 let chosen = first!(if flag { 7 } else { 3 });
4145 chosen + 1
4146}
4147fn main() {
4148 println!("{}", value(true));
4149}
4150"#;
4151 let transformed =
4152 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
4153 assert!(
4156 !transformed
4157 .code
4158 .contains("first!(if flag { 7 } else { 3 }); }"),
4159 "{}",
4160 transformed.code
4161 );
4162 let original = compile_and_run(source, "attributed-let-macro-original");
4163 let instrumented = compile_and_run(
4164 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4165 "attributed-let-macro-instrumented",
4166 );
4167 assert_eq!(
4168 instrumented.status,
4169 original.status,
4170 "{}",
4171 String::from_utf8_lossy(&instrumented.stderr)
4172 );
4173 assert_eq!(instrumented.stdout, original.stdout);
4174 assert_eq!(instrumented.stdout, b"8\n");
4175 }
4176
4177 #[test]
4178 fn obligations_no_probe_can_reach_are_declined() {
4179 let source = r#"pub struct Tagged(usize);
4185impl Tagged {
4186 pub const fn new(len: usize, on_heap: bool) -> Self {
4187 Self(if on_heap { len << 1 } else { len })
4188 }
4189 pub fn plain(len: usize) -> Self {
4190 Self(if len > 0 { len } else { 0 })
4191 }
4192}
4193"#;
4194 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
4195 let declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
4196 assert!(!declined.is_empty(), "the const fn was not declined");
4197
4198 let line_of = |id: &str| {
4200 manifest
4201 .points
4202 .iter()
4203 .find(|point| point.id == id)
4204 .map(|point| point.line)
4205 .or_else(|| {
4206 manifest
4207 .decisions
4208 .iter()
4209 .find(|decision| decision.id == id)
4210 .map(|decision| decision.line)
4211 })
4212 .or_else(|| {
4213 manifest
4214 .branches
4215 .iter()
4216 .find(|branch| branch.id == id)
4217 .map(|branch| branch.line)
4218 })
4219 };
4220 for id in &declined {
4221 let line = line_of(id).unwrap_or_else(|| panic!("no obligation {id}"));
4222 assert!(
4223 (3..=5).contains(&line),
4224 "declined an obligation outside the const fn, at line {line}"
4225 );
4226 }
4227 let measured = manifest
4228 .points
4229 .iter()
4230 .map(|point| (point.id.clone(), point.line))
4231 .filter(|(id, _)| !declined.contains(id))
4232 .collect::<Vec<_>>();
4233 assert!(
4234 measured.iter().any(|(_, line)| (6..=8).contains(line)),
4235 "the plain fn must stay measured: {measured:?}"
4236 );
4237 }
4238
4239 #[test]
4240 fn a_documented_function_is_reported_where_it_starts() {
4241 let source = r#"/// Documented.
4244/// Twice.
4245#[inline]
4246pub fn documented(value: i32) -> i32 {
4247 value + 1
4248}
4249"#;
4250 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
4251 let point = manifest
4252 .points
4253 .iter()
4254 .find(|point| {
4255 point.kind == PointKind::Function && point.label.as_deref() == Some("documented")
4256 })
4257 .unwrap();
4258 assert_eq!(point.line, 4, "{point:?}");
4259 assert!(
4260 point.source.starts_with("pub fn documented"),
4261 "{}",
4262 point.source
4263 );
4264 }
4265
4266 #[test]
4267 fn rejects_non_crate_local_runtime_paths() {
4268 assert_eq!(
4269 instrument_rust_source("src/lib.rs", "fn okay() {}", "supercov::runtime"),
4270 Err(RustInstrumenterError::InvalidRuntimePath)
4271 );
4272 }
4273
4274 #[test]
4275 fn rejects_invalid_rust_without_partial_obligations() {
4276 assert!(matches!(
4277 build_rust_manifest("src/lib.rs", "fn broken( {\n"),
4278 Err(RustInstrumenterError::Parse(_))
4279 ));
4280 }
4281}