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