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
813 for binary in root.descendants().filter_map(ast::BinExpr::cast) {
814 let kind = match binary.op_kind() {
815 Some(BinaryOp::LogicOp(LogicOp::And)) => "logical-and",
816 Some(BinaryOp::LogicOp(LogicOp::Or)) => "logical-or",
817 _ => continue,
818 };
819 let range = binary.rhs().map_or_else(
820 || binary.syntax().text_range(),
821 |right| right.syntax().text_range(),
822 );
823 self.branch(
824 range,
825 kind,
826 [
827 ("short-circuit", "short-circuited"),
828 ("evaluated", "right operand evaluated"),
829 ],
830 );
831 }
832
833 for expression in root.descendants().filter_map(ast::ForExpr::cast) {
834 self.branch(
835 expression.syntax().text_range(),
836 "for-loop",
837 [("zero", "zero iterations"), ("entered", "entered")],
838 );
839 }
840 for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
841 let Some(list) = expression.match_arm_list() else {
842 continue;
843 };
844 let arms = list.arms().collect::<Vec<_>>();
845 let last = arms.len().saturating_sub(1);
846 for (index, arm) in arms.iter().enumerate() {
847 let range = arm.syntax().text_range();
848 if index == last {
849 self.branch(range, "match-arm", [("selected", "selected")]);
853 } else {
854 self.branch(
855 range,
856 "match-arm",
857 [("missed", "not selected"), ("selected", "selected")],
858 );
859 }
860 }
861 }
862 for expression in root.descendants().filter_map(ast::TryExpr::cast) {
863 self.branch(
864 expression.syntax().text_range(),
865 "try-operator",
866 [("continued", "continued"), ("returned", "early return")],
867 );
868 }
869
870 let mut macro_names = BTreeSet::new();
876 let mut first_macro = None;
877 let mut macro_sites = 0;
878 for call in root.descendants().filter_map(ast::MacroCall::cast) {
879 macro_names.insert(
880 call.path()
881 .map(|path| path.syntax().text().to_string())
882 .unwrap_or_else(|| "?".into()),
883 );
884 first_macro.get_or_insert_with(|| call.syntax().text_range());
885 macro_sites += 1;
886 }
887 if let Some(range) = first_macro {
888 let named = macro_names
889 .iter()
890 .take(4)
891 .map(|name| format!("`{name}!`"))
892 .collect::<Vec<_>>()
893 .join(", ");
894 let rest = macro_names.len().saturating_sub(4);
895 self.site_limitation(
896 "rust-macro-expansion-not-instrumented",
897 range,
898 format!(
899 "{named}{} expand in the compiler: their arguments and expansions are outside the owned source denominator ({macro_sites} call site{} in this file)",
900 if rest == 0 {
901 String::new()
902 } else {
903 format!(" and {rest} other macro{}", if rest == 1 { "" } else { "s" })
904 },
905 if macro_sites == 1 { "" } else { "s" }
906 ),
907 );
908 }
909
910 let bears_obligation = |node: &ra_ap_syntax::SyntaxNode| {
915 ast::StmtList::cast(node.clone()).is_some_and(|list| {
916 list.statements().next().is_some() || list.tail_expr().is_some()
917 }) || ast::IfExpr::can_cast(node.kind())
918 || ast::WhileExpr::can_cast(node.kind())
919 || ast::MatchGuard::can_cast(node.kind())
920 || ast::ForExpr::can_cast(node.kind())
921 || ast::MatchArm::can_cast(node.kind())
922 || ast::TryExpr::can_cast(node.kind())
923 || ast::ClosureExpr::can_cast(node.kind())
924 || ast::BinExpr::cast(node.clone()).is_some_and(|binary| {
925 matches!(
926 binary.op_kind(),
927 Some(BinaryOp::LogicOp(LogicOp::And | LogicOp::Or))
928 )
929 })
930 };
931 if let Some(node) = root
932 .descendants()
933 .find(|node| bears_obligation(node) && in_const_context(node))
934 {
935 self.site_limitation(
936 "rust-const-context-not-instrumented",
937 node.text_range(),
938 "Runtime probes cannot execute in const fn or compile-time evaluation; this and any later const context in the file stay declared".into(),
939 );
940 }
941 if let Some(node) = root
942 .descendants()
943 .find(|node| bears_obligation(node) && in_global_allocator(node))
944 {
945 self.site_limitation(
946 "rust-global-allocator-not-instrumented",
947 node.text_range(),
948 "Probing a GlobalAlloc implementation recurses into itself, because the runtime allocates".into(),
949 );
950 }
951
952 self.decline_unreachable_obligations(root);
953 self.leave_out_test_code();
954
955 if let Some(error) = self.error {
956 return Err(error);
957 }
958 self.manifest
959 .decisions
960 .sort_by(|left, right| left.id.cmp(&right.id));
961 self.manifest
962 .points
963 .sort_by(|left, right| left.id.cmp(&right.id));
964 self.manifest
965 .branches
966 .sort_by(|left, right| left.id.cmp(&right.id));
967 self.manifest.limitations.sort_by(|left, right| {
968 left.get("id")
969 .and_then(|value| value.as_str())
970 .cmp(&right.get("id").and_then(|value| value.as_str()))
971 });
972 Ok(self.manifest)
973 }
974}
975
976pub const FAILED_TRANSFORM_DUMP_ENV: &str = "SUPERCOV_RUST_DUMP_FAILED_INSTRUMENTATION";
979
980const EXPRESSION_MACROS: &[&str] = &[
989 "assert",
990 "debug_assert",
991 "assert_eq",
992 "assert_ne",
993 "debug_assert_eq",
994 "debug_assert_ne",
995 "println",
996 "print",
997 "eprintln",
998 "eprint",
999 "format",
1000 "format_args",
1001 "write",
1002 "writeln",
1003 "panic",
1004 "unreachable",
1005 "todo",
1006 "unimplemented",
1007 "vec",
1008 "dbg",
1009 "matches",
1010];
1011
1012const ASSERTION_MACROS: &[&str] = &["assert", "debug_assert"];
1014
1015const ASSERTION_STATEMENT_MACROS: &[&str] = &[
1018 "assert",
1019 "assert_eq",
1020 "assert_ne",
1021 "debug_assert",
1022 "debug_assert_eq",
1023 "debug_assert_ne",
1024];
1025
1026struct ExpressionView {
1030 text: String,
1031 assertions: Vec<TextRange>,
1032 assertion_calls: Vec<TextRange>,
1035 matches: Vec<TextRange>,
1037}
1038
1039const EDITIONS: [Edition; 4] = [
1045 Edition::Edition2024,
1046 Edition::Edition2021,
1047 Edition::Edition2018,
1048 Edition::Edition2015,
1049];
1050
1051fn parse_any_edition(source: &str) -> Result<(SourceFile, Edition), Vec<String>> {
1052 let mut newest_errors = None;
1053 for edition in EDITIONS {
1054 let parsed = SourceFile::parse(source, edition);
1055 let errors = parsed.errors();
1056 if errors.is_empty() {
1057 return Ok((parsed.tree(), edition));
1058 }
1059 newest_errors.get_or_insert_with(|| {
1060 errors
1061 .into_iter()
1062 .map(|error| error.to_string())
1063 .collect::<Vec<_>>()
1064 });
1065 }
1066 Err(newest_errors.unwrap_or_default())
1067}
1068
1069fn expression_view(source: &str, edition: Edition) -> ExpressionView {
1070 let mut text = source.to_owned();
1071 let mut assertions = Vec::new();
1072 let mut assertion_calls = Vec::new();
1073 let mut matches = Vec::new();
1074 for _ in 0..16 {
1077 let tree = SourceFile::parse(&text, edition).tree();
1078 let Some(next) = rewrite_expression_macros(
1079 &text,
1080 &tree,
1081 edition,
1082 &mut assertions,
1083 &mut assertion_calls,
1084 &mut matches,
1085 ) else {
1086 break;
1087 };
1088 text = next;
1089 }
1090 ExpressionView {
1091 text,
1092 assertions,
1093 assertion_calls,
1094 matches,
1095 }
1096}
1097
1098fn is_statement_macro(call: &ast::MacroCall) -> bool {
1101 call.syntax().parent().is_some_and(|parent| {
1102 ast::MacroExpr::can_cast(parent.kind())
1103 && parent.parent().is_some_and(|grandparent| {
1104 ast::ExprStmt::can_cast(grandparent.kind())
1105 || ast::StmtList::can_cast(grandparent.kind())
1106 })
1107 })
1108}
1109
1110fn first_top_level_comma_end(arguments: &ast::TokenTree) -> Option<usize> {
1112 arguments
1113 .syntax()
1114 .children_with_tokens()
1115 .filter_map(|element| element.into_token())
1116 .find(|token| token.kind() == SyntaxKind::COMMA)
1117 .map(|token| usize::from(token.text_range().end()))
1118}
1119
1120fn rewrite_expression_macros(
1123 source: &str,
1124 tree: &SourceFile,
1125 edition: Edition,
1126 assertions: &mut Vec<TextRange>,
1127 assertion_calls: &mut Vec<TextRange>,
1128 matches: &mut Vec<TextRange>,
1129) -> Option<String> {
1130 let mut text = source.as_bytes().to_vec();
1131 let mut changed = false;
1132 for call in tree.syntax().descendants().filter_map(ast::MacroCall::cast) {
1133 let Some(name) = call
1134 .path()
1135 .and_then(|path| path.segment())
1136 .and_then(|segment| segment.name_ref())
1137 .map(|name| name.text().to_string())
1138 else {
1139 continue;
1140 };
1141 if !EXPRESSION_MACROS.contains(&name.as_str()) {
1142 continue;
1143 }
1144 let (Some(bang), Some(arguments)) = (call.excl_token(), call.token_tree()) else {
1145 continue;
1146 };
1147 let parenthesised = arguments.l_paren_token().is_some();
1148 if !parenthesised && arguments.l_brack_token().is_none() {
1149 continue;
1150 }
1151 let range = arguments.syntax().text_range();
1152 let (start, end) = (usize::from(range.start()), usize::from(range.end()));
1153 let mut rewritten = source.as_bytes()[start..end].to_vec();
1154 if !parenthesised {
1155 rewritten[0] = b'(';
1156 *rewritten
1157 .last_mut()
1158 .expect("a token tree has a closing delimiter") = b')';
1159 }
1160 if name == "matches" {
1161 let Some(comma_end) = first_top_level_comma_end(&arguments) else {
1164 continue;
1165 };
1166 for byte in &mut rewritten[comma_end - start..end - start - 1] {
1167 if *byte != b'\n' {
1168 *byte = b' ';
1169 }
1170 }
1171 }
1172 let probe = format!(
1174 "fn __supercov() {{ let _ = __f{}; }}",
1175 String::from_utf8_lossy(&rewritten)
1176 );
1177 if !SourceFile::parse(&probe, edition).errors().is_empty() {
1178 if !parenthesised && !is_statement_macro(&call) {
1182 let array = source.as_bytes()[start..end].to_vec();
1183 let array_probe = format!(
1184 "fn __supercov() {{ let _ = {}; }}",
1185 String::from_utf8_lossy(&array)
1186 );
1187 if SourceFile::parse(&array_probe, edition).errors().is_empty() {
1188 let prefix_start = usize::from(call.syntax().text_range().start());
1189 let prefix_start = call.attrs().last().map_or(prefix_start, |attribute| {
1190 usize::from(attribute.syntax().text_range().end())
1191 });
1192 for byte in &mut text[prefix_start..start] {
1193 if *byte != b'\n' {
1194 *byte = b' ';
1195 }
1196 }
1197 changed = true;
1198 }
1199 }
1200 continue;
1201 }
1202 text[usize::from(bang.text_range().start())] = b'_';
1203 text[start..end].copy_from_slice(&rewritten);
1204 changed = true;
1205 if ASSERTION_MACROS.contains(&name.as_str()) {
1206 assertions.push(range);
1207 }
1208 if ASSERTION_STATEMENT_MACROS.contains(&name.as_str()) {
1209 assertion_calls.push(call.syntax().text_range());
1210 }
1211 if name == "matches" {
1212 matches.push(call.syntax().text_range());
1213 }
1214 }
1215 changed.then(|| String::from_utf8(text).expect("rewriting ASCII keeps the source UTF-8"))
1216}
1217
1218fn assertion_argument_count(root: &ra_ap_syntax::SyntaxNode, arguments: TextRange) -> usize {
1221 root.descendants()
1222 .find(|node| node.text_range() == arguments && ast::ArgList::can_cast(node.kind()))
1223 .and_then(ast::ArgList::cast)
1224 .map_or(0, |list| list.args().count())
1225}
1226
1227fn assertion_condition(root: &ra_ap_syntax::SyntaxNode, arguments: TextRange) -> Option<ast::Expr> {
1230 root.descendants()
1231 .find(|node| node.text_range() == arguments && ast::ArgList::can_cast(node.kind()))
1232 .and_then(ast::ArgList::cast)?
1233 .args()
1234 .next()
1235}
1236
1237struct ParsedSource {
1242 tree: SourceFile,
1243 assertions: Vec<TextRange>,
1244 assertion_calls: Vec<TextRange>,
1245 matches: Vec<TextRange>,
1246 edition: Edition,
1247}
1248
1249fn parse_for_instrumentation(source: &str) -> Result<ParsedSource, RustInstrumenterError> {
1250 if source.len() > u32::MAX as usize {
1251 return Err(RustInstrumenterError::SourceTooLarge);
1252 }
1253 let (tree, edition) = parse_any_edition(source).map_err(RustInstrumenterError::Parse)?;
1254 let view = expression_view(source, edition);
1255 let parsed_view = SourceFile::parse(&view.text, edition);
1256 if parsed_view.errors().is_empty() {
1257 Ok(ParsedSource {
1258 tree: parsed_view.tree(),
1259 assertions: view.assertions,
1260 assertion_calls: view.assertion_calls,
1261 matches: view.matches,
1262 edition,
1263 })
1264 } else {
1265 Ok(ParsedSource {
1266 tree,
1267 assertions: Vec::new(),
1268 assertion_calls: Vec::new(),
1269 matches: Vec::new(),
1270 edition,
1271 })
1272 }
1273}
1274
1275fn standalone_matches(
1279 root: &ra_ap_syntax::SyntaxNode,
1280 matches: &[TextRange],
1281 assertions: &[TextRange],
1282) -> Vec<ast::Expr> {
1283 let mut atoms = Vec::new();
1284 let mut conditions = Vec::new();
1285 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
1286 conditions.extend(expression.condition());
1287 }
1288 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
1289 conditions.extend(expression.condition());
1290 }
1291 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
1292 conditions.extend(guard.condition());
1293 }
1294 for arguments in assertions {
1295 conditions.extend(assertion_condition(root, *arguments));
1296 }
1297 for condition in &conditions {
1298 RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
1299 }
1300 matches
1301 .iter()
1302 .filter(|range| !atoms.contains(range))
1303 .filter_map(|range| {
1304 root.descendants()
1305 .find(|node| node.text_range() == *range && ast::CallExpr::can_cast(node.kind()))
1306 .and_then(ast::Expr::cast)
1307 })
1308 .collect()
1309}
1310
1311#[derive(Debug, Clone, Default, PartialEq)]
1317pub struct TargetCfg {
1318 names: BTreeSet<String>,
1319 pairs: BTreeSet<(String, String)>,
1320}
1321
1322impl TargetCfg {
1323 pub fn new(values: &[cargo_platform::Cfg]) -> Self {
1324 let mut cfg = Self::default();
1325 for value in values {
1326 match value {
1327 cargo_platform::Cfg::Name(name) => {
1328 cfg.names.insert(name.name.clone());
1329 }
1330 cargo_platform::Cfg::KeyPair(key, value) => {
1331 cfg.pairs.insert((key.name.clone(), value.clone()));
1332 }
1333 }
1334 }
1335 cfg
1336 }
1337
1338 fn decides(key: &str) -> bool {
1340 matches!(key, "unix" | "windows" | "panic") || key.starts_with("target_")
1341 }
1342
1343 fn evaluate(&self, expression: &cargo_platform::CfgExpr) -> Option<bool> {
1344 use cargo_platform::{Cfg, CfgExpr};
1345 match expression {
1346 CfgExpr::True => Some(true),
1347 CfgExpr::False => Some(false),
1348 CfgExpr::Not(inner) => self.evaluate(inner).map(|value| !value),
1349 CfgExpr::All(items) => {
1350 let values = items
1351 .iter()
1352 .map(|item| self.evaluate(item))
1353 .collect::<Vec<_>>();
1354 if values.contains(&Some(false)) {
1355 Some(false)
1356 } else {
1357 values.iter().all(Option::is_some).then_some(true)
1358 }
1359 }
1360 CfgExpr::Any(items) => {
1361 let values = items
1362 .iter()
1363 .map(|item| self.evaluate(item))
1364 .collect::<Vec<_>>();
1365 if values.contains(&Some(true)) {
1366 Some(true)
1367 } else {
1368 values.iter().all(Option::is_some).then_some(false)
1369 }
1370 }
1371 CfgExpr::Value(Cfg::Name(name)) => {
1372 Self::decides(&name.name).then(|| self.names.contains(&name.name))
1373 }
1374 CfgExpr::Value(Cfg::KeyPair(key, value)) => Self::decides(&key.name)
1375 .then(|| self.pairs.contains(&(key.name.clone(), value.clone()))),
1376 }
1377 }
1378
1379 fn compiles_out(&self, node: &ra_ap_syntax::SyntaxNode) -> bool {
1381 item_attributes(node).iter().any(|text| {
1382 text.strip_prefix("cfg(")
1383 .and_then(|inner| inner.strip_suffix(')'))
1384 .and_then(|inner| inner.parse::<cargo_platform::CfgExpr>().ok())
1385 .is_some_and(|expression| self.evaluate(&expression) == Some(false))
1386 })
1387 }
1388}
1389
1390fn item_attributes(node: &ra_ap_syntax::SyntaxNode) -> Vec<String> {
1393 let attributes = if let Some(item) = ast::Item::cast(node.clone()) {
1394 item.attrs().collect::<Vec<_>>()
1395 } else if let Some(item) = ast::AssocItem::cast(node.clone()) {
1396 item.attrs().collect::<Vec<_>>()
1397 } else {
1398 return Vec::new();
1399 };
1400 attributes
1401 .iter()
1402 .filter_map(|attribute| attribute.meta())
1403 .map(|meta| {
1404 meta.syntax()
1405 .text()
1406 .to_string()
1407 .chars()
1408 .filter(|character| !character.is_whitespace())
1409 .collect()
1410 })
1411 .collect()
1412}
1413
1414pub fn test_code_path(file: &str) -> bool {
1418 let mut parts = file.split(['/', '\\']).collect::<Vec<_>>();
1419 let name = parts.pop().unwrap_or_default();
1420 parts
1421 .iter()
1422 .any(|directory| matches!(*directory, "tests" | "examples" | "benches"))
1423 || name == "tests.rs"
1424 || name
1425 .strip_suffix("tests.rs")
1426 .and_then(|stem| stem.strip_suffix(['_', '-']))
1427 .is_some_and(|stem| {
1428 !stem.is_empty()
1429 && stem.chars().all(|character| {
1430 character.is_ascii_alphanumeric() || "_-".contains(character)
1431 })
1432 })
1433}
1434
1435pub(crate) fn test_item(node: &ra_ap_syntax::SyntaxNode) -> bool {
1438 item_attributes(node).iter().any(|text| {
1439 let path = text
1440 .chars()
1441 .take_while(|character| character.is_alphanumeric() || "_:".contains(*character))
1442 .collect::<String>();
1443 matches!(path.rsplit("::").next(), Some("test" | "bench"))
1444 || text == "cfg(test)"
1445 || text.starts_with("cfg(all(test,")
1446 || text.starts_with("cfg(all(test)")
1447 })
1448}
1449
1450fn compiled_out_ranges(root: &ra_ap_syntax::SyntaxNode, cfg: Option<&TargetCfg>) -> Vec<TextRange> {
1452 let Some(cfg) = cfg else {
1453 return Vec::new();
1454 };
1455 root.descendants()
1456 .filter(|node| cfg.compiles_out(node))
1457 .map(|node| node.text_range())
1458 .collect()
1459}
1460
1461fn test_code_ranges(root: &ra_ap_syntax::SyntaxNode, test_file: bool) -> Vec<TextRange> {
1463 if test_file {
1464 return vec![root.text_range()];
1465 }
1466 root.descendants()
1467 .filter(test_item)
1468 .map(|node| node.text_range())
1469 .collect()
1470}
1471
1472fn in_test_code(tests: &[TextRange], range: TextRange) -> bool {
1473 tests.iter().any(|test| test.contains_range(range))
1474}
1475
1476pub fn build_rust_manifest(
1477 file: &str,
1478 source: &str,
1479) -> Result<CoverageManifest, RustInstrumenterError> {
1480 let parsed = parse_for_instrumentation(source)?;
1481 let tests = test_code_ranges(parsed.tree.syntax(), test_code_path(file));
1482 RustObligationCollector::new(file, source)
1483 .leaving_out(tests, Vec::new())
1484 .collect(&parsed.tree, &parsed.assertions, &parsed.matches)
1485}
1486
1487fn before_its_line(root: &ra_ap_syntax::SyntaxNode, source: &str, offset: usize) -> usize {
1498 let line = source[..offset].rfind('\n').map_or(0, |at| at + 1);
1499 if line == 0 || !source[line..offset].chars().all(char::is_whitespace) {
1500 return offset;
1501 }
1502 let Ok(at) = u32::try_from(offset) else {
1503 return offset;
1504 };
1505 let mut before = root
1506 .token_at_offset(ra_ap_syntax::TextSize::from(at))
1507 .right_biased()
1508 .and_then(|token| token.prev_token());
1509 while let Some(token) = &before {
1510 if !token.kind().is_trivia() {
1511 return usize::from(token.text_range().end());
1512 }
1513 before = token.prev_token();
1514 }
1515 offset
1516}
1517
1518fn block_entry_offset(block: &ast::BlockExpr) -> Option<usize> {
1519 let list = block.stmt_list()?;
1520 list.attrs()
1521 .last()
1522 .map(|attribute| usize::from(attribute.syntax().text_range().end()))
1523 .or_else(|| {
1524 list.l_curly_token()
1525 .map(|token| usize::from(token.text_range().end()))
1526 })
1527}
1528
1529fn item_range(node: &ra_ap_syntax::SyntaxNode) -> TextRange {
1534 let range = node.text_range();
1535 node.children_with_tokens()
1536 .find(|element| match element {
1537 ra_ap_syntax::NodeOrToken::Token(token) => !token.kind().is_trivia(),
1538 ra_ap_syntax::NodeOrToken::Node(child) => !ast::Attr::can_cast(child.kind()),
1539 })
1540 .map_or(range, |element| {
1541 TextRange::new(element.text_range().start(), range.end())
1542 })
1543}
1544
1545fn range_after_attributes(node: &impl HasAttrs) -> TextRange {
1546 let range = node.syntax().text_range();
1547 node.attrs().last().map_or(range, |attribute| {
1548 TextRange::new(attribute.syntax().text_range().end(), range.end())
1549 })
1550}
1551
1552fn has_let(expression: &ast::Expr) -> bool {
1553 expression
1554 .syntax()
1555 .descendants()
1556 .any(|node| ast::LetExpr::can_cast(node.kind()))
1557}
1558
1559enum ChainHost<'a> {
1561 If(&'a ast::IfExpr),
1562 While(&'a ast::WhileExpr),
1563}
1564
1565fn allocate_chain_table_name(
1567 file: &str,
1568 condition: &ast::Expr,
1569 identifiers: &mut BTreeSet<String>,
1570) -> String {
1571 let id = stable_id(file, "chain", condition.syntax().text_range(), "operators");
1572 let suffix = id
1573 .rsplit(':')
1574 .next()
1575 .unwrap_or("chain")
1576 .to_ascii_uppercase();
1577 let base = format!("__SUPERCOV_CHAIN_{suffix}");
1578 let mut candidate = base.clone();
1579 let mut attempt = 0_usize;
1580 while !identifiers.insert(candidate.clone()) {
1581 attempt += 1;
1582 candidate = format!("{base}_{attempt}");
1583 }
1584 candidate
1585}
1586
1587fn allocate_identifier(
1589 file: &str,
1590 range: TextRange,
1591 kind: &str,
1592 identifiers: &mut BTreeSet<String>,
1593) -> String {
1594 let id = stable_id(file, kind, range, "");
1595 let suffix = id.rsplit(':').next().unwrap_or(kind);
1596 let base = format!("__supercov_{kind}_{suffix}");
1597 let mut candidate = base.clone();
1598 let mut attempt = 0_usize;
1599 while !identifiers.insert(candidate.clone()) {
1600 attempt += 1;
1601 candidate = format!("{base}_{attempt}");
1602 }
1603 candidate
1604}
1605
1606fn own_breaks(body: &ast::BlockExpr, label: Option<ast::Label>) -> Vec<TextRange> {
1610 let own_label = label
1611 .and_then(|label| label.lifetime())
1612 .map(|lifetime| lifetime.text().to_string());
1613 body.syntax()
1614 .descendants()
1615 .filter_map(ast::BreakExpr::cast)
1616 .filter(|expression| match expression.lifetime() {
1617 Some(lifetime) => own_label.as_deref() == Some(lifetime.text().to_string().as_str()),
1618 None => !expression
1619 .syntax()
1620 .ancestors()
1621 .skip(1)
1622 .take_while(|ancestor| ancestor != body.syntax())
1623 .any(|ancestor| {
1624 ast::LoopExpr::can_cast(ancestor.kind())
1625 || ast::WhileExpr::can_cast(ancestor.kind())
1626 || ast::ForExpr::can_cast(ancestor.kind())
1627 || ast::ClosureExpr::can_cast(ancestor.kind())
1628 }),
1629 })
1630 .map(|expression| expression.syntax().text_range())
1631 .collect()
1632}
1633
1634fn instrument_let_chain(
1650 insertions: &mut Vec<Insertion>,
1651 runtime_path: &str,
1652 file: &str,
1653 condition: &ast::Expr,
1654 host: ChainHost<'_>,
1655 identifiers: &mut BTreeSet<String>,
1656) {
1657 let (kind, host_range, body, label) = match &host {
1660 ChainHost::If(expression) => (
1661 "if",
1662 range_after_attributes(*expression),
1663 expression.then_branch(),
1664 None,
1665 ),
1666 ChainHost::While(expression) => (
1667 "while",
1668 range_after_attributes(*expression),
1669 expression.loop_body(),
1670 expression.label(),
1671 ),
1672 };
1673 let Some(body) = body else {
1674 return;
1675 };
1676 let Some(body_offset) = block_entry_offset(&body) else {
1677 return;
1678 };
1679 let range = condition.syntax().text_range();
1680 let id = stable_id(file, "decision", range, kind);
1681 let mut atoms = Vec::new();
1682 RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
1683 let lets = condition
1684 .syntax()
1685 .descendants()
1686 .filter_map(ast::LetExpr::cast)
1687 .map(|expression| expression.syntax().text_range())
1688 .collect::<Vec<_>>();
1689 let frame = allocate_frame_name(file, condition, kind, identifiers);
1690 let table = allocate_chain_table_name(file, condition, identifiers);
1691 let single_let = atoms.len() == 1;
1692 let broke = match &host {
1693 ChainHost::While(_) if single_let => {
1694 Some(allocate_identifier(file, range, "broke", identifiers))
1695 }
1696 _ => None,
1697 };
1698
1699 let mut operators = Vec::new();
1700 for binary in condition
1701 .syntax()
1702 .descendants()
1703 .filter_map(ast::BinExpr::cast)
1704 {
1705 if !matches!(binary.op_kind(), Some(BinaryOp::LogicOp(LogicOp::And))) {
1708 continue;
1709 }
1710 let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
1711 continue;
1712 };
1713 if !has_let(&left) {
1714 continue;
1715 }
1716 let branch = stable_id(file, "branch", right.syntax().text_range(), "logical-and");
1717 let right_range = right.syntax().text_range();
1718 let Some(first) = atoms
1719 .iter()
1720 .position(|atom| right_range.contains_range(*atom))
1721 else {
1722 continue;
1723 };
1724 operators.push(format!(
1725 "({first}, {:?}, {:?})",
1726 format!("{branch}:short-circuit"),
1727 format!("{branch}:evaluated")
1728 ));
1729 }
1730
1731 let mark = format!("{runtime_path}::reached(&mut {frame}, 0);");
1732 let record_false = format!("{runtime_path}::decision_chain(&mut {frame}, false, {table});");
1733 let mut prefix = format!(
1734 "{{ const {table}: &[(usize, &str, &str)] = &[{}]; let mut {frame} = {runtime_path}::DecisionFrame::new({id:?}, {}); ",
1735 operators.join(", "),
1736 atoms.len()
1737 );
1738 if single_let {
1739 prefix.push_str(&mark);
1740 prefix.push(' ');
1741 }
1742 if let Some(broke) = &broke {
1743 prefix.push_str(&format!("let mut {broke} = false; "));
1744 }
1745 let suffix = match &host {
1746 ChainHost::If(expression) => match expression.else_branch() {
1747 Some(_) => " }".to_owned(),
1748 None => format!(" else {{ {record_false} }} }}"),
1749 },
1750 ChainHost::While(_) => match &broke {
1751 Some(broke) => format!(" if !{broke} {{ {mark} {record_false} }} }}"),
1752 None => format!(" {record_false} }}"),
1753 },
1754 };
1755 push_wrapper(insertions, host_range, host_range, 1, prefix, suffix);
1756 if !single_let {
1757 push_direct(
1758 insertions,
1759 usize::from(range.start()),
1760 format!("{runtime_path}::reached(&mut {frame}, 0) && "),
1761 );
1762 }
1763 for (index, atom) in atoms.iter().enumerate() {
1764 if lets.contains(atom) {
1765 if index > 0 {
1766 push_direct(
1767 insertions,
1768 usize::from(atom.start()),
1769 format!("{runtime_path}::reached(&mut {frame}, {index}) && "),
1770 );
1771 }
1772 } else {
1773 push_wrapper(
1774 insertions,
1775 *atom,
1776 *atom,
1777 1,
1778 format!("{runtime_path}::condition(("),
1779 format!("), &mut {frame}, {index})"),
1780 );
1781 }
1782 }
1783 let mut entry = String::new();
1784 if broke.is_some() {
1785 entry.push_str(&format!(" {mark}"));
1786 }
1787 entry.push_str(&format!(
1788 " {runtime_path}::decision_chain(&mut {frame}, true, {table});"
1789 ));
1790 push_direct(insertions, body_offset, entry);
1791 if let Some(broke) = &broke {
1792 for break_range in own_breaks(&body, label) {
1793 push_wrapper(
1794 insertions,
1795 break_range,
1796 break_range,
1797 0,
1798 format!("{{ {broke} = true; "),
1799 " }".into(),
1800 );
1801 }
1802 }
1803 if let ChainHost::If(expression) = &host {
1804 match expression.else_branch() {
1805 Some(ast::ElseBranch::Block(block)) => {
1806 if let Some(offset) = block_entry_offset(&block) {
1807 push_direct(insertions, offset, format!(" {record_false}"));
1808 }
1809 }
1810 Some(ast::ElseBranch::IfExpr(nested)) => {
1811 let nested_range = nested.syntax().text_range();
1812 push_wrapper(
1813 insertions,
1814 nested_range,
1815 nested_range,
1816 0,
1817 format!("{{ {record_false} "),
1818 " }".into(),
1819 );
1820 }
1821 None => {}
1822 }
1823 }
1824}
1825
1826fn enclosing_block_entry(node: &ra_ap_syntax::SyntaxNode) -> Option<usize> {
1830 node.ancestors()
1831 .skip(1)
1832 .find_map(ast::BlockExpr::cast)
1833 .and_then(|block| block_entry_offset(&block))
1834}
1835
1836fn plain_block(block: &ast::BlockExpr) -> bool {
1840 block
1841 .syntax()
1842 .first_token()
1843 .is_some_and(|token| token.kind() == SyntaxKind::L_CURLY)
1844}
1845
1846fn instrument_decision(
1847 insertions: &mut Vec<Insertion>,
1848 runtime_path: &str,
1849 file: &str,
1850 condition: &ast::Expr,
1851 kind: &str,
1852 frame_name: &str,
1853) -> bool {
1854 if cannot_carry_probe(condition.syntax())
1855 || condition
1856 .syntax()
1857 .descendants()
1858 .any(|node| ast::LetExpr::can_cast(node.kind()))
1859 {
1860 return false;
1861 }
1862 let range = condition.syntax().text_range();
1863 let id = stable_id(file, "decision", range, kind);
1864 let mut condition_ranges = Vec::new();
1865 RustObligationCollector::atomic_condition_ranges(condition, &mut condition_ranges);
1866 push_wrapper(
1867 insertions,
1868 range,
1869 range,
1870 0,
1871 format!(
1872 "({{ let mut {frame_name} = {runtime_path}::DecisionFrame::new({id:?}, {}); {runtime_path}::decision((",
1873 condition_ranges.len()
1874 ),
1875 format!("), &mut {frame_name}) }})"),
1876 );
1877 for (index, atomic_range) in condition_ranges.into_iter().enumerate() {
1881 push_wrapper(
1882 insertions,
1883 atomic_range,
1884 atomic_range,
1885 1,
1886 format!("{runtime_path}::condition(("),
1887 format!("), &mut {frame_name}, {index})"),
1888 );
1889 }
1890 true
1891}
1892
1893pub fn instrument_rust_source(
1903 file: &str,
1904 source: &str,
1905 runtime_path: &str,
1906) -> Result<RustInstrumentedSource, RustInstrumenterError> {
1907 instrument_rust_file(file, source, runtime_path, test_code_path(file), None)
1908}
1909
1910pub fn instrument_rust_file(
1915 file: &str,
1916 source: &str,
1917 runtime_path: &str,
1918 test_file: bool,
1919 cfg: Option<&TargetCfg>,
1920) -> Result<RustInstrumentedSource, RustInstrumenterError> {
1921 if !valid_runtime_path(runtime_path) {
1922 return Err(RustInstrumenterError::InvalidRuntimePath);
1923 }
1924 let parsed = parse_for_instrumentation(source)?;
1930 let tests = test_code_ranges(parsed.tree.syntax(), test_file);
1931 let compiled_out = compiled_out_ranges(parsed.tree.syntax(), cfg);
1932 let mut manifest = RustObligationCollector::new(file, source)
1933 .leaving_out(tests.clone(), compiled_out.clone())
1934 .collect(&parsed.tree, &parsed.assertions, &parsed.matches)?;
1935 let ParsedSource {
1936 tree,
1937 assertions,
1938 assertion_calls,
1939 matches,
1940 edition,
1941 } = parsed;
1942 let root = tree.syntax();
1943 let mut insertions = Vec::new();
1944 let mut identifiers = root
1945 .descendants_with_tokens()
1946 .filter_map(|element| element.into_token())
1947 .filter(|token| token.kind() == SyntaxKind::IDENT)
1948 .map(|token| token.text().to_string())
1949 .collect::<BTreeSet<_>>();
1950
1951 let mut skipped_attributed_statement = false;
1952 let attributed_probe = |insertions: &mut Vec<Insertion>,
1963 skipped: &mut bool,
1964 expression: Option<ast::Expr>,
1965 has_attrs: bool,
1966 statement: bool,
1970 trailing: bool,
1971 range: TextRange,
1972 id: String| {
1973 if !has_attrs {
1974 push_direct(
1975 insertions,
1976 before_its_line(root, source, usize::from(range.start())),
1977 format!("{runtime_path}::hit({id:?});"),
1978 );
1979 return;
1980 }
1981 let Some(expression) = expression else {
1982 *skipped = true;
1983 return;
1984 };
1985 if let ast::Expr::BlockExpr(block) = &expression
1986 && let Some(offset) = block_entry_offset(block)
1987 {
1988 push_direct(insertions, offset, format!("{runtime_path}::hit({id:?});"));
1989 return;
1990 }
1991 if trailing && let ast::Expr::MacroExpr(_) = &expression {
2002 if expression.attrs().any(|attribute| {
2003 attribute.meta().is_some_and(|meta| {
2006 let text = meta.syntax().text().to_string();
2007 let name = text
2008 .chars()
2009 .take_while(|character| character.is_alphanumeric() || *character == '_')
2010 .collect::<String>();
2011 matches!(name.as_str(), "cfg" | "cfg_attr")
2012 })
2013 }) {
2014 *skipped = true;
2015 return;
2016 }
2017 let range = expression.syntax().text_range();
2018 push_wrapper(
2019 insertions,
2020 range,
2021 range,
2022 0,
2023 format!("{{ {runtime_path}::hit({id:?}); "),
2024 " }".into(),
2025 );
2026 return;
2027 }
2028 if statement && let ast::Expr::MacroExpr(_) = &expression {
2035 let start = expression.attrs().last().map_or_else(
2036 || expression.syntax().text_range().start(),
2037 |attribute| attribute.syntax().text_range().end(),
2038 );
2039 let wrapped = TextRange::new(start, expression.syntax().text_range().end());
2040 push_wrapper(
2041 insertions,
2042 wrapped,
2043 wrapped,
2044 0,
2045 format!(" {{ {runtime_path}::hit({id:?}); "),
2046 "; }".into(),
2047 );
2048 return;
2049 }
2050 let start = expression.attrs().last().map_or_else(
2056 || expression.syntax().text_range().start(),
2057 |attribute| attribute.syntax().text_range().end(),
2058 );
2059 let wrapped = TextRange::new(start, expression.syntax().text_range().end());
2060 push_wrapper(
2061 insertions,
2062 wrapped,
2063 wrapped,
2064 0,
2065 format!(" {{ {runtime_path}::hit({id:?}); ("),
2066 ") }".into(),
2067 );
2068 };
2069 for list in root.descendants().filter_map(ast::StmtList::cast) {
2070 let last_statement = list.statements().last();
2071 for statement in list.statements() {
2072 let (range, expression, has_attrs, statement_expression, trailing) = match &statement {
2073 ast::Stmt::ExprStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
2074 let expression = statement.expr();
2075 let has_attrs = expression
2078 .as_ref()
2079 .is_some_and(|expression| expression.attrs().next().is_some());
2080 let trailing = statement.semicolon_token().is_none()
2083 && list.tail_expr().is_none()
2084 && last_statement.as_ref() == Some(&ast::Stmt::ExprStmt(statement.clone()));
2085 (
2086 statement.syntax().text_range(),
2087 expression,
2088 has_attrs,
2089 true,
2090 trailing,
2091 )
2092 }
2093 ast::Stmt::LetStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
2094 let has_attrs = statement.attrs().next().is_some();
2095 let initializer = has_attrs.then(|| statement.initializer()).flatten();
2098 (
2099 statement.syntax().text_range(),
2100 initializer,
2101 has_attrs,
2102 false,
2103 false,
2104 )
2105 }
2106 _ => continue,
2107 };
2108 let id = stable_id(file, "statement", range, "");
2109 if !has_attrs
2116 && !trailing
2117 && assertion_calls
2118 .iter()
2119 .any(|call| range.contains_range(*call))
2120 {
2121 push_direct(
2122 &mut insertions,
2123 usize::from(range.end()),
2124 format!("{runtime_path}::assertion({id:?});"),
2125 );
2126 }
2127 attributed_probe(
2128 &mut insertions,
2129 &mut skipped_attributed_statement,
2130 expression,
2131 has_attrs,
2132 statement_expression,
2133 trailing,
2134 range,
2135 id,
2136 );
2137 }
2138 if let Some(tail) = list
2139 .tail_expr()
2140 .filter(|tail| !cannot_carry_probe(tail.syntax()))
2141 {
2142 let range = tail.syntax().text_range();
2143 let id = stable_id(file, "statement", range, "");
2144 let has_attrs = tail.attrs().next().is_some();
2145 attributed_probe(
2146 &mut insertions,
2147 &mut skipped_attributed_statement,
2148 Some(tail),
2149 has_attrs,
2150 true,
2151 true,
2152 range,
2153 id,
2154 );
2155 }
2156 }
2157
2158 for function in root.descendants().filter_map(ast::Fn::cast) {
2159 if cannot_carry_probe(function.syntax()) {
2162 continue;
2163 }
2164 let Some(body) = function.body() else {
2165 continue;
2166 };
2167 let label = function.name().map(|name| name.text().to_string());
2168 let id = stable_id(
2169 file,
2170 "function",
2171 function.syntax().text_range(),
2172 label.as_deref().unwrap_or(""),
2173 );
2174 if let Some(offset) = block_entry_offset(&body) {
2175 push_direct(
2176 &mut insertions,
2177 offset,
2178 format!("{runtime_path}::hit({id:?});"),
2179 );
2180 }
2181 }
2182
2183 for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
2184 let Some(body) = closure.body() else {
2185 continue;
2186 };
2187 if cannot_carry_probe(body.syntax()) {
2188 continue;
2189 }
2190 let id = stable_id(file, "function", closure.syntax().text_range(), "<closure>");
2191 if let ast::Expr::BlockExpr(block) = &body {
2192 if let Some(offset) = block_entry_offset(block) {
2193 push_direct(
2194 &mut insertions,
2195 offset,
2196 format!("{runtime_path}::hit({id:?});"),
2197 );
2198 }
2199 } else {
2200 let range = body.syntax().text_range();
2201 push_wrapper(
2202 &mut insertions,
2203 range,
2204 closure.syntax().text_range(),
2205 0,
2206 format!("{{ {runtime_path}::hit({id:?}); ("),
2207 ") }".into(),
2208 );
2209 }
2210 }
2211
2212 for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
2219 if cannot_carry_probe(expression.syntax()) {
2220 continue;
2221 }
2222 let Some(list) = expression.match_arm_list() else {
2223 continue;
2224 };
2225 let arms = list.arms().collect::<Vec<_>>();
2226 if arms.is_empty() {
2227 continue;
2228 }
2229 let Some(table_offset) = enclosing_block_entry(expression.syntax()) else {
2230 continue;
2231 };
2232 let table = allocate_table_name(file, &expression, &mut identifiers);
2233 let entries = arms
2234 .iter()
2235 .map(|arm| {
2236 let id = stable_id(file, "branch", arm.syntax().text_range(), "match-arm");
2237 format!(
2238 "{:?}, {:?}",
2239 format!("{id}:missed"),
2240 format!("{id}:selected")
2241 )
2242 })
2243 .collect::<Vec<_>>()
2244 .join(", ");
2245 push_direct(
2246 &mut insertions,
2247 table_offset,
2248 format!(" const {table}: &[&str] = &[{entries}];"),
2252 );
2253 for (index, arm) in arms.iter().enumerate() {
2254 let Some(body) = arm.expr() else {
2255 continue;
2256 };
2257 let call = format!("{runtime_path}::arms({table}, {index});");
2258 match &body {
2259 ast::Expr::BlockExpr(block) if plain_block(block) => {
2260 if let Some(offset) = block_entry_offset(block) {
2261 push_direct(&mut insertions, offset, format!(" {call}"));
2262 }
2263 }
2264 _ => push_wrapper(
2265 &mut insertions,
2266 body.syntax().text_range(),
2267 arm.syntax().text_range(),
2268 0,
2269 format!("{{ {call} ("),
2270 ") }".into(),
2271 ),
2272 }
2273 }
2274 }
2275
2276 for binary in root.descendants().filter_map(ast::BinExpr::cast) {
2280 let short_circuits_when = match binary.op_kind() {
2281 Some(BinaryOp::LogicOp(LogicOp::And)) => false,
2282 Some(BinaryOp::LogicOp(LogicOp::Or)) => true,
2283 _ => continue,
2284 };
2285 if cannot_carry_probe(binary.syntax()) {
2286 continue;
2287 }
2288 let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
2289 continue;
2290 };
2291 if has_let(&left) {
2295 continue;
2296 }
2297 let kind = if short_circuits_when {
2298 "logical-or"
2299 } else {
2300 "logical-and"
2301 };
2302 let id = stable_id(file, "branch", right.syntax().text_range(), kind);
2303 push_wrapper(
2304 &mut insertions,
2305 left.syntax().text_range(),
2306 binary.syntax().text_range(),
2307 2,
2308 format!("{runtime_path}::logical(("),
2309 format!(
2310 "), {short_circuits_when}, {:?}, {:?})",
2311 format!("{id}:short-circuit"),
2312 format!("{id}:evaluated")
2313 ),
2314 );
2315 }
2316
2317 for expression in root.descendants().filter_map(ast::ForExpr::cast) {
2321 if cannot_carry_probe(expression.syntax()) {
2322 continue;
2323 }
2324 let Some(iterable) = expression.iterable() else {
2325 continue;
2326 };
2327 let id = stable_id(file, "branch", expression.syntax().text_range(), "for-loop");
2328 push_wrapper(
2332 &mut insertions,
2333 iterable.syntax().text_range(),
2334 iterable.syntax().text_range(),
2335 0,
2336 format!("{runtime_path}::for_loop(("),
2337 format!(
2338 "), {:?}, {:?})",
2339 format!("{id}:zero"),
2340 format!("{id}:entered")
2341 ),
2342 );
2343 }
2344
2345 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
2349 if cannot_carry_probe(expression.syntax()) {
2350 continue;
2351 }
2352 let Some(offset) = expression.loop_body().as_ref().and_then(block_entry_offset) else {
2353 continue;
2354 };
2355 let id = stable_id(
2356 file,
2357 "branch",
2358 expression.syntax().text_range(),
2359 "while-loop",
2360 );
2361 let flag = allocate_flag_name(file, &expression, &mut identifiers);
2362 let range = range_after_attributes(&expression);
2363 push_wrapper(
2364 &mut insertions,
2365 range,
2366 range,
2367 0,
2368 format!("{{ let mut {flag} = true; "),
2369 format!(
2370 " {runtime_path}::zero_iterations({flag}, {:?}) }}",
2371 format!("{id}:zero")
2372 ),
2373 );
2374 push_direct(
2375 &mut insertions,
2376 offset,
2377 format!(
2378 " {runtime_path}::entered(&mut {flag}, {:?});",
2379 format!("{id}:entered")
2380 ),
2381 );
2382 }
2383
2384 for expression in root.descendants().filter_map(ast::TryExpr::cast) {
2388 if cannot_carry_probe(expression.syntax()) {
2389 continue;
2390 }
2391 let Some(operand) = expression.expr() else {
2392 continue;
2393 };
2394 let id = stable_id(
2395 file,
2396 "branch",
2397 expression.syntax().text_range(),
2398 "try-operator",
2399 );
2400 push_wrapper(
2403 &mut insertions,
2404 operand.syntax().text_range(),
2405 operand.syntax().text_range(),
2406 0,
2407 format!("{runtime_path}::TryProbe::probe(("),
2408 format!(
2409 "), {:?}, {:?})",
2410 format!("{id}:continued"),
2411 format!("{id}:returned")
2412 ),
2413 );
2414 }
2415
2416 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
2417 let Some(condition) = expression.condition() else {
2418 continue;
2419 };
2420 if has_let(&condition) {
2421 if !cannot_carry_probe(condition.syntax()) {
2422 instrument_let_chain(
2423 &mut insertions,
2424 runtime_path,
2425 file,
2426 &condition,
2427 ChainHost::If(&expression),
2428 &mut identifiers,
2429 );
2430 }
2431 continue;
2432 }
2433 let frame_name = allocate_frame_name(file, &condition, "if", &mut identifiers);
2434 instrument_decision(
2435 &mut insertions,
2436 runtime_path,
2437 file,
2438 &condition,
2439 "if",
2440 &frame_name,
2441 );
2442 }
2443 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
2444 let Some(condition) = expression.condition() else {
2445 continue;
2446 };
2447 if has_let(&condition) {
2448 if !cannot_carry_probe(condition.syntax()) {
2449 instrument_let_chain(
2450 &mut insertions,
2451 runtime_path,
2452 file,
2453 &condition,
2454 ChainHost::While(&expression),
2455 &mut identifiers,
2456 );
2457 }
2458 continue;
2459 }
2460 let frame_name = allocate_frame_name(file, &condition, "while", &mut identifiers);
2461 instrument_decision(
2462 &mut insertions,
2463 runtime_path,
2464 file,
2465 &condition,
2466 "while",
2467 &frame_name,
2468 );
2469 }
2470 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
2471 if let Some(condition) = guard.condition() {
2472 let frame_name = allocate_frame_name(file, &condition, "match-guard", &mut identifiers);
2473 instrument_decision(
2474 &mut insertions,
2475 runtime_path,
2476 file,
2477 &condition,
2478 "match-guard",
2479 &frame_name,
2480 );
2481 }
2482 }
2483 for arguments in &assertions {
2490 let Some(condition) = assertion_condition(root, *arguments) else {
2491 continue;
2492 };
2493 let frame_name = allocate_frame_name(file, &condition, "assert", &mut identifiers);
2494 if !instrument_decision(
2495 &mut insertions,
2496 runtime_path,
2497 file,
2498 &condition,
2499 "assert",
2500 &frame_name,
2501 ) {
2502 continue;
2503 }
2504 if assertion_argument_count(root, *arguments) == 1 {
2505 let range = condition.syntax().text_range();
2506 let original = &source[usize::from(range.start())..usize::from(range.end())];
2507 push_direct(
2508 &mut insertions,
2509 usize::from(range.end()),
2510 format!(", \"assertion failed: {{}}\", stringify!({original})"),
2511 );
2512 }
2513 }
2514 for expression in standalone_matches(root, &matches, &assertions) {
2517 let frame_name = allocate_frame_name(file, &expression, "matches", &mut identifiers);
2518 instrument_decision(
2519 &mut insertions,
2520 runtime_path,
2521 file,
2522 &expression,
2523 "matches",
2524 &frame_name,
2525 );
2526 }
2527
2528 if !compiled_out.is_empty() {
2531 insertions.retain(|insertion| {
2532 !compiled_out.iter().any(|range| {
2533 usize::from(range.start()) <= insertion.offset
2534 && insertion.offset <= usize::from(range.end())
2535 })
2536 });
2537 }
2538 if !tests.is_empty() {
2541 let inside = |offset: usize| {
2542 tests.iter().any(|test| {
2543 usize::from(test.start()) <= offset && offset <= usize::from(test.end())
2544 })
2545 };
2546 let markers = manifest
2547 .assertion_sites
2548 .iter()
2549 .map(|site| {
2550 (
2551 format!("{runtime_path}::assertion({:?});", site.id),
2552 site.id.clone(),
2553 )
2554 })
2555 .collect::<std::collections::BTreeMap<_, _>>();
2556 let mut marked = BTreeSet::new();
2557 insertions.retain(|insertion| {
2558 if !inside(insertion.offset) {
2559 return true;
2560 }
2561 match markers.get(&insertion.text) {
2562 Some(id) => {
2563 marked.insert(id.clone());
2564 true
2565 }
2566 None => false,
2567 }
2568 });
2569 manifest
2570 .assertion_sites
2571 .retain(|site| marked.contains(&site.id));
2572 }
2573
2574 if skipped_attributed_statement && !test_file {
2575 add_manifest_limitation(
2576 &mut manifest,
2577 file,
2578 "rust-attributed-statement-probes-not-injected",
2579 "A `let` without an initializer that carries outer attributes has no expression to hold a probe",
2580 );
2581 }
2582 manifest.limitations.sort_by(|left, right| {
2583 left.get("id")
2584 .and_then(|value| value.as_str())
2585 .cmp(&right.get("id").and_then(|value| value.as_str()))
2586 });
2587
2588 let code = apply_insertions(source, insertions)?;
2589 let transformed = SourceFile::parse(&code, edition);
2590 let errors = transformed
2591 .errors()
2592 .into_iter()
2593 .map(|error| error.to_string())
2594 .collect::<Vec<_>>();
2595 if !errors.is_empty() {
2596 if let Some(directory) = std::env::var_os(FAILED_TRANSFORM_DUMP_ENV) {
2600 let name = file.replace(['/', '\\'], "__");
2601 let _ = std::fs::create_dir_all(&directory);
2602 let _ = std::fs::write(std::path::Path::new(&directory).join(name), &code);
2603 }
2604 return Err(RustInstrumenterError::Parse(errors));
2605 }
2606 Ok(RustInstrumentedSource { code, manifest })
2607}
2608
2609#[cfg(test)]
2610mod tests {
2611 use std::{
2612 fs,
2613 process::Command,
2614 time::{SystemTime, UNIX_EPOCH},
2615 };
2616
2617 use super::*;
2618
2619 fn limitation_kind_of(limitation: &serde_json::Value) -> Option<&str> {
2621 limitation.get("id")?.as_str()?.split('#').next()
2622 }
2623
2624 const NOOP_RUNTIME: &str = r#"
2625#[doc(hidden)]
2626mod __supercov_runtime_v1 {
2627 pub struct DecisionFrame;
2628 impl DecisionFrame {
2629 pub fn new(_: &'static str, _: usize) -> Self { Self }
2630 }
2631 pub fn hit(_: &'static str) {}
2632 pub fn arms(_: &[&'static str], _: usize) {}
2633 pub fn logical(left: bool, _: bool, _: &'static str, _: &'static str) -> bool { left }
2634 pub fn for_loop<I: IntoIterator>(iterable: I, _: &'static str, _: &'static str) -> I::IntoIter {
2635 iterable.into_iter()
2636 }
2637 pub fn entered(_: &mut bool, _: &'static str) {}
2638 pub fn zero_iterations(_: bool, _: &'static str) {}
2639 pub trait TryProbe: Sized {
2640 fn probe(self, _: &'static str, _: &'static str) -> Self { self }
2641 }
2642 impl<T> TryProbe for T {}
2643 pub fn condition<V: std::ops::Not<Output = bool>>(value: V, _: &mut DecisionFrame, _: usize) -> bool { !!value }
2644 pub fn decision(value: bool, _: &mut DecisionFrame) -> bool { value }
2645 pub fn reached(_: &mut DecisionFrame, _: usize) -> bool { true }
2646 pub fn decision_chain(_: &mut DecisionFrame, _: bool, _: &[(usize, &'static str, &'static str)]) {}
2647 pub fn assertion(_: &'static str) {}
2648}
2649"#;
2650
2651 fn compile_and_run(source: &str, name: &str) -> std::process::Output {
2652 compile_and_run_edition(source, name, "2024")
2653 }
2654
2655 fn compile_and_run_edition(source: &str, name: &str, edition: &str) -> std::process::Output {
2656 let nonce = SystemTime::now()
2657 .duration_since(UNIX_EPOCH)
2658 .unwrap()
2659 .as_nanos();
2660 let directory = std::env::temp_dir().join(format!(
2661 "supercov-rust-transform-{}-{nonce}-{name}",
2662 std::process::id()
2663 ));
2664 fs::create_dir(&directory).unwrap();
2665 let input = directory.join("main.rs");
2666 let binary = directory.join("program");
2667 fs::write(&input, source).unwrap();
2668 let compile = Command::new("rustc")
2669 .arg(format!("--edition={edition}"))
2670 .arg(&input)
2671 .arg("-o")
2672 .arg(&binary)
2673 .output()
2674 .unwrap();
2675 assert!(
2676 compile.status.success(),
2677 "rustc failed:\n{}\nsource:\n{source}",
2678 String::from_utf8_lossy(&compile.stderr)
2679 );
2680 let output = Command::new(&binary).output().unwrap();
2681 fs::remove_dir_all(directory).unwrap();
2682 output
2683 }
2684
2685 #[test]
2686 fn a_match_and_a_loop_take_no_line_of_their_own() {
2687 let source = r#"fn kind(key: &str) -> u8 {
2692 match key.len() {
2693 0 => 0,
2694 n if n < 4 => {
2695 1
2696 }
2697 _ => 2,
2698 }
2699}
2700
2701fn scan(key: &str, limit: usize) -> usize {
2702 let mut seen = 0;
2703 while seen < limit && seen < key.len() {
2704 if key.as_bytes()[seen] == b'-' || seen > 8 {
2705 break;
2706 } else {
2707 seen += 1;
2708 }
2709 }
2710 for _ in 0..limit {
2711 seen += 0;
2712 }
2713 seen
2714}
2715"#;
2716 let transformed =
2717 instrument_rust_source("src/scan.rs", source, "crate::__supercov_runtime_v1").unwrap();
2718 assert_eq!(
2719 transformed.code.lines().count(),
2720 source.lines().count(),
2721 "{}",
2722 transformed.code
2723 );
2724 for (written, rewritten) in source.lines().zip(transformed.code.lines()) {
2726 let end = written.trim().trim_end_matches(['{', ';', ',']).trim();
2727 let last = end.rsplit(' ').next().unwrap_or_default();
2728 assert!(rewritten.contains(last), "{written:?} became {rewritten:?}");
2729 }
2730 }
2731
2732 #[test]
2733 fn a_panic_is_reported_where_its_author_wrote_it() {
2734 let source = r#"fn check(key: &str) -> usize {
2740 if key.len() < 4 {
2741 panic!("key too short");
2742 }
2743 key.len()
2744}
2745
2746fn later(values: &[usize]) -> usize {
2747 let first = values[0];
2748 let total: usize = values.iter().sum();
2749 assert!(total > first, "nothing but the first");
2750 total
2751}
2752
2753fn main() {
2754 let mode = std::env::args().nth(1).unwrap_or_default();
2755 if mode == "later" {
2756 later(&[3]);
2757 }
2758 let closure = |key: &str| {
2759 check(key)
2760 };
2761 closure("ab");
2762}
2763"#;
2764 let transformed =
2765 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2766 assert_eq!(
2767 transformed.code.lines().count(),
2768 source.lines().count(),
2769 "{}",
2770 transformed.code
2771 );
2772 for (number, (written, rewritten)) in
2774 source.lines().zip(transformed.code.lines()).enumerate()
2775 {
2776 let code = written.trim_start();
2777 let word = code.split(['(', ' ']).next().unwrap_or(code);
2779 if matches!(word, "panic!" | "assert!" | "let") {
2780 assert_eq!(
2781 rewritten.find(word),
2782 written.find(word),
2783 "line {}: {rewritten}",
2784 number + 1
2785 );
2786 }
2787 }
2788 let location = |output: &std::process::Output| {
2789 String::from_utf8_lossy(&output.stderr)
2790 .lines()
2791 .find(|line| line.contains("panicked at"))
2792 .map(|line| line[line.find("main.rs").unwrap()..].to_owned())
2793 .expect("a panic")
2794 };
2795 let original = compile_and_run(source, "panic-original");
2796 let instrumented = compile_and_run(
2797 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2798 "panic-instrumented",
2799 );
2800 assert_eq!(location(&original), "main.rs:3:9:");
2801 assert_eq!(location(&instrumented), location(&original));
2802 }
2803
2804 #[test]
2805 fn a_target_cfg_decides_only_what_the_target_settles() {
2806 let cfg = TargetCfg::new(
2807 &[
2808 "unix",
2809 "target_os=\"linux\"",
2810 "target_family=\"unix\"",
2811 "target_pointer_width=\"64\"",
2812 "debug_assertions",
2813 ]
2814 .map(|value| value.parse::<cargo_platform::Cfg>().unwrap()),
2815 );
2816 let decide = |expression: &str| cfg.evaluate(&expression.parse().unwrap());
2817 assert_eq!(decide("unix"), Some(true));
2818 assert_eq!(decide("windows"), Some(false));
2819 assert_eq!(decide("target_os = \"macos\""), Some(false));
2820 assert_eq!(decide("not(target_os = \"linux\")"), Some(false));
2821 assert_eq!(
2822 decide("all(unix, target_pointer_width = \"64\")"),
2823 Some(true)
2824 );
2825 assert_eq!(decide("any(windows, target_os = \"wasi\")"), Some(false));
2826 assert_eq!(decide("feature = \"extra\""), None);
2828 assert_eq!(decide("not(feature = \"extra\")"), None);
2829 assert_eq!(decide("debug_assertions"), None);
2830 assert_eq!(decide("all(windows, feature = \"extra\")"), Some(false));
2831 assert_eq!(decide("any(unix, feature = \"extra\")"), Some(true));
2832 assert_eq!(decide("any(windows, feature = \"extra\")"), None);
2833
2834 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";
2835 let instrumented = instrument_rust_file(
2836 "src/lib.rs",
2837 source,
2838 "crate::__supercov_runtime_v1",
2839 false,
2840 Some(&cfg),
2841 )
2842 .unwrap();
2843 let lines = instrumented
2844 .manifest
2845 .points
2846 .iter()
2847 .map(|point| point.line)
2848 .collect::<BTreeSet<_>>();
2849 assert!(
2850 !lines.contains(&3),
2851 "the windows item is left out: {lines:?}"
2852 );
2853 assert!(lines.contains(&5) && lines.contains(&7), "{lines:?}");
2854 let windows = &instrumented.code[instrumented.code.find("#[cfg(windows)]").unwrap()
2855 ..instrumented.code.find("#[cfg(unix)]").unwrap()];
2856 assert!(!windows.contains("__supercov_runtime_v1"), "{windows}");
2857 }
2858
2859 #[test]
2860 fn test_code_is_known_by_where_it_sits() {
2861 for file in [
2862 "tests/it.rs",
2863 "tests/common/mod.rs",
2864 "crates/a/tests/it.rs",
2865 "examples/demo.rs",
2866 "benches/speed.rs",
2867 "src/tests.rs",
2868 "src/parser_tests.rs",
2869 "src/parser-tests.rs",
2870 ] {
2871 assert!(test_code_path(file), "{file}");
2872 }
2873 for file in [
2874 "src/lib.rs",
2875 "src/attests.rs",
2876 "src/_tests.rs",
2877 "crates/foo-tests/src/lib.rs",
2878 "src/testing.rs",
2879 ] {
2880 assert!(!test_code_path(file), "{file}");
2881 }
2882 }
2883
2884 #[test]
2885 fn test_items_keep_only_their_assertion_markers() {
2886 let source = r#"pub fn add(a: i32, b: i32) -> i32 {
2887 if a > 0 && b > 0 { a + b } else { 0 }
2888}
2889
2890#[cfg(test)]
2891mod tests {
2892 use super::*;
2893 #[test]
2894 fn adds() {
2895 let total = add(1, 2);
2896 assert_eq!(total, 3);
2897 }
2898}
2899
2900#[cfg(all(test, feature = "slow"))]
2901fn slow_helper() -> bool { true || false }
2902
2903#[tokio::test]
2904async fn async_adds() {
2905 assert!(add(2, 2) == 4);
2906}
2907"#;
2908 let transformed =
2909 instrument_rust_source("src/lib.rs", source, "crate::__supercov_runtime_v1").unwrap();
2910 let manifest = &transformed.manifest;
2911 assert!(
2912 manifest.points.iter().all(|point| point.line <= 3),
2913 "{:?}",
2914 manifest.points
2915 );
2916 assert!(manifest.decisions.iter().all(|decision| decision.line == 2));
2917 assert!(manifest.branches.iter().all(|branch| branch.line == 2));
2918 assert_eq!(manifest.decisions.len(), 1);
2919 let sites = manifest
2920 .assertion_sites
2921 .iter()
2922 .map(|site| site.source.as_str())
2923 .collect::<BTreeSet<_>>();
2924 assert_eq!(
2925 sites,
2926 BTreeSet::from(["assert!(add(2, 2) == 4);", "assert_eq!(total, 3);"])
2927 );
2928 let tests = &transformed.code[transformed.code.find("#[cfg(test)]").unwrap()..];
2929 assert_eq!(tests.matches("::assertion(").count(), 2, "{tests}");
2930 assert!(!tests.contains("::hit("), "{tests}");
2931 assert!(!tests.contains("::logical("), "{tests}");
2932
2933 let file = instrument_rust_source(
2935 "tests/it.rs",
2936 "#[test]\nfn it() {\n let x = 1;\n assert_eq!(x, 1);\n}\n",
2937 "crate::__supercov_runtime_v1",
2938 )
2939 .unwrap();
2940 assert!(file.manifest.points.is_empty());
2941 assert_eq!(file.manifest.assertion_sites.len(), 1);
2942 assert!(!file.code.contains("::hit("));
2943 }
2944
2945 #[test]
2946 fn discovers_rust_obligations_with_exact_ranges_and_stable_ids() {
2947 let source = r#"fn classify<T>(values: &[T], first: bool, second: bool, third: bool) -> Option<&T> {
2948 let picked = if first && (second || third) {
2949 values.first()?
2950 } else {
2951 None
2952 };
2953 for value in values {
2954 if first || second {
2955 return Some(value);
2956 }
2957 }
2958 match picked {
2959 Some(value) if second && third => Some(value),
2960 _ => None,
2961 }
2962}
2963
2964fn closure(value: i32) -> bool {
2965 (|candidate| candidate > 0)(value)
2966}
2967"#;
2968 let first = build_rust_manifest("src/lib.rs", source).unwrap();
2969 let second = build_rust_manifest("src/lib.rs", source).unwrap();
2970 assert_eq!(first, second);
2971 assert!(first.points.iter().any(|point| {
2972 point.kind == PointKind::Function && point.label.as_deref() == Some("classify")
2973 }));
2974 assert!(first.points.iter().any(|point| {
2975 point.kind == PointKind::Function && point.label.as_deref() == Some("<closure>")
2976 }));
2977 let first_if = first
2978 .decisions
2979 .iter()
2980 .find(|decision| decision.line == 2)
2981 .unwrap();
2982 assert_eq!(first_if.conditions, ["first", "second", "third"]);
2983 assert_eq!(first_if.column, 20);
2984 assert!(
2985 first
2986 .branches
2987 .iter()
2988 .any(|branch| branch.kind == "for-loop")
2989 );
2990 let mut arms = first
2991 .branches
2992 .iter()
2993 .filter(|branch| branch.kind == "match-arm")
2994 .collect::<Vec<_>>();
2995 arms.sort_by_key(|branch| branch.line);
2996 assert_eq!(arms.len(), 2);
2997 assert_eq!(
2998 arms[0]
2999 .alternatives
3000 .iter()
3001 .map(|alternative| alternative.label.as_str())
3002 .collect::<Vec<_>>(),
3003 ["not selected", "selected"]
3004 );
3005 assert_eq!(
3008 arms[1]
3009 .alternatives
3010 .iter()
3011 .map(|alternative| alternative.label.as_str())
3012 .collect::<Vec<_>>(),
3013 ["selected"]
3014 );
3015 assert!(
3016 first
3017 .branches
3018 .iter()
3019 .any(|branch| branch.kind == "try-operator")
3020 );
3021 assert!(first.decisions.iter().all(|decision| {
3022 decision.id.starts_with("rs:decision:") && decision.conditions.len() >= 2
3023 }));
3024 assert!(first.limitations.is_empty());
3025 }
3026
3027 #[test]
3028 fn declares_macro_and_const_boundaries_instead_of_hiding_them() {
3029 let source = r#"const fn doubled(value: usize) -> usize { value * 2 }
3030
3031macro_rules! noop {
3032 () => {};
3033}
3034
3035fn checked(value: bool) -> bool {
3036 assert!(value);
3037 noop!();
3038 const { doubled(2) == 4 }
3039}
3040"#;
3041 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
3042 assert!(manifest.decisions.iter().any(|decision| {
3045 decision.line == 8 && decision.source == "value" && decision.conditions == ["value"]
3046 }));
3047 let ids = manifest
3048 .limitations
3049 .iter()
3050 .filter_map(limitation_kind_of)
3051 .collect::<BTreeSet<_>>();
3052 assert_eq!(
3053 ids,
3054 BTreeSet::from([
3055 "rust-const-context-not-instrumented",
3056 "rust-macro-expansion-not-instrumented"
3057 ])
3058 );
3059 assert!(!manifest.points.iter().any(|point| {
3060 point.kind == PointKind::Function && point.label.as_deref() == Some("doubled")
3061 }));
3062 }
3063
3064 #[test]
3065 fn transforms_points_and_nested_decisions_without_changing_behavior() {
3066 let source = r#"use std::sync::atomic::{AtomicUsize, Ordering};
3067
3068static CALLS: AtomicUsize = AtomicUsize::new(0);
3069
3070fn observed(name: &str, value: bool) -> bool {
3071 let order = CALLS.fetch_add(1, Ordering::SeqCst);
3072 println!("{order}:{name}:{value}");
3073 value
3074}
3075
3076fn classify(first: bool, second: bool, third: bool) -> i32 {
3077 if observed("a", first) && (observed("b", second) || observed("c", third)) {
3078 7
3079 } else {
3080 3
3081 }
3082}
3083
3084fn main() {
3085 let closure = |value: i32| value + 1;
3086 println!("result={}", closure(classify(true, false, true)));
3087}
3088"#;
3089 let transformed =
3090 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3091 assert!(transformed.code.contains("::condition("));
3092 assert!(transformed.code.contains("::decision("));
3093 assert!(transformed.code.contains("::hit("));
3094 let original = compile_and_run(source, "original");
3095 let instrumented = compile_and_run(
3096 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3097 "instrumented",
3098 );
3099 assert_eq!(instrumented.status, original.status);
3100 assert_eq!(instrumented.stdout, original.stdout);
3101 assert_eq!(instrumented.stderr, original.stderr);
3102 }
3103
3104 #[test]
3105 fn let_chains_take_derived_condition_probes_and_const_contexts_stay_declared() {
3106 let source = r#"const fn enabled(value: bool) -> bool {
3107 if value { true } else { false }
3108}
3109
3110fn classify(value: Option<bool>, fallback: bool) -> bool {
3111 if let Some(inner) = value && inner && fallback { true } else { false }
3112}
3113"#;
3114 let transformed =
3115 instrument_rust_source("src/lib.rs", source, "crate::__supercov_runtime_v1").unwrap();
3116 let ids = transformed
3117 .manifest
3118 .limitations
3119 .iter()
3120 .filter_map(limitation_kind_of)
3121 .collect::<BTreeSet<_>>();
3122 assert!(ids.contains("rust-const-context-not-instrumented"));
3123 assert!(!ids.contains("rust-let-chain-probes-not-injected"));
3124 assert!(
3127 transformed
3128 .code
3129 .contains("::reached(&mut __supercov_decision_")
3130 );
3131 assert!(
3132 transformed
3133 .code
3134 .contains("::condition((inner), &mut __supercov_decision_")
3135 );
3136 assert!(
3137 transformed
3138 .code
3139 .contains("::decision_chain(&mut __supercov_decision_")
3140 );
3141 assert!(transformed.code.contains("&& let Some(inner) = value &&"));
3142 assert!(!transformed.code.contains("condition((let"));
3143 }
3144
3145 #[test]
3146 fn std_macro_arguments_take_probes_and_assertions_are_decisions() {
3147 let source = r#"use std::fmt::Write as _;
3148
3149fn classify(values: &[i32], strict: bool) -> String {
3150 let mut out = String::new();
3151 assert!(values.len() < 10 && (strict || !values.is_empty()), "bad input {:?}", values);
3152 debug_assert!(values.iter().all(|v| *v > -100));
3153 let doubled = vec![values.iter().map(|v| v * 2).sum::<i32>(), if strict { 1 } else { 2 }];
3154 let repeated = vec![if strict { 1 } else { 0 }; values.len()];
3155 let small = matches!(values.first(), Some(v) if *v < 3);
3156 if matches!(values.len(), 1 | 2) && small {
3157 println!("small");
3158 }
3159 println!("{}", repeated.len() + small as usize);
3160 write!(out, "{}", doubled.iter().map(|d| if *d > 4 { "big" } else { "small" }).collect::<Vec<_>>().join(",")).unwrap();
3161 println!("{} {}", format!("{:?}", doubled), if values.first().copied().unwrap_or(0) > 0 && strict { "positive" } else { "other" });
3162 assert_eq!(doubled.len(), if strict { 2 } else { 2 }, "length for strict={strict}");
3163 out
3164}
3165
3166fn main() {
3167 println!("{}", classify(&[1, 2], true));
3168 println!("{}", classify(&[3], false));
3169 println!("{}", classify(&[], true));
3170 let total: i32 = dbg!(vec![1, 2, 3]).into_iter().sum();
3171 println!("{total}");
3172}
3173"#;
3174 let transformed =
3175 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3176 assert!(transformed.code.contains(
3179 r#", "assertion failed: {}", stringify!(values.iter().all(|v| *v > -100)))"#
3180 ));
3181 assert!(
3183 transformed
3184 .code
3185 .contains(r#"), "bad input {:?}", values);"#)
3186 );
3187 let assertion = transformed
3191 .manifest
3192 .decisions
3193 .iter()
3194 .find(|decision| decision.line == 5)
3195 .expect("assert! decision");
3196 assert_eq!(
3197 assertion.conditions,
3198 ["values.len() < 10", "strict", "!values.is_empty()"]
3199 );
3200 assert!(
3201 transformed
3202 .manifest
3203 .decisions
3204 .iter()
3205 .any(|decision| decision.line == 7)
3206 );
3207 assert!(
3208 transformed
3209 .manifest
3210 .decisions
3211 .iter()
3212 .any(|decision| decision.line == 9)
3213 );
3214 assert!(
3215 transformed
3216 .code
3217 .contains("assert!(({ let mut __supercov_decision_")
3218 );
3219 assert!(
3220 transformed
3221 .code
3222 .contains(", if ({ let mut __supercov_decision_")
3223 );
3224 assert!(
3226 transformed
3227 .code
3228 .contains("vec![if ({ let mut __supercov_decision_")
3229 );
3230 assert!(
3233 transformed
3234 .code
3235 .contains("let small = ({ let mut __supercov_decision_")
3236 );
3237 assert_eq!(
3238 transformed
3239 .manifest
3240 .decisions
3241 .iter()
3242 .filter(|decision| decision.line == 10)
3243 .count(),
3244 1
3245 );
3246 assert_eq!(
3247 transformed
3248 .manifest
3249 .decisions
3250 .iter()
3251 .filter(|decision| decision.line == 9)
3252 .count(),
3253 1
3254 );
3255 assert!(
3256 transformed
3257 .code
3258 .contains("vec![values.iter().map(|v| { crate::__supercov_runtime_v1::hit(")
3259 );
3260 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
3261 limitation.get("id").and_then(|id| id.as_str())
3262 == Some("rust-macro-expansion-not-instrumented")
3263 }));
3264 let original = compile_and_run(source, "original-macros");
3265 let instrumented = compile_and_run(
3266 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3267 "instrumented-macros",
3268 );
3269 assert_eq!(instrumented.status, original.status);
3270 assert_eq!(instrumented.stdout, original.stdout);
3271 let after_location = |stderr: &[u8]| {
3274 String::from_utf8_lossy(stderr)
3275 .lines()
3276 .map(|line| {
3277 line.split_once("] ")
3278 .map_or(line, |(_, rest)| rest)
3279 .to_owned()
3280 })
3281 .collect::<Vec<_>>()
3282 };
3283 assert_eq!(
3284 after_location(&instrumented.stderr),
3285 after_location(&original.stderr)
3286 );
3287 }
3288
3289 #[test]
3290 fn assertion_panic_messages_survive_instrumentation() {
3291 let source = r#"fn grow(len: usize, new_capacity: usize) {
3294 assert!(new_capacity >= len);
3295}
3296
3297fn check(value: i32) {
3298 assert!(value > 0 && value < 10, "value {value} out of range");
3299}
3300
3301// tokio: `assert!` only negates its operand, so a `&bool` is accepted.
3302fn all_seen(seen: &[bool]) {
3303 for was_seen in seen {
3304 assert!(was_seen);
3305 debug_assert!(was_seen, "seen");
3306 }
3307}
3308
3309fn main() {
3310 std::panic::set_hook(Box::new(|_| {}));
3311 all_seen(&[true, true]);
3312 match std::panic::catch_unwind(|| all_seen(&[true, false])) {
3313 Ok(()) => println!("ok"),
3314 Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
3315 }
3316 for (len, capacity) in [(3, 5), (8, 5)] {
3317 match std::panic::catch_unwind(|| grow(len, capacity)) {
3318 Ok(()) => println!("ok"),
3319 Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
3320 }
3321 }
3322 match std::panic::catch_unwind(|| check(12)) {
3323 Ok(()) => println!("ok"),
3324 Err(payload) => println!("{}", payload.downcast_ref::<String>().cloned().unwrap_or_default()),
3325 }
3326}
3327"#;
3328 let transformed =
3329 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3330 let original = compile_and_run(source, "original-assert-message");
3331 let instrumented = compile_and_run(
3332 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3333 "instrumented-assert-message",
3334 );
3335 assert_eq!(instrumented.status, original.status);
3336 assert_eq!(instrumented.stdout, original.stdout);
3337 assert!(
3338 String::from_utf8_lossy(&instrumented.stdout)
3339 .contains("assertion failed: new_capacity >= len")
3340 );
3341 assert!(
3342 String::from_utf8_lossy(&instrumented.stdout).contains("assertion failed: was_seen")
3343 );
3344 }
3345
3346 #[test]
3347 fn files_that_predate_a_reserved_word_still_instrument() {
3348 let source = r#"struct Rng(u64);
3350impl Rng {
3351 fn gen(&mut self) -> u64 {
3352 self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1);
3353 self.0 >> 33
3354 }
3355}
3356
3357fn main() {
3358 let mut rng = Rng(7);
3359 let mut odd = 0;
3360 for _ in 0..10 {
3361 if rng.gen() % 2 == 1 {
3362 odd += 1;
3363 }
3364 }
3365 println!("{odd}");
3366}
3367"#;
3368 let transformed =
3369 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3370 assert!(
3371 transformed
3372 .manifest
3373 .decisions
3374 .iter()
3375 .any(|decision| decision.line == 13)
3376 );
3377 let original = compile_and_run_edition(source, "original-gen", "2021");
3378 let instrumented = compile_and_run_edition(
3379 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3380 "instrumented-gen",
3381 "2021",
3382 );
3383 assert_eq!(instrumented.status, original.status);
3384 assert_eq!(instrumented.stdout, original.stdout);
3385 }
3386
3387 #[test]
3388 fn lone_let_conditions_compile_before_edition_2024_and_record_breaks() {
3389 let source = r#"fn first_even(values: &[i32]) -> Option<i32> {
3393 let mut it = values.iter();
3394 'scan: while let Some(value) = it.next() {
3395 if *value < 0 {
3396 break;
3397 }
3398 for _ in 0..1 {
3399 if *value == 99 {
3400 break 'scan;
3401 }
3402 if *value == 98 {
3403 break;
3404 }
3405 }
3406 if *value % 2 == 0 {
3407 return Some(*value);
3408 }
3409 }
3410 None
3411}
3412
3413fn describe(value: Option<i32>) -> &'static str {
3414 if let Some(inner) = value {
3415 if inner > 0 { "positive" } else { "non-positive" }
3416 } else if let None = value {
3417 "none"
3418 } else {
3419 "unreachable"
3420 }
3421}
3422
3423fn count(values: &[Option<i32>]) -> usize {
3424 let mut total = 0;
3425 for value in values {
3426 if let Some(_) = value {
3427 total += 1;
3428 }
3429 }
3430 total
3431}
3432
3433fn main() {
3434 println!("{:?} {:?} {:?} {:?}", first_even(&[1, 3, 4]), first_even(&[1, -1, 4]), first_even(&[99, 4]), first_even(&[98, 3, 6]));
3435 println!("{} {} {}", describe(Some(2)), describe(Some(-2)), describe(None));
3436 println!("{}", count(&[Some(1), None, Some(3)]));
3437}
3438"#;
3439 let transformed =
3440 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3441 assert!(!transformed.code.contains("&& let"));
3442 assert!(transformed.code.contains("__supercov_broke_"));
3443 assert_eq!(transformed.code.matches("= true; break").count(), 2);
3444 for edition in ["2021", "2024"] {
3445 let original =
3446 compile_and_run_edition(source, &format!("original-lone-let-{edition}"), edition);
3447 let instrumented = compile_and_run_edition(
3448 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3449 &format!("instrumented-lone-let-{edition}"),
3450 edition,
3451 );
3452 assert_eq!(instrumented.status, original.status);
3453 assert_eq!(instrumented.stdout, original.stdout);
3454 assert_eq!(instrumented.stderr, original.stderr);
3455 }
3456 }
3457
3458 #[test]
3459 fn let_chains_keep_their_behavior() {
3460 let source = r#"fn describe(value: Option<i32>, flag: bool) -> &'static str {
3461 if let Some(inner) = value && inner > 0 && flag {
3462 "positive"
3463 } else if let Some(inner) = value && (inner < 0 || flag) {
3464 "negative-or-flagged"
3465 } else {
3466 "other"
3467 }
3468}
3469
3470fn count_pairs(values: &[(Option<i32>, i32)]) -> i32 {
3471 let mut total = 0;
3472 let mut it = values.iter();
3473 while let Some((first, second)) = it.next() && let Some(inner) = first && *second > 0 {
3474 total += inner * second;
3475 if total > 100 {
3476 break;
3477 }
3478 }
3479 total
3480}
3481
3482fn tail(value: Option<&str>) -> usize {
3483 let pick = |v: Option<&str>| if let Some(text) = v && !text.is_empty() { text.len() } else { 0 };
3484 if let Some(text) = value && text.starts_with('x') {
3485 println!("x-prefixed");
3486 }
3487 pick(value)
3488}
3489
3490fn main() {
3491 for value in [Some(3), Some(-3), Some(0), None] {
3492 for flag in [true, false] {
3493 println!("{value:?} {flag} {}", describe(value, flag));
3494 }
3495 }
3496 println!("{}", count_pairs(&[(Some(2), 3), (Some(4), 5), (None, 1), (Some(9), 9)]));
3497 println!("{}", count_pairs(&[(Some(50), 3), (Some(4), 5)]));
3498 println!("{} {} {}", tail(Some("xyz")), tail(Some("")), tail(None));
3499}
3500"#;
3501 let transformed =
3502 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3503 assert_eq!(
3504 transformed.code.matches("const __SUPERCOV_CHAIN_").count(),
3505 5
3506 );
3507 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
3508 limitation.get("id").and_then(|id| id.as_str())
3509 == Some("rust-let-chain-probes-not-injected")
3510 }));
3511 let original = compile_and_run(source, "original-chains");
3512 let instrumented = compile_and_run(
3513 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3514 "instrumented-chains",
3515 );
3516 assert_eq!(instrumented.status, original.status);
3517 assert_eq!(instrumented.stdout, original.stdout);
3518 assert_eq!(instrumented.stderr, original.stderr);
3519 }
3520
3521 #[test]
3522 fn instrumented_const_and_static_initialisers_still_compile() {
3523 let source = r#"const DIRECT: usize = if cfg!(unix) { 100 } else { 1_000 };
3529static WIDTH: usize = if cfg!(unix) { 2 } else { 4 };
3530
3531enum Mode {
3532 Narrow = if cfg!(unix) { 1 } else { 2 },
3533}
3534
3535struct Buffer([u8; if cfg!(unix) { 4 } else { 8 }]);
3536
3537impl Buffer {
3538 const SPAN: usize = if cfg!(unix) { 5 } else { 9 };
3539}
3540
3541fn scaled(flag: bool) -> usize {
3542 const LOCAL: usize = if cfg!(unix) { 3 } else { 6 };
3543 if flag { LOCAL + Buffer::SPAN } else { DIRECT + WIDTH }
3544}
3545
3546fn main() {
3547 let buffer = Buffer([0; if cfg!(unix) { 4 } else { 8 }]);
3548 println!(
3549 "{} {} {} {}",
3550 scaled(true),
3551 scaled(false),
3552 Mode::Narrow as usize,
3553 buffer.0.len()
3554 );
3555}
3556"#;
3557 let transformed =
3558 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3559 assert!(transformed.code.contains("::decision("));
3562 let ids = transformed
3563 .manifest
3564 .limitations
3565 .iter()
3566 .filter_map(limitation_kind_of)
3567 .collect::<BTreeSet<_>>();
3568 assert!(ids.contains("rust-const-context-not-instrumented"));
3569
3570 let original = compile_and_run(source, "const-original");
3571 let instrumented = compile_and_run(
3572 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3573 "const-instrumented",
3574 );
3575 assert_eq!(instrumented.status, original.status);
3576 assert_eq!(instrumented.stdout, original.stdout);
3577 assert_eq!(instrumented.stderr, original.stderr);
3578 }
3579
3580 #[test]
3581 fn a_probed_global_allocator_would_recurse_into_itself() {
3582 let source = r#"use std::alloc::{GlobalAlloc, Layout, System};
3588
3589struct Odd;
3590
3591unsafe impl GlobalAlloc for Odd {
3592 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
3593 if layout.align() == 1 && layout.size() > 0 {
3594 System.alloc(layout)
3595 } else {
3596 System.alloc(layout)
3597 }
3598 }
3599
3600 unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
3601 System.dealloc(pointer, layout);
3602 }
3603}
3604
3605#[global_allocator]
3606static ODD: Odd = Odd;
3607
3608fn classify(flag: bool) -> usize {
3609 if flag { 1 } else { 2 }
3610}
3611
3612fn main() {
3613 let held = std::vec![7u8; 32];
3614 println!("{} {}", classify(!held.is_empty()), held.len());
3615}
3616"#;
3617 let transformed =
3618 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3619 let allocator = transformed
3621 .code
3622 .split("unsafe impl GlobalAlloc for Odd")
3623 .nth(1)
3624 .and_then(|rest| rest.split("#[global_allocator]").next())
3625 .expect("the instrumented source still contains the allocator impl");
3626 assert!(
3627 !allocator.contains("__supercov_runtime_v1"),
3628 "probe injected into a GlobalAlloc impl:\n{allocator}"
3629 );
3630 assert!(transformed.code.contains("::decision("));
3632 let ids = transformed
3633 .manifest
3634 .limitations
3635 .iter()
3636 .filter_map(limitation_kind_of)
3637 .collect::<BTreeSet<_>>();
3638 assert!(ids.contains("rust-global-allocator-not-instrumented"));
3639
3640 let original = compile_and_run(source, "alloc-original");
3641 let instrumented = compile_and_run(
3642 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3643 "alloc-instrumented",
3644 );
3645 assert_eq!(instrumented.status, original.status);
3646 assert_eq!(instrumented.stdout, original.stdout);
3647 assert_eq!(instrumented.stderr, original.stderr);
3648 }
3649
3650 #[test]
3651 fn match_arms_record_selection_without_changing_behavior() {
3652 let source = r#"#[derive(Debug)]
3653enum Shape { Dot, Line(i32), Box { w: i32, h: i32 } }
3654
3655fn area(shape: &Shape) -> i32 {
3656 match shape {
3657 Shape::Dot => 0,
3658 Shape::Line(length) if *length < 0 => -length,
3659 Shape::Line(length) => *length,
3660 Shape::Box { w, h } => {
3661 let area = w * h;
3662 area
3663 }
3664 }
3665}
3666
3667fn describe(value: i32) -> &'static str {
3668 let inner = |v: i32| match v { 0 => "none", 1 => "one", _ => "many" };
3669 match value {
3670 0 => inner(value),
3671 n if n < 0 => unsafe { std::hint::unreachable_unchecked() },
3672 n => match n % 2 {
3673 0 => "even",
3674 _ => inner(n),
3675 },
3676 }
3677}
3678
3679fn main() {
3680 for shape in [Shape::Dot, Shape::Line(-3), Shape::Line(4), Shape::Box { w: 2, h: 5 }] {
3681 println!("{shape:?}={}", area(&shape));
3682 }
3683 for value in [0, 1, 3, 8] {
3684 println!("{value}:{}", describe(value));
3685 }
3686}
3687"#;
3688 let transformed =
3689 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3690 assert!(transformed.code.contains("::arms(__SUPERCOV_ARMS_"));
3691 assert_eq!(
3692 transformed.code.matches("const __SUPERCOV_ARMS_").count(),
3693 4
3694 );
3695 let arms = transformed
3696 .manifest
3697 .branches
3698 .iter()
3699 .filter(|branch| branch.kind == "match-arm")
3700 .count();
3701 assert_eq!(arms, 4 + 3 + 3 + 2);
3702 for branch in transformed
3705 .manifest
3706 .branches
3707 .iter()
3708 .filter(|branch| branch.kind == "match-arm")
3709 {
3710 for alternative in &branch.alternatives {
3711 assert!(
3712 transformed.code.contains(&format!("{:?}", alternative.id)),
3713 "{} is not in any table",
3714 alternative.id
3715 );
3716 }
3717 }
3718 let original = compile_and_run(source, "original-arms");
3719 let instrumented = compile_and_run(
3720 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3721 "instrumented-arms",
3722 );
3723 assert_eq!(instrumented.status, original.status);
3724 assert_eq!(instrumented.stdout, original.stdout);
3725 assert_eq!(instrumented.stderr, original.stderr);
3726 }
3727
3728 #[test]
3729 fn loops_logic_and_try_record_their_branches_without_changing_behavior() {
3730 let source = r#"use std::ops::ControlFlow;
3731
3732fn total(values: &[i32]) -> i32 {
3733 let mut sum = 0;
3734 for value in values {
3735 sum += value;
3736 }
3737 'outer: for row in 0..3 {
3738 for column in 0..3 {
3739 if column > row {
3740 continue 'outer;
3741 }
3742 sum += row * column;
3743 }
3744 }
3745 sum
3746}
3747
3748fn first_even(values: &[i32]) -> Option<i32> {
3749 let mut index = 0;
3750 'scan: while index < values.len() {
3751 if values[index] % 2 == 0 {
3752 break 'scan;
3753 }
3754 index += 1;
3755 }
3756 let mut it = values.iter().skip(index);
3757 while let Some(value) = it.next() {
3758 return Some(*value);
3759 }
3760 None
3761}
3762
3763fn parse_twice(text: &str) -> Result<i32, String> {
3764 let value: i32 = text.trim().parse().map_err(|_| "bad".to_string())?;
3765 let doubled = Some(value).map(|v| v * 2).ok_or("none")?;
3766 Ok(doubled)
3767}
3768
3769fn halve(value: i32) -> Option<i32> {
3770 let even = (value % 2 == 0).then_some(value)?;
3771 Some(even / 2)
3772}
3773
3774fn flow(values: &[i32]) -> ControlFlow<i32, i32> {
3775 let mut sum = 0;
3776 for value in values {
3777 let step: ControlFlow<i32, i32> = if *value < 0 { ControlFlow::Break(*value) } else { ControlFlow::Continue(*value) };
3778 sum += step?;
3779 }
3780 ControlFlow::Continue(sum)
3781}
3782
3783fn gate(a: bool, b: bool, c: bool) -> bool {
3784 let both = a && b;
3785 let either = a || b || c;
3786 both || (either && !c) || (c && a && (b || !b))
3787}
3788
3789fn main() {
3790 println!("{} {}", total(&[]), total(&[1, 2, 3]));
3791 println!("{:?} {:?} {:?}", first_even(&[]), first_even(&[1, 3]), first_even(&[1, 4, 6]));
3792 println!("{:?} {:?}", parse_twice(" 21 "), parse_twice("x"));
3793 println!("{:?} {:?}", halve(8), halve(7));
3794 println!("{:?} {:?}", flow(&[1, 2]), flow(&[1, -5, 2]));
3795 for a in [false, true] {
3796 for b in [false, true] {
3797 for c in [false, true] {
3798 print!("{}", gate(a, b, c) as u8);
3799 }
3800 }
3801 }
3802 println!();
3803}
3804"#;
3805 let transformed =
3806 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3807 for marker in [
3808 "::logical((",
3809 "::for_loop((",
3810 "::entered(&mut __supercov_loop_",
3811 "::zero_iterations(__supercov_loop_",
3812 "::TryProbe::probe((",
3813 ] {
3814 assert!(transformed.code.contains(marker), "{marker} missing");
3815 }
3816 let kinds = |kind: &str| {
3817 transformed
3818 .manifest
3819 .branches
3820 .iter()
3821 .filter(|branch| branch.kind == kind)
3822 .count()
3823 };
3824 assert_eq!(kinds("for-loop"), 3 + 1 + 3);
3825 assert_eq!(kinds("while-loop"), 2);
3826 assert_eq!(kinds("try-operator"), 4);
3827 assert_eq!(kinds("logical-and"), 4);
3828 assert_eq!(kinds("logical-or"), 5);
3829 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
3830 limitation.get("id").and_then(|id| id.as_str())
3831 == Some("rust-structural-branch-probes-not-yet-injected")
3832 }));
3833 let original = compile_and_run(source, "original-structural");
3834 let instrumented = compile_and_run(
3835 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3836 "instrumented-structural",
3837 );
3838 assert_eq!(instrumented.status, original.status);
3839 assert_eq!(instrumented.stdout, original.stdout);
3840 assert_eq!(instrumented.stderr, original.stderr);
3841 }
3842
3843 #[test]
3844 fn cfg_gated_sibling_blocks_keep_their_tail_position() {
3845 let source = r#"pub fn is_available() -> bool {
3851 #[cfg(target_endian = "little")]
3852 {
3853 true
3854 }
3855 #[cfg(not(target_endian = "little"))]
3856 {
3857 false
3858 }
3859}
3860
3861fn main() {
3862 println!("{}", is_available());
3863}
3864"#;
3865 let transformed =
3866 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3867 let original = compile_and_run(source, "cfg-original");
3868 let instrumented = compile_and_run(
3869 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3870 "cfg-instrumented",
3871 );
3872 assert_eq!(instrumented.status, original.status);
3873 assert_eq!(instrumented.stdout, original.stdout);
3874 assert!(
3876 transformed
3877 .code
3878 .contains("{crate::__supercov_runtime_v1::hit("),
3879 "{}",
3880 transformed.code
3881 );
3882
3883 let attributed_let = r#"fn main() {
3887 #[cfg(target_endian = "little")]
3888 let value = 1;
3889 #[cfg(not(target_endian = "little"))]
3890 let value = 2;
3891 #[cfg(target_endian = "little")]
3892 let borrowed: &String = &String::from("little");
3893 #[cfg(not(target_endian = "little"))]
3894 let borrowed: &String = &String::from("big");
3895 #[cfg(target_endian = "little")]
3896 print!("le ");
3897 #[cfg(not(target_endian = "little"))]
3898 print!("be ");
3899 #[allow(unused_assignments)]
3900 let mut later;
3901 later = value + 1;
3902 println!("{value} {borrowed} {later}");
3903}
3904"#;
3905 let transformed = instrument_rust_source(
3906 "src/main.rs",
3907 attributed_let,
3908 "crate::__supercov_runtime_v1",
3909 )
3910 .unwrap();
3911 let ids = transformed
3912 .manifest
3913 .limitations
3914 .iter()
3915 .filter_map(limitation_kind_of)
3916 .collect::<BTreeSet<_>>();
3917 assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
3919 assert!(
3920 transformed
3921 .code
3922 .contains("let value = { crate::__supercov_runtime_v1::hit(")
3923 );
3924 assert!(
3925 transformed
3926 .code
3927 .contains("let borrowed: &String = { crate::__supercov_runtime_v1::hit(")
3928 );
3929 assert!(
3930 transformed
3931 .code
3932 .contains("] { crate::__supercov_runtime_v1::hit(")
3933 );
3934 let original = compile_and_run(attributed_let, "cfg-let-original");
3935 let instrumented = compile_and_run(
3936 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3937 "cfg-let-instrumented",
3938 );
3939 assert_eq!(instrumented.status, original.status);
3940 assert_eq!(instrumented.stdout, original.stdout);
3941 assert_eq!(instrumented.stderr, original.stderr);
3942
3943 let trailing_macro = r#"macro_rules! pick { ($e:expr) => { $e } }
3948fn value() -> i32 {
3949 let base = 20;
3950 #[rustfmt::skip]
3951 pick! { base + 1 }
3952}
3953fn effect() {
3954 #[rustfmt::skip]
3955 println! { "effect" }
3956}
3957fn gated() {
3958 #[cfg(target_endian = "little")]
3959 println! { "little" }
3960}
3961fn main() {
3962 effect();
3963 gated();
3964 println!("{}", value());
3965}
3966"#;
3967 let transformed = instrument_rust_source(
3968 "src/main.rs",
3969 trailing_macro,
3970 "crate::__supercov_runtime_v1",
3971 )
3972 .unwrap();
3973 assert!(
3974 transformed
3975 .code
3976 .contains("{ crate::__supercov_runtime_v1::hit(\"rs:statement:")
3977 );
3978 assert!(
3979 transformed
3980 .code
3981 .contains("); #[rustfmt::skip]\n pick! { base + 1 } }")
3982 );
3983 assert!(
3984 transformed
3985 .code
3986 .contains("); #[rustfmt::skip]\n println! { \"effect\" } }")
3987 );
3988 assert!(
3989 transformed.code.contains(
3990 "\n #[cfg(target_endian = \"little\")]\n println! { \"little\" }\n"
3991 )
3992 );
3993 let ids = transformed
3994 .manifest
3995 .limitations
3996 .iter()
3997 .filter_map(limitation_kind_of)
3998 .collect::<BTreeSet<_>>();
3999 assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
4000 let original = compile_and_run(trailing_macro, "trailing-macro-original");
4001 let instrumented = compile_and_run(
4002 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4003 "trailing-macro-instrumented",
4004 );
4005 assert_eq!(
4006 instrumented.status,
4007 original.status,
4008 "{}",
4009 String::from_utf8_lossy(&instrumented.stderr)
4010 );
4011 assert_eq!(instrumented.stdout, original.stdout);
4012 assert_eq!(instrumented.stderr, original.stderr);
4013 }
4014
4015 #[test]
4016 fn an_attributed_macro_statement_keeps_its_macro_in_statement_position() {
4017 let source = r#"macro_rules! trace {
4022 ($($arg:tt)*) => {
4023 #[cfg(target_endian = "little")]
4024 {
4025 println!($($arg)+);
4026 }
4027 }
4028}
4029fn manual() {
4030 #[cfg(any(target_endian = "little", target_endian = "big"))]
4031 trace!("manual");
4032 let _ = 1;
4033}
4034fn main() {
4035 manual();
4036}
4037"#;
4038 let transformed =
4039 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
4040 assert!(
4043 transformed.code.contains(r#"trace!("manual"); }"#),
4044 "{}",
4045 transformed.code
4046 );
4047 let original = compile_and_run(source, "attributed-macro-statement-original");
4048 let instrumented = compile_and_run(
4049 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4050 "attributed-macro-statement-instrumented",
4051 );
4052 assert_eq!(
4053 instrumented.status,
4054 original.status,
4055 "{}",
4056 String::from_utf8_lossy(&instrumented.stderr)
4057 );
4058 assert_eq!(instrumented.stdout, original.stdout);
4059 assert_eq!(instrumented.stderr, original.stderr);
4060 }
4061
4062 #[test]
4063 fn an_attributed_lets_macro_initialiser_keeps_its_value() {
4064 let source = r#"macro_rules! first {
4070 ($e:expr) => { $e }
4071}
4072fn value(flag: bool) -> i32 {
4073 #[cfg(any(target_endian = "little", target_endian = "big"))]
4074 let chosen = first!(if flag { 7 } else { 3 });
4075 chosen + 1
4076}
4077fn main() {
4078 println!("{}", value(true));
4079}
4080"#;
4081 let transformed =
4082 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
4083 assert!(
4086 !transformed
4087 .code
4088 .contains("first!(if flag { 7 } else { 3 }); }"),
4089 "{}",
4090 transformed.code
4091 );
4092 let original = compile_and_run(source, "attributed-let-macro-original");
4093 let instrumented = compile_and_run(
4094 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
4095 "attributed-let-macro-instrumented",
4096 );
4097 assert_eq!(
4098 instrumented.status,
4099 original.status,
4100 "{}",
4101 String::from_utf8_lossy(&instrumented.stderr)
4102 );
4103 assert_eq!(instrumented.stdout, original.stdout);
4104 assert_eq!(instrumented.stdout, b"8\n");
4105 }
4106
4107 #[test]
4108 fn obligations_no_probe_can_reach_are_declined() {
4109 let source = r#"pub struct Tagged(usize);
4115impl Tagged {
4116 pub const fn new(len: usize, on_heap: bool) -> Self {
4117 Self(if on_heap { len << 1 } else { len })
4118 }
4119 pub fn plain(len: usize) -> Self {
4120 Self(if len > 0 { len } else { 0 })
4121 }
4122}
4123"#;
4124 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
4125 let declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
4126 assert!(!declined.is_empty(), "the const fn was not declined");
4127
4128 let line_of = |id: &str| {
4130 manifest
4131 .points
4132 .iter()
4133 .find(|point| point.id == id)
4134 .map(|point| point.line)
4135 .or_else(|| {
4136 manifest
4137 .decisions
4138 .iter()
4139 .find(|decision| decision.id == id)
4140 .map(|decision| decision.line)
4141 })
4142 .or_else(|| {
4143 manifest
4144 .branches
4145 .iter()
4146 .find(|branch| branch.id == id)
4147 .map(|branch| branch.line)
4148 })
4149 };
4150 for id in &declined {
4151 let line = line_of(id).unwrap_or_else(|| panic!("no obligation {id}"));
4152 assert!(
4153 (3..=5).contains(&line),
4154 "declined an obligation outside the const fn, at line {line}"
4155 );
4156 }
4157 let measured = manifest
4158 .points
4159 .iter()
4160 .map(|point| (point.id.clone(), point.line))
4161 .filter(|(id, _)| !declined.contains(id))
4162 .collect::<Vec<_>>();
4163 assert!(
4164 measured.iter().any(|(_, line)| (6..=8).contains(line)),
4165 "the plain fn must stay measured: {measured:?}"
4166 );
4167 }
4168
4169 #[test]
4170 fn a_documented_function_is_reported_where_it_starts() {
4171 let source = r#"/// Documented.
4174/// Twice.
4175#[inline]
4176pub fn documented(value: i32) -> i32 {
4177 value + 1
4178}
4179"#;
4180 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
4181 let point = manifest
4182 .points
4183 .iter()
4184 .find(|point| {
4185 point.kind == PointKind::Function && point.label.as_deref() == Some("documented")
4186 })
4187 .unwrap();
4188 assert_eq!(point.line, 4, "{point:?}");
4189 assert!(
4190 point.source.starts_with("pub fn documented"),
4191 "{}",
4192 point.source
4193 );
4194 }
4195
4196 #[test]
4197 fn rejects_non_crate_local_runtime_paths() {
4198 assert_eq!(
4199 instrument_rust_source("src/lib.rs", "fn okay() {}", "supercov::runtime"),
4200 Err(RustInstrumenterError::InvalidRuntimePath)
4201 );
4202 }
4203
4204 #[test]
4205 fn rejects_invalid_rust_without_partial_obligations() {
4206 assert!(matches!(
4207 build_rust_manifest("src/lib.rs", "fn broken( {\n"),
4208 Err(RustInstrumenterError::Parse(_))
4209 ));
4210 }
4211}