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 block_entry_offset(block: &ast::BlockExpr) -> Option<usize> {
1488 let list = block.stmt_list()?;
1489 list.attrs()
1490 .last()
1491 .map(|attribute| usize::from(attribute.syntax().text_range().end()))
1492 .or_else(|| {
1493 list.l_curly_token()
1494 .map(|token| usize::from(token.text_range().end()))
1495 })
1496}
1497
1498fn item_range(node: &ra_ap_syntax::SyntaxNode) -> TextRange {
1503 let range = node.text_range();
1504 node.children_with_tokens()
1505 .find(|element| match element {
1506 ra_ap_syntax::NodeOrToken::Token(token) => !token.kind().is_trivia(),
1507 ra_ap_syntax::NodeOrToken::Node(child) => !ast::Attr::can_cast(child.kind()),
1508 })
1509 .map_or(range, |element| {
1510 TextRange::new(element.text_range().start(), range.end())
1511 })
1512}
1513
1514fn range_after_attributes(node: &impl HasAttrs) -> TextRange {
1515 let range = node.syntax().text_range();
1516 node.attrs().last().map_or(range, |attribute| {
1517 TextRange::new(attribute.syntax().text_range().end(), range.end())
1518 })
1519}
1520
1521fn has_let(expression: &ast::Expr) -> bool {
1522 expression
1523 .syntax()
1524 .descendants()
1525 .any(|node| ast::LetExpr::can_cast(node.kind()))
1526}
1527
1528enum ChainHost<'a> {
1530 If(&'a ast::IfExpr),
1531 While(&'a ast::WhileExpr),
1532}
1533
1534fn allocate_chain_table_name(
1536 file: &str,
1537 condition: &ast::Expr,
1538 identifiers: &mut BTreeSet<String>,
1539) -> String {
1540 let id = stable_id(file, "chain", condition.syntax().text_range(), "operators");
1541 let suffix = id
1542 .rsplit(':')
1543 .next()
1544 .unwrap_or("chain")
1545 .to_ascii_uppercase();
1546 let base = format!("__SUPERCOV_CHAIN_{suffix}");
1547 let mut candidate = base.clone();
1548 let mut attempt = 0_usize;
1549 while !identifiers.insert(candidate.clone()) {
1550 attempt += 1;
1551 candidate = format!("{base}_{attempt}");
1552 }
1553 candidate
1554}
1555
1556fn allocate_identifier(
1558 file: &str,
1559 range: TextRange,
1560 kind: &str,
1561 identifiers: &mut BTreeSet<String>,
1562) -> String {
1563 let id = stable_id(file, kind, range, "");
1564 let suffix = id.rsplit(':').next().unwrap_or(kind);
1565 let base = format!("__supercov_{kind}_{suffix}");
1566 let mut candidate = base.clone();
1567 let mut attempt = 0_usize;
1568 while !identifiers.insert(candidate.clone()) {
1569 attempt += 1;
1570 candidate = format!("{base}_{attempt}");
1571 }
1572 candidate
1573}
1574
1575fn own_breaks(body: &ast::BlockExpr, label: Option<ast::Label>) -> Vec<TextRange> {
1579 let own_label = label
1580 .and_then(|label| label.lifetime())
1581 .map(|lifetime| lifetime.text().to_string());
1582 body.syntax()
1583 .descendants()
1584 .filter_map(ast::BreakExpr::cast)
1585 .filter(|expression| match expression.lifetime() {
1586 Some(lifetime) => own_label.as_deref() == Some(lifetime.text().to_string().as_str()),
1587 None => !expression
1588 .syntax()
1589 .ancestors()
1590 .skip(1)
1591 .take_while(|ancestor| ancestor != body.syntax())
1592 .any(|ancestor| {
1593 ast::LoopExpr::can_cast(ancestor.kind())
1594 || ast::WhileExpr::can_cast(ancestor.kind())
1595 || ast::ForExpr::can_cast(ancestor.kind())
1596 || ast::ClosureExpr::can_cast(ancestor.kind())
1597 }),
1598 })
1599 .map(|expression| expression.syntax().text_range())
1600 .collect()
1601}
1602
1603fn instrument_let_chain(
1619 insertions: &mut Vec<Insertion>,
1620 runtime_path: &str,
1621 file: &str,
1622 condition: &ast::Expr,
1623 host: ChainHost<'_>,
1624 identifiers: &mut BTreeSet<String>,
1625) {
1626 let (kind, host_range, body, label) = match &host {
1629 ChainHost::If(expression) => (
1630 "if",
1631 range_after_attributes(*expression),
1632 expression.then_branch(),
1633 None,
1634 ),
1635 ChainHost::While(expression) => (
1636 "while",
1637 range_after_attributes(*expression),
1638 expression.loop_body(),
1639 expression.label(),
1640 ),
1641 };
1642 let Some(body) = body else {
1643 return;
1644 };
1645 let Some(body_offset) = block_entry_offset(&body) else {
1646 return;
1647 };
1648 let range = condition.syntax().text_range();
1649 let id = stable_id(file, "decision", range, kind);
1650 let mut atoms = Vec::new();
1651 RustObligationCollector::atomic_condition_ranges(condition, &mut atoms);
1652 let lets = condition
1653 .syntax()
1654 .descendants()
1655 .filter_map(ast::LetExpr::cast)
1656 .map(|expression| expression.syntax().text_range())
1657 .collect::<Vec<_>>();
1658 let frame = allocate_frame_name(file, condition, kind, identifiers);
1659 let table = allocate_chain_table_name(file, condition, identifiers);
1660 let single_let = atoms.len() == 1;
1661 let broke = match &host {
1662 ChainHost::While(_) if single_let => {
1663 Some(allocate_identifier(file, range, "broke", identifiers))
1664 }
1665 _ => None,
1666 };
1667
1668 let mut operators = Vec::new();
1669 for binary in condition
1670 .syntax()
1671 .descendants()
1672 .filter_map(ast::BinExpr::cast)
1673 {
1674 if !matches!(binary.op_kind(), Some(BinaryOp::LogicOp(LogicOp::And))) {
1677 continue;
1678 }
1679 let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
1680 continue;
1681 };
1682 if !has_let(&left) {
1683 continue;
1684 }
1685 let branch = stable_id(file, "branch", right.syntax().text_range(), "logical-and");
1686 let right_range = right.syntax().text_range();
1687 let Some(first) = atoms
1688 .iter()
1689 .position(|atom| right_range.contains_range(*atom))
1690 else {
1691 continue;
1692 };
1693 operators.push(format!(
1694 "({first}, {:?}, {:?})",
1695 format!("{branch}:short-circuit"),
1696 format!("{branch}:evaluated")
1697 ));
1698 }
1699
1700 let mark = format!("{runtime_path}::reached(&mut {frame}, 0);");
1701 let record_false = format!("{runtime_path}::decision_chain(&mut {frame}, false, {table});");
1702 let mut prefix = format!(
1703 "{{ const {table}: &[(usize, &str, &str)] = &[{}]; let mut {frame} = {runtime_path}::DecisionFrame::new({id:?}, {}); ",
1704 operators.join(", "),
1705 atoms.len()
1706 );
1707 if single_let {
1708 prefix.push_str(&mark);
1709 prefix.push(' ');
1710 }
1711 if let Some(broke) = &broke {
1712 prefix.push_str(&format!("let mut {broke} = false; "));
1713 }
1714 let suffix = match &host {
1715 ChainHost::If(expression) => match expression.else_branch() {
1716 Some(_) => " }".to_owned(),
1717 None => format!(" else {{ {record_false} }} }}"),
1718 },
1719 ChainHost::While(_) => match &broke {
1720 Some(broke) => format!(" if !{broke} {{ {mark} {record_false} }} }}"),
1721 None => format!(" {record_false} }}"),
1722 },
1723 };
1724 push_wrapper(insertions, host_range, host_range, 1, prefix, suffix);
1725 if !single_let {
1726 push_direct(
1727 insertions,
1728 usize::from(range.start()),
1729 format!("{runtime_path}::reached(&mut {frame}, 0) && "),
1730 );
1731 }
1732 for (index, atom) in atoms.iter().enumerate() {
1733 if lets.contains(atom) {
1734 if index > 0 {
1735 push_direct(
1736 insertions,
1737 usize::from(atom.start()),
1738 format!("{runtime_path}::reached(&mut {frame}, {index}) && "),
1739 );
1740 }
1741 } else {
1742 push_wrapper(
1743 insertions,
1744 *atom,
1745 *atom,
1746 1,
1747 format!("{runtime_path}::condition(("),
1748 format!("), &mut {frame}, {index})"),
1749 );
1750 }
1751 }
1752 let mut entry = String::new();
1753 if broke.is_some() {
1754 entry.push_str(&format!("\n{mark}"));
1755 }
1756 entry.push_str(&format!(
1757 "\n{runtime_path}::decision_chain(&mut {frame}, true, {table});"
1758 ));
1759 push_direct(insertions, body_offset, entry);
1760 if let Some(broke) = &broke {
1761 for break_range in own_breaks(&body, label) {
1762 push_wrapper(
1763 insertions,
1764 break_range,
1765 break_range,
1766 0,
1767 format!("{{ {broke} = true; "),
1768 " }".into(),
1769 );
1770 }
1771 }
1772 if let ChainHost::If(expression) = &host {
1773 match expression.else_branch() {
1774 Some(ast::ElseBranch::Block(block)) => {
1775 if let Some(offset) = block_entry_offset(&block) {
1776 push_direct(insertions, offset, format!("\n{record_false}"));
1777 }
1778 }
1779 Some(ast::ElseBranch::IfExpr(nested)) => {
1780 let nested_range = nested.syntax().text_range();
1781 push_wrapper(
1782 insertions,
1783 nested_range,
1784 nested_range,
1785 0,
1786 format!("{{ {record_false} "),
1787 " }".into(),
1788 );
1789 }
1790 None => {}
1791 }
1792 }
1793}
1794
1795fn enclosing_block_entry(node: &ra_ap_syntax::SyntaxNode) -> Option<usize> {
1799 node.ancestors()
1800 .skip(1)
1801 .find_map(ast::BlockExpr::cast)
1802 .and_then(|block| block_entry_offset(&block))
1803}
1804
1805fn plain_block(block: &ast::BlockExpr) -> bool {
1809 block
1810 .syntax()
1811 .first_token()
1812 .is_some_and(|token| token.kind() == SyntaxKind::L_CURLY)
1813}
1814
1815fn instrument_decision(
1816 insertions: &mut Vec<Insertion>,
1817 runtime_path: &str,
1818 file: &str,
1819 condition: &ast::Expr,
1820 kind: &str,
1821 frame_name: &str,
1822) -> bool {
1823 if cannot_carry_probe(condition.syntax())
1824 || condition
1825 .syntax()
1826 .descendants()
1827 .any(|node| ast::LetExpr::can_cast(node.kind()))
1828 {
1829 return false;
1830 }
1831 let range = condition.syntax().text_range();
1832 let id = stable_id(file, "decision", range, kind);
1833 let mut condition_ranges = Vec::new();
1834 RustObligationCollector::atomic_condition_ranges(condition, &mut condition_ranges);
1835 push_wrapper(
1836 insertions,
1837 range,
1838 range,
1839 0,
1840 format!(
1841 "({{ let mut {frame_name} = {runtime_path}::DecisionFrame::new({id:?}, {}); {runtime_path}::decision((",
1842 condition_ranges.len()
1843 ),
1844 format!("), &mut {frame_name}) }})"),
1845 );
1846 for (index, atomic_range) in condition_ranges.into_iter().enumerate() {
1850 push_wrapper(
1851 insertions,
1852 atomic_range,
1853 atomic_range,
1854 1,
1855 format!("{runtime_path}::condition(("),
1856 format!("), &mut {frame_name}, {index})"),
1857 );
1858 }
1859 true
1860}
1861
1862pub fn instrument_rust_source(
1872 file: &str,
1873 source: &str,
1874 runtime_path: &str,
1875) -> Result<RustInstrumentedSource, RustInstrumenterError> {
1876 instrument_rust_file(file, source, runtime_path, test_code_path(file), None)
1877}
1878
1879pub fn instrument_rust_file(
1884 file: &str,
1885 source: &str,
1886 runtime_path: &str,
1887 test_file: bool,
1888 cfg: Option<&TargetCfg>,
1889) -> Result<RustInstrumentedSource, RustInstrumenterError> {
1890 if !valid_runtime_path(runtime_path) {
1891 return Err(RustInstrumenterError::InvalidRuntimePath);
1892 }
1893 let parsed = parse_for_instrumentation(source)?;
1899 let tests = test_code_ranges(parsed.tree.syntax(), test_file);
1900 let compiled_out = compiled_out_ranges(parsed.tree.syntax(), cfg);
1901 let mut manifest = RustObligationCollector::new(file, source)
1902 .leaving_out(tests.clone(), compiled_out.clone())
1903 .collect(&parsed.tree, &parsed.assertions, &parsed.matches)?;
1904 let ParsedSource {
1905 tree,
1906 assertions,
1907 assertion_calls,
1908 matches,
1909 edition,
1910 } = parsed;
1911 let root = tree.syntax();
1912 let mut insertions = Vec::new();
1913 let mut identifiers = root
1914 .descendants_with_tokens()
1915 .filter_map(|element| element.into_token())
1916 .filter(|token| token.kind() == SyntaxKind::IDENT)
1917 .map(|token| token.text().to_string())
1918 .collect::<BTreeSet<_>>();
1919
1920 let mut skipped_attributed_statement = false;
1921 let attributed_probe = |insertions: &mut Vec<Insertion>,
1932 skipped: &mut bool,
1933 expression: Option<ast::Expr>,
1934 has_attrs: bool,
1935 statement: bool,
1939 trailing: bool,
1940 range: TextRange,
1941 id: String| {
1942 if !has_attrs {
1943 push_direct(
1944 insertions,
1945 usize::from(range.start()),
1946 format!("{runtime_path}::hit({id:?});"),
1947 );
1948 return;
1949 }
1950 let Some(expression) = expression else {
1951 *skipped = true;
1952 return;
1953 };
1954 if let ast::Expr::BlockExpr(block) = &expression
1955 && let Some(offset) = block_entry_offset(block)
1956 {
1957 push_direct(
1958 insertions,
1959 offset,
1960 format!("\n{runtime_path}::hit({id:?});"),
1961 );
1962 return;
1963 }
1964 if trailing && let ast::Expr::MacroExpr(_) = &expression {
1975 if expression.attrs().any(|attribute| {
1976 attribute.meta().is_some_and(|meta| {
1979 let text = meta.syntax().text().to_string();
1980 let name = text
1981 .chars()
1982 .take_while(|character| character.is_alphanumeric() || *character == '_')
1983 .collect::<String>();
1984 matches!(name.as_str(), "cfg" | "cfg_attr")
1985 })
1986 }) {
1987 *skipped = true;
1988 return;
1989 }
1990 let range = expression.syntax().text_range();
1991 push_wrapper(
1992 insertions,
1993 range,
1994 range,
1995 0,
1996 format!("{{ {runtime_path}::hit({id:?}); "),
1997 " }".into(),
1998 );
1999 return;
2000 }
2001 if statement && let ast::Expr::MacroExpr(_) = &expression {
2008 let start = expression.attrs().last().map_or_else(
2009 || expression.syntax().text_range().start(),
2010 |attribute| attribute.syntax().text_range().end(),
2011 );
2012 let wrapped = TextRange::new(start, expression.syntax().text_range().end());
2013 push_wrapper(
2014 insertions,
2015 wrapped,
2016 wrapped,
2017 0,
2018 format!(" {{ {runtime_path}::hit({id:?}); "),
2019 "; }".into(),
2020 );
2021 return;
2022 }
2023 let start = expression.attrs().last().map_or_else(
2029 || expression.syntax().text_range().start(),
2030 |attribute| attribute.syntax().text_range().end(),
2031 );
2032 let wrapped = TextRange::new(start, expression.syntax().text_range().end());
2033 push_wrapper(
2034 insertions,
2035 wrapped,
2036 wrapped,
2037 0,
2038 format!(" {{ {runtime_path}::hit({id:?}); ("),
2039 ") }".into(),
2040 );
2041 };
2042 for list in root.descendants().filter_map(ast::StmtList::cast) {
2043 let last_statement = list.statements().last();
2044 for statement in list.statements() {
2045 let (range, expression, has_attrs, statement_expression, trailing) = match &statement {
2046 ast::Stmt::ExprStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
2047 let expression = statement.expr();
2048 let has_attrs = expression
2051 .as_ref()
2052 .is_some_and(|expression| expression.attrs().next().is_some());
2053 let trailing = statement.semicolon_token().is_none()
2056 && list.tail_expr().is_none()
2057 && last_statement.as_ref() == Some(&ast::Stmt::ExprStmt(statement.clone()));
2058 (
2059 statement.syntax().text_range(),
2060 expression,
2061 has_attrs,
2062 true,
2063 trailing,
2064 )
2065 }
2066 ast::Stmt::LetStmt(statement) if !cannot_carry_probe(statement.syntax()) => {
2067 let has_attrs = statement.attrs().next().is_some();
2068 let initializer = has_attrs.then(|| statement.initializer()).flatten();
2071 (
2072 statement.syntax().text_range(),
2073 initializer,
2074 has_attrs,
2075 false,
2076 false,
2077 )
2078 }
2079 _ => continue,
2080 };
2081 let id = stable_id(file, "statement", range, "");
2082 if !has_attrs
2089 && !trailing
2090 && assertion_calls
2091 .iter()
2092 .any(|call| range.contains_range(*call))
2093 {
2094 push_direct(
2095 &mut insertions,
2096 usize::from(range.end()),
2097 format!("{runtime_path}::assertion({id:?});"),
2098 );
2099 }
2100 attributed_probe(
2101 &mut insertions,
2102 &mut skipped_attributed_statement,
2103 expression,
2104 has_attrs,
2105 statement_expression,
2106 trailing,
2107 range,
2108 id,
2109 );
2110 }
2111 if let Some(tail) = list
2112 .tail_expr()
2113 .filter(|tail| !cannot_carry_probe(tail.syntax()))
2114 {
2115 let range = tail.syntax().text_range();
2116 let id = stable_id(file, "statement", range, "");
2117 let has_attrs = tail.attrs().next().is_some();
2118 attributed_probe(
2119 &mut insertions,
2120 &mut skipped_attributed_statement,
2121 Some(tail),
2122 has_attrs,
2123 true,
2124 true,
2125 range,
2126 id,
2127 );
2128 }
2129 }
2130
2131 for function in root.descendants().filter_map(ast::Fn::cast) {
2132 if cannot_carry_probe(function.syntax()) {
2135 continue;
2136 }
2137 let Some(body) = function.body() else {
2138 continue;
2139 };
2140 let label = function.name().map(|name| name.text().to_string());
2141 let id = stable_id(
2142 file,
2143 "function",
2144 function.syntax().text_range(),
2145 label.as_deref().unwrap_or(""),
2146 );
2147 if let Some(offset) = block_entry_offset(&body) {
2148 push_direct(
2149 &mut insertions,
2150 offset,
2151 format!("\n{runtime_path}::hit({id:?});"),
2152 );
2153 }
2154 }
2155
2156 for closure in root.descendants().filter_map(ast::ClosureExpr::cast) {
2157 let Some(body) = closure.body() else {
2158 continue;
2159 };
2160 if cannot_carry_probe(body.syntax()) {
2161 continue;
2162 }
2163 let id = stable_id(file, "function", closure.syntax().text_range(), "<closure>");
2164 if let ast::Expr::BlockExpr(block) = &body {
2165 if let Some(offset) = block_entry_offset(block) {
2166 push_direct(
2167 &mut insertions,
2168 offset,
2169 format!("\n{runtime_path}::hit({id:?});"),
2170 );
2171 }
2172 } else {
2173 let range = body.syntax().text_range();
2174 push_wrapper(
2175 &mut insertions,
2176 range,
2177 closure.syntax().text_range(),
2178 0,
2179 format!("{{ {runtime_path}::hit({id:?}); ("),
2180 ") }".into(),
2181 );
2182 }
2183 }
2184
2185 for expression in root.descendants().filter_map(ast::MatchExpr::cast) {
2192 if cannot_carry_probe(expression.syntax()) {
2193 continue;
2194 }
2195 let Some(list) = expression.match_arm_list() else {
2196 continue;
2197 };
2198 let arms = list.arms().collect::<Vec<_>>();
2199 if arms.is_empty() {
2200 continue;
2201 }
2202 let Some(table_offset) = enclosing_block_entry(expression.syntax()) else {
2203 continue;
2204 };
2205 let table = allocate_table_name(file, &expression, &mut identifiers);
2206 let entries = arms
2207 .iter()
2208 .map(|arm| {
2209 let id = stable_id(file, "branch", arm.syntax().text_range(), "match-arm");
2210 format!(
2211 "{:?}, {:?}",
2212 format!("{id}:missed"),
2213 format!("{id}:selected")
2214 )
2215 })
2216 .collect::<Vec<_>>()
2217 .join(", ");
2218 push_direct(
2219 &mut insertions,
2220 table_offset,
2221 format!("\nconst {table}: &[&str] = &[{entries}];"),
2222 );
2223 for (index, arm) in arms.iter().enumerate() {
2224 let Some(body) = arm.expr() else {
2225 continue;
2226 };
2227 let call = format!("{runtime_path}::arms({table}, {index});");
2228 match &body {
2229 ast::Expr::BlockExpr(block) if plain_block(block) => {
2230 if let Some(offset) = block_entry_offset(block) {
2231 push_direct(&mut insertions, offset, format!("\n{call}"));
2232 }
2233 }
2234 _ => push_wrapper(
2235 &mut insertions,
2236 body.syntax().text_range(),
2237 arm.syntax().text_range(),
2238 0,
2239 format!("{{ {call} ("),
2240 ") }".into(),
2241 ),
2242 }
2243 }
2244 }
2245
2246 for binary in root.descendants().filter_map(ast::BinExpr::cast) {
2250 let short_circuits_when = match binary.op_kind() {
2251 Some(BinaryOp::LogicOp(LogicOp::And)) => false,
2252 Some(BinaryOp::LogicOp(LogicOp::Or)) => true,
2253 _ => continue,
2254 };
2255 if cannot_carry_probe(binary.syntax()) {
2256 continue;
2257 }
2258 let (Some(left), Some(right)) = (binary.lhs(), binary.rhs()) else {
2259 continue;
2260 };
2261 if has_let(&left) {
2265 continue;
2266 }
2267 let kind = if short_circuits_when {
2268 "logical-or"
2269 } else {
2270 "logical-and"
2271 };
2272 let id = stable_id(file, "branch", right.syntax().text_range(), kind);
2273 push_wrapper(
2274 &mut insertions,
2275 left.syntax().text_range(),
2276 binary.syntax().text_range(),
2277 2,
2278 format!("{runtime_path}::logical(("),
2279 format!(
2280 "), {short_circuits_when}, {:?}, {:?})",
2281 format!("{id}:short-circuit"),
2282 format!("{id}:evaluated")
2283 ),
2284 );
2285 }
2286
2287 for expression in root.descendants().filter_map(ast::ForExpr::cast) {
2291 if cannot_carry_probe(expression.syntax()) {
2292 continue;
2293 }
2294 let Some(iterable) = expression.iterable() else {
2295 continue;
2296 };
2297 let id = stable_id(file, "branch", expression.syntax().text_range(), "for-loop");
2298 push_wrapper(
2302 &mut insertions,
2303 iterable.syntax().text_range(),
2304 iterable.syntax().text_range(),
2305 0,
2306 format!("{runtime_path}::for_loop(("),
2307 format!(
2308 "), {:?}, {:?})",
2309 format!("{id}:zero"),
2310 format!("{id}:entered")
2311 ),
2312 );
2313 }
2314
2315 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
2319 if cannot_carry_probe(expression.syntax()) {
2320 continue;
2321 }
2322 let Some(offset) = expression.loop_body().as_ref().and_then(block_entry_offset) else {
2323 continue;
2324 };
2325 let id = stable_id(
2326 file,
2327 "branch",
2328 expression.syntax().text_range(),
2329 "while-loop",
2330 );
2331 let flag = allocate_flag_name(file, &expression, &mut identifiers);
2332 let range = range_after_attributes(&expression);
2333 push_wrapper(
2334 &mut insertions,
2335 range,
2336 range,
2337 0,
2338 format!("{{ let mut {flag} = true; "),
2339 format!(
2340 " {runtime_path}::zero_iterations({flag}, {:?}) }}",
2341 format!("{id}:zero")
2342 ),
2343 );
2344 push_direct(
2345 &mut insertions,
2346 offset,
2347 format!(
2348 "\n{runtime_path}::entered(&mut {flag}, {:?});",
2349 format!("{id}:entered")
2350 ),
2351 );
2352 }
2353
2354 for expression in root.descendants().filter_map(ast::TryExpr::cast) {
2358 if cannot_carry_probe(expression.syntax()) {
2359 continue;
2360 }
2361 let Some(operand) = expression.expr() else {
2362 continue;
2363 };
2364 let id = stable_id(
2365 file,
2366 "branch",
2367 expression.syntax().text_range(),
2368 "try-operator",
2369 );
2370 push_wrapper(
2373 &mut insertions,
2374 operand.syntax().text_range(),
2375 operand.syntax().text_range(),
2376 0,
2377 format!("{runtime_path}::TryProbe::probe(("),
2378 format!(
2379 "), {:?}, {:?})",
2380 format!("{id}:continued"),
2381 format!("{id}:returned")
2382 ),
2383 );
2384 }
2385
2386 for expression in root.descendants().filter_map(ast::IfExpr::cast) {
2387 let Some(condition) = expression.condition() else {
2388 continue;
2389 };
2390 if has_let(&condition) {
2391 if !cannot_carry_probe(condition.syntax()) {
2392 instrument_let_chain(
2393 &mut insertions,
2394 runtime_path,
2395 file,
2396 &condition,
2397 ChainHost::If(&expression),
2398 &mut identifiers,
2399 );
2400 }
2401 continue;
2402 }
2403 let frame_name = allocate_frame_name(file, &condition, "if", &mut identifiers);
2404 instrument_decision(
2405 &mut insertions,
2406 runtime_path,
2407 file,
2408 &condition,
2409 "if",
2410 &frame_name,
2411 );
2412 }
2413 for expression in root.descendants().filter_map(ast::WhileExpr::cast) {
2414 let Some(condition) = expression.condition() else {
2415 continue;
2416 };
2417 if has_let(&condition) {
2418 if !cannot_carry_probe(condition.syntax()) {
2419 instrument_let_chain(
2420 &mut insertions,
2421 runtime_path,
2422 file,
2423 &condition,
2424 ChainHost::While(&expression),
2425 &mut identifiers,
2426 );
2427 }
2428 continue;
2429 }
2430 let frame_name = allocate_frame_name(file, &condition, "while", &mut identifiers);
2431 instrument_decision(
2432 &mut insertions,
2433 runtime_path,
2434 file,
2435 &condition,
2436 "while",
2437 &frame_name,
2438 );
2439 }
2440 for guard in root.descendants().filter_map(ast::MatchGuard::cast) {
2441 if let Some(condition) = guard.condition() {
2442 let frame_name = allocate_frame_name(file, &condition, "match-guard", &mut identifiers);
2443 instrument_decision(
2444 &mut insertions,
2445 runtime_path,
2446 file,
2447 &condition,
2448 "match-guard",
2449 &frame_name,
2450 );
2451 }
2452 }
2453 for arguments in &assertions {
2460 let Some(condition) = assertion_condition(root, *arguments) else {
2461 continue;
2462 };
2463 let frame_name = allocate_frame_name(file, &condition, "assert", &mut identifiers);
2464 if !instrument_decision(
2465 &mut insertions,
2466 runtime_path,
2467 file,
2468 &condition,
2469 "assert",
2470 &frame_name,
2471 ) {
2472 continue;
2473 }
2474 if assertion_argument_count(root, *arguments) == 1 {
2475 let range = condition.syntax().text_range();
2476 let original = &source[usize::from(range.start())..usize::from(range.end())];
2477 push_direct(
2478 &mut insertions,
2479 usize::from(range.end()),
2480 format!(", \"assertion failed: {{}}\", stringify!({original})"),
2481 );
2482 }
2483 }
2484 for expression in standalone_matches(root, &matches, &assertions) {
2487 let frame_name = allocate_frame_name(file, &expression, "matches", &mut identifiers);
2488 instrument_decision(
2489 &mut insertions,
2490 runtime_path,
2491 file,
2492 &expression,
2493 "matches",
2494 &frame_name,
2495 );
2496 }
2497
2498 if !compiled_out.is_empty() {
2501 insertions.retain(|insertion| {
2502 !compiled_out.iter().any(|range| {
2503 usize::from(range.start()) <= insertion.offset
2504 && insertion.offset <= usize::from(range.end())
2505 })
2506 });
2507 }
2508 if !tests.is_empty() {
2511 let inside = |offset: usize| {
2512 tests.iter().any(|test| {
2513 usize::from(test.start()) <= offset && offset <= usize::from(test.end())
2514 })
2515 };
2516 let markers = manifest
2517 .assertion_sites
2518 .iter()
2519 .map(|site| {
2520 (
2521 format!("{runtime_path}::assertion({:?});", site.id),
2522 site.id.clone(),
2523 )
2524 })
2525 .collect::<std::collections::BTreeMap<_, _>>();
2526 let mut marked = BTreeSet::new();
2527 insertions.retain(|insertion| {
2528 if !inside(insertion.offset) {
2529 return true;
2530 }
2531 match markers.get(&insertion.text) {
2532 Some(id) => {
2533 marked.insert(id.clone());
2534 true
2535 }
2536 None => false,
2537 }
2538 });
2539 manifest
2540 .assertion_sites
2541 .retain(|site| marked.contains(&site.id));
2542 }
2543
2544 if skipped_attributed_statement && !test_file {
2545 add_manifest_limitation(
2546 &mut manifest,
2547 file,
2548 "rust-attributed-statement-probes-not-injected",
2549 "A `let` without an initializer that carries outer attributes has no expression to hold a probe",
2550 );
2551 }
2552 manifest.limitations.sort_by(|left, right| {
2553 left.get("id")
2554 .and_then(|value| value.as_str())
2555 .cmp(&right.get("id").and_then(|value| value.as_str()))
2556 });
2557
2558 let code = apply_insertions(source, insertions)?;
2559 let transformed = SourceFile::parse(&code, edition);
2560 let errors = transformed
2561 .errors()
2562 .into_iter()
2563 .map(|error| error.to_string())
2564 .collect::<Vec<_>>();
2565 if !errors.is_empty() {
2566 if let Some(directory) = std::env::var_os(FAILED_TRANSFORM_DUMP_ENV) {
2570 let name = file.replace(['/', '\\'], "__");
2571 let _ = std::fs::create_dir_all(&directory);
2572 let _ = std::fs::write(std::path::Path::new(&directory).join(name), &code);
2573 }
2574 return Err(RustInstrumenterError::Parse(errors));
2575 }
2576 Ok(RustInstrumentedSource { code, manifest })
2577}
2578
2579#[cfg(test)]
2580mod tests {
2581 use std::{
2582 fs,
2583 process::Command,
2584 time::{SystemTime, UNIX_EPOCH},
2585 };
2586
2587 use super::*;
2588
2589 fn limitation_kind_of(limitation: &serde_json::Value) -> Option<&str> {
2591 limitation.get("id")?.as_str()?.split('#').next()
2592 }
2593
2594 const NOOP_RUNTIME: &str = r#"
2595#[doc(hidden)]
2596mod __supercov_runtime_v1 {
2597 pub struct DecisionFrame;
2598 impl DecisionFrame {
2599 pub fn new(_: &'static str, _: usize) -> Self { Self }
2600 }
2601 pub fn hit(_: &'static str) {}
2602 pub fn arms(_: &[&'static str], _: usize) {}
2603 pub fn logical(left: bool, _: bool, _: &'static str, _: &'static str) -> bool { left }
2604 pub fn for_loop<I: IntoIterator>(iterable: I, _: &'static str, _: &'static str) -> I::IntoIter {
2605 iterable.into_iter()
2606 }
2607 pub fn entered(_: &mut bool, _: &'static str) {}
2608 pub fn zero_iterations(_: bool, _: &'static str) {}
2609 pub trait TryProbe: Sized {
2610 fn probe(self, _: &'static str, _: &'static str) -> Self { self }
2611 }
2612 impl<T> TryProbe for T {}
2613 pub fn condition<V: std::ops::Not<Output = bool>>(value: V, _: &mut DecisionFrame, _: usize) -> bool { !!value }
2614 pub fn decision(value: bool, _: &mut DecisionFrame) -> bool { value }
2615 pub fn reached(_: &mut DecisionFrame, _: usize) -> bool { true }
2616 pub fn decision_chain(_: &mut DecisionFrame, _: bool, _: &[(usize, &'static str, &'static str)]) {}
2617 pub fn assertion(_: &'static str) {}
2618}
2619"#;
2620
2621 fn compile_and_run(source: &str, name: &str) -> std::process::Output {
2622 compile_and_run_edition(source, name, "2024")
2623 }
2624
2625 fn compile_and_run_edition(source: &str, name: &str, edition: &str) -> std::process::Output {
2626 let nonce = SystemTime::now()
2627 .duration_since(UNIX_EPOCH)
2628 .unwrap()
2629 .as_nanos();
2630 let directory = std::env::temp_dir().join(format!(
2631 "supercov-rust-transform-{}-{nonce}-{name}",
2632 std::process::id()
2633 ));
2634 fs::create_dir(&directory).unwrap();
2635 let input = directory.join("main.rs");
2636 let binary = directory.join("program");
2637 fs::write(&input, source).unwrap();
2638 let compile = Command::new("rustc")
2639 .arg(format!("--edition={edition}"))
2640 .arg(&input)
2641 .arg("-o")
2642 .arg(&binary)
2643 .output()
2644 .unwrap();
2645 assert!(
2646 compile.status.success(),
2647 "rustc failed:\n{}\nsource:\n{source}",
2648 String::from_utf8_lossy(&compile.stderr)
2649 );
2650 let output = Command::new(&binary).output().unwrap();
2651 fs::remove_dir_all(directory).unwrap();
2652 output
2653 }
2654
2655 #[test]
2656 fn a_target_cfg_decides_only_what_the_target_settles() {
2657 let cfg = TargetCfg::new(
2658 &[
2659 "unix",
2660 "target_os=\"linux\"",
2661 "target_family=\"unix\"",
2662 "target_pointer_width=\"64\"",
2663 "debug_assertions",
2664 ]
2665 .map(|value| value.parse::<cargo_platform::Cfg>().unwrap()),
2666 );
2667 let decide = |expression: &str| cfg.evaluate(&expression.parse().unwrap());
2668 assert_eq!(decide("unix"), Some(true));
2669 assert_eq!(decide("windows"), Some(false));
2670 assert_eq!(decide("target_os = \"macos\""), Some(false));
2671 assert_eq!(decide("not(target_os = \"linux\")"), Some(false));
2672 assert_eq!(
2673 decide("all(unix, target_pointer_width = \"64\")"),
2674 Some(true)
2675 );
2676 assert_eq!(decide("any(windows, target_os = \"wasi\")"), Some(false));
2677 assert_eq!(decide("feature = \"extra\""), None);
2679 assert_eq!(decide("not(feature = \"extra\")"), None);
2680 assert_eq!(decide("debug_assertions"), None);
2681 assert_eq!(decide("all(windows, feature = \"extra\")"), Some(false));
2682 assert_eq!(decide("any(unix, feature = \"extra\")"), Some(true));
2683 assert_eq!(decide("any(windows, feature = \"extra\")"), None);
2684
2685 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";
2686 let instrumented = instrument_rust_file(
2687 "src/lib.rs",
2688 source,
2689 "crate::__supercov_runtime_v1",
2690 false,
2691 Some(&cfg),
2692 )
2693 .unwrap();
2694 let lines = instrumented
2695 .manifest
2696 .points
2697 .iter()
2698 .map(|point| point.line)
2699 .collect::<BTreeSet<_>>();
2700 assert!(
2701 !lines.contains(&3),
2702 "the windows item is left out: {lines:?}"
2703 );
2704 assert!(lines.contains(&5) && lines.contains(&7), "{lines:?}");
2705 let windows = &instrumented.code[instrumented.code.find("#[cfg(windows)]").unwrap()
2706 ..instrumented.code.find("#[cfg(unix)]").unwrap()];
2707 assert!(!windows.contains("__supercov_runtime_v1"), "{windows}");
2708 }
2709
2710 #[test]
2711 fn test_code_is_known_by_where_it_sits() {
2712 for file in [
2713 "tests/it.rs",
2714 "tests/common/mod.rs",
2715 "crates/a/tests/it.rs",
2716 "examples/demo.rs",
2717 "benches/speed.rs",
2718 "src/tests.rs",
2719 "src/parser_tests.rs",
2720 "src/parser-tests.rs",
2721 ] {
2722 assert!(test_code_path(file), "{file}");
2723 }
2724 for file in [
2725 "src/lib.rs",
2726 "src/attests.rs",
2727 "src/_tests.rs",
2728 "crates/foo-tests/src/lib.rs",
2729 "src/testing.rs",
2730 ] {
2731 assert!(!test_code_path(file), "{file}");
2732 }
2733 }
2734
2735 #[test]
2736 fn test_items_keep_only_their_assertion_markers() {
2737 let source = r#"pub fn add(a: i32, b: i32) -> i32 {
2738 if a > 0 && b > 0 { a + b } else { 0 }
2739}
2740
2741#[cfg(test)]
2742mod tests {
2743 use super::*;
2744 #[test]
2745 fn adds() {
2746 let total = add(1, 2);
2747 assert_eq!(total, 3);
2748 }
2749}
2750
2751#[cfg(all(test, feature = "slow"))]
2752fn slow_helper() -> bool { true || false }
2753
2754#[tokio::test]
2755async fn async_adds() {
2756 assert!(add(2, 2) == 4);
2757}
2758"#;
2759 let transformed =
2760 instrument_rust_source("src/lib.rs", source, "crate::__supercov_runtime_v1").unwrap();
2761 let manifest = &transformed.manifest;
2762 assert!(
2763 manifest.points.iter().all(|point| point.line <= 3),
2764 "{:?}",
2765 manifest.points
2766 );
2767 assert!(manifest.decisions.iter().all(|decision| decision.line == 2));
2768 assert!(manifest.branches.iter().all(|branch| branch.line == 2));
2769 assert_eq!(manifest.decisions.len(), 1);
2770 let sites = manifest
2771 .assertion_sites
2772 .iter()
2773 .map(|site| site.source.as_str())
2774 .collect::<BTreeSet<_>>();
2775 assert_eq!(
2776 sites,
2777 BTreeSet::from(["assert!(add(2, 2) == 4);", "assert_eq!(total, 3);"])
2778 );
2779 let tests = &transformed.code[transformed.code.find("#[cfg(test)]").unwrap()..];
2780 assert_eq!(tests.matches("::assertion(").count(), 2, "{tests}");
2781 assert!(!tests.contains("::hit("), "{tests}");
2782 assert!(!tests.contains("::logical("), "{tests}");
2783
2784 let file = instrument_rust_source(
2786 "tests/it.rs",
2787 "#[test]\nfn it() {\n let x = 1;\n assert_eq!(x, 1);\n}\n",
2788 "crate::__supercov_runtime_v1",
2789 )
2790 .unwrap();
2791 assert!(file.manifest.points.is_empty());
2792 assert_eq!(file.manifest.assertion_sites.len(), 1);
2793 assert!(!file.code.contains("::hit("));
2794 }
2795
2796 #[test]
2797 fn discovers_rust_obligations_with_exact_ranges_and_stable_ids() {
2798 let source = r#"fn classify<T>(values: &[T], first: bool, second: bool, third: bool) -> Option<&T> {
2799 let picked = if first && (second || third) {
2800 values.first()?
2801 } else {
2802 None
2803 };
2804 for value in values {
2805 if first || second {
2806 return Some(value);
2807 }
2808 }
2809 match picked {
2810 Some(value) if second && third => Some(value),
2811 _ => None,
2812 }
2813}
2814
2815fn closure(value: i32) -> bool {
2816 (|candidate| candidate > 0)(value)
2817}
2818"#;
2819 let first = build_rust_manifest("src/lib.rs", source).unwrap();
2820 let second = build_rust_manifest("src/lib.rs", source).unwrap();
2821 assert_eq!(first, second);
2822 assert!(first.points.iter().any(|point| {
2823 point.kind == PointKind::Function && point.label.as_deref() == Some("classify")
2824 }));
2825 assert!(first.points.iter().any(|point| {
2826 point.kind == PointKind::Function && point.label.as_deref() == Some("<closure>")
2827 }));
2828 let first_if = first
2829 .decisions
2830 .iter()
2831 .find(|decision| decision.line == 2)
2832 .unwrap();
2833 assert_eq!(first_if.conditions, ["first", "second", "third"]);
2834 assert_eq!(first_if.column, 20);
2835 assert!(
2836 first
2837 .branches
2838 .iter()
2839 .any(|branch| branch.kind == "for-loop")
2840 );
2841 let mut arms = first
2842 .branches
2843 .iter()
2844 .filter(|branch| branch.kind == "match-arm")
2845 .collect::<Vec<_>>();
2846 arms.sort_by_key(|branch| branch.line);
2847 assert_eq!(arms.len(), 2);
2848 assert_eq!(
2849 arms[0]
2850 .alternatives
2851 .iter()
2852 .map(|alternative| alternative.label.as_str())
2853 .collect::<Vec<_>>(),
2854 ["not selected", "selected"]
2855 );
2856 assert_eq!(
2859 arms[1]
2860 .alternatives
2861 .iter()
2862 .map(|alternative| alternative.label.as_str())
2863 .collect::<Vec<_>>(),
2864 ["selected"]
2865 );
2866 assert!(
2867 first
2868 .branches
2869 .iter()
2870 .any(|branch| branch.kind == "try-operator")
2871 );
2872 assert!(first.decisions.iter().all(|decision| {
2873 decision.id.starts_with("rs:decision:") && decision.conditions.len() >= 2
2874 }));
2875 assert!(first.limitations.is_empty());
2876 }
2877
2878 #[test]
2879 fn declares_macro_and_const_boundaries_instead_of_hiding_them() {
2880 let source = r#"const fn doubled(value: usize) -> usize { value * 2 }
2881
2882macro_rules! noop {
2883 () => {};
2884}
2885
2886fn checked(value: bool) -> bool {
2887 assert!(value);
2888 noop!();
2889 const { doubled(2) == 4 }
2890}
2891"#;
2892 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
2893 assert!(manifest.decisions.iter().any(|decision| {
2896 decision.line == 8 && decision.source == "value" && decision.conditions == ["value"]
2897 }));
2898 let ids = manifest
2899 .limitations
2900 .iter()
2901 .filter_map(limitation_kind_of)
2902 .collect::<BTreeSet<_>>();
2903 assert_eq!(
2904 ids,
2905 BTreeSet::from([
2906 "rust-const-context-not-instrumented",
2907 "rust-macro-expansion-not-instrumented"
2908 ])
2909 );
2910 assert!(!manifest.points.iter().any(|point| {
2911 point.kind == PointKind::Function && point.label.as_deref() == Some("doubled")
2912 }));
2913 }
2914
2915 #[test]
2916 fn transforms_points_and_nested_decisions_without_changing_behavior() {
2917 let source = r#"use std::sync::atomic::{AtomicUsize, Ordering};
2918
2919static CALLS: AtomicUsize = AtomicUsize::new(0);
2920
2921fn observed(name: &str, value: bool) -> bool {
2922 let order = CALLS.fetch_add(1, Ordering::SeqCst);
2923 println!("{order}:{name}:{value}");
2924 value
2925}
2926
2927fn classify(first: bool, second: bool, third: bool) -> i32 {
2928 if observed("a", first) && (observed("b", second) || observed("c", third)) {
2929 7
2930 } else {
2931 3
2932 }
2933}
2934
2935fn main() {
2936 let closure = |value: i32| value + 1;
2937 println!("result={}", closure(classify(true, false, true)));
2938}
2939"#;
2940 let transformed =
2941 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
2942 assert!(transformed.code.contains("::condition("));
2943 assert!(transformed.code.contains("::decision("));
2944 assert!(transformed.code.contains("::hit("));
2945 let original = compile_and_run(source, "original");
2946 let instrumented = compile_and_run(
2947 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
2948 "instrumented",
2949 );
2950 assert_eq!(instrumented.status, original.status);
2951 assert_eq!(instrumented.stdout, original.stdout);
2952 assert_eq!(instrumented.stderr, original.stderr);
2953 }
2954
2955 #[test]
2956 fn let_chains_take_derived_condition_probes_and_const_contexts_stay_declared() {
2957 let source = r#"const fn enabled(value: bool) -> bool {
2958 if value { true } else { false }
2959}
2960
2961fn classify(value: Option<bool>, fallback: bool) -> bool {
2962 if let Some(inner) = value && inner && fallback { true } else { false }
2963}
2964"#;
2965 let transformed =
2966 instrument_rust_source("src/lib.rs", source, "crate::__supercov_runtime_v1").unwrap();
2967 let ids = transformed
2968 .manifest
2969 .limitations
2970 .iter()
2971 .filter_map(limitation_kind_of)
2972 .collect::<BTreeSet<_>>();
2973 assert!(ids.contains("rust-const-context-not-instrumented"));
2974 assert!(!ids.contains("rust-let-chain-probes-not-injected"));
2975 assert!(
2978 transformed
2979 .code
2980 .contains("::reached(&mut __supercov_decision_")
2981 );
2982 assert!(
2983 transformed
2984 .code
2985 .contains("::condition((inner), &mut __supercov_decision_")
2986 );
2987 assert!(
2988 transformed
2989 .code
2990 .contains("::decision_chain(&mut __supercov_decision_")
2991 );
2992 assert!(transformed.code.contains("&& let Some(inner) = value &&"));
2993 assert!(!transformed.code.contains("condition((let"));
2994 }
2995
2996 #[test]
2997 fn std_macro_arguments_take_probes_and_assertions_are_decisions() {
2998 let source = r#"use std::fmt::Write as _;
2999
3000fn classify(values: &[i32], strict: bool) -> String {
3001 let mut out = String::new();
3002 assert!(values.len() < 10 && (strict || !values.is_empty()), "bad input {:?}", values);
3003 debug_assert!(values.iter().all(|v| *v > -100));
3004 let doubled = vec![values.iter().map(|v| v * 2).sum::<i32>(), if strict { 1 } else { 2 }];
3005 let repeated = vec![if strict { 1 } else { 0 }; values.len()];
3006 let small = matches!(values.first(), Some(v) if *v < 3);
3007 if matches!(values.len(), 1 | 2) && small {
3008 println!("small");
3009 }
3010 println!("{}", repeated.len() + small as usize);
3011 write!(out, "{}", doubled.iter().map(|d| if *d > 4 { "big" } else { "small" }).collect::<Vec<_>>().join(",")).unwrap();
3012 println!("{} {}", format!("{:?}", doubled), if values.first().copied().unwrap_or(0) > 0 && strict { "positive" } else { "other" });
3013 assert_eq!(doubled.len(), if strict { 2 } else { 2 }, "length for strict={strict}");
3014 out
3015}
3016
3017fn main() {
3018 println!("{}", classify(&[1, 2], true));
3019 println!("{}", classify(&[3], false));
3020 println!("{}", classify(&[], true));
3021 let total: i32 = dbg!(vec![1, 2, 3]).into_iter().sum();
3022 println!("{total}");
3023}
3024"#;
3025 let transformed =
3026 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3027 assert!(transformed.code.contains(
3030 r#", "assertion failed: {}", stringify!(values.iter().all(|v| *v > -100)))"#
3031 ));
3032 assert!(
3034 transformed
3035 .code
3036 .contains(r#"), "bad input {:?}", values);"#)
3037 );
3038 let assertion = transformed
3042 .manifest
3043 .decisions
3044 .iter()
3045 .find(|decision| decision.line == 5)
3046 .expect("assert! decision");
3047 assert_eq!(
3048 assertion.conditions,
3049 ["values.len() < 10", "strict", "!values.is_empty()"]
3050 );
3051 assert!(
3052 transformed
3053 .manifest
3054 .decisions
3055 .iter()
3056 .any(|decision| decision.line == 7)
3057 );
3058 assert!(
3059 transformed
3060 .manifest
3061 .decisions
3062 .iter()
3063 .any(|decision| decision.line == 9)
3064 );
3065 assert!(
3066 transformed
3067 .code
3068 .contains("assert!(({ let mut __supercov_decision_")
3069 );
3070 assert!(
3071 transformed
3072 .code
3073 .contains(", if ({ let mut __supercov_decision_")
3074 );
3075 assert!(
3077 transformed
3078 .code
3079 .contains("vec![if ({ let mut __supercov_decision_")
3080 );
3081 assert!(
3084 transformed
3085 .code
3086 .contains("let small = ({ let mut __supercov_decision_")
3087 );
3088 assert_eq!(
3089 transformed
3090 .manifest
3091 .decisions
3092 .iter()
3093 .filter(|decision| decision.line == 10)
3094 .count(),
3095 1
3096 );
3097 assert_eq!(
3098 transformed
3099 .manifest
3100 .decisions
3101 .iter()
3102 .filter(|decision| decision.line == 9)
3103 .count(),
3104 1
3105 );
3106 assert!(
3107 transformed
3108 .code
3109 .contains("vec![values.iter().map(|v| { crate::__supercov_runtime_v1::hit(")
3110 );
3111 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
3112 limitation.get("id").and_then(|id| id.as_str())
3113 == Some("rust-macro-expansion-not-instrumented")
3114 }));
3115 let original = compile_and_run(source, "original-macros");
3116 let instrumented = compile_and_run(
3117 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3118 "instrumented-macros",
3119 );
3120 assert_eq!(instrumented.status, original.status);
3121 assert_eq!(instrumented.stdout, original.stdout);
3122 let after_location = |stderr: &[u8]| {
3125 String::from_utf8_lossy(stderr)
3126 .lines()
3127 .map(|line| {
3128 line.split_once("] ")
3129 .map_or(line, |(_, rest)| rest)
3130 .to_owned()
3131 })
3132 .collect::<Vec<_>>()
3133 };
3134 assert_eq!(
3135 after_location(&instrumented.stderr),
3136 after_location(&original.stderr)
3137 );
3138 }
3139
3140 #[test]
3141 fn assertion_panic_messages_survive_instrumentation() {
3142 let source = r#"fn grow(len: usize, new_capacity: usize) {
3145 assert!(new_capacity >= len);
3146}
3147
3148fn check(value: i32) {
3149 assert!(value > 0 && value < 10, "value {value} out of range");
3150}
3151
3152// tokio: `assert!` only negates its operand, so a `&bool` is accepted.
3153fn all_seen(seen: &[bool]) {
3154 for was_seen in seen {
3155 assert!(was_seen);
3156 debug_assert!(was_seen, "seen");
3157 }
3158}
3159
3160fn main() {
3161 std::panic::set_hook(Box::new(|_| {}));
3162 all_seen(&[true, true]);
3163 match std::panic::catch_unwind(|| all_seen(&[true, false])) {
3164 Ok(()) => println!("ok"),
3165 Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
3166 }
3167 for (len, capacity) in [(3, 5), (8, 5)] {
3168 match std::panic::catch_unwind(|| grow(len, capacity)) {
3169 Ok(()) => println!("ok"),
3170 Err(payload) => println!("{}", payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_default()),
3171 }
3172 }
3173 match std::panic::catch_unwind(|| check(12)) {
3174 Ok(()) => println!("ok"),
3175 Err(payload) => println!("{}", payload.downcast_ref::<String>().cloned().unwrap_or_default()),
3176 }
3177}
3178"#;
3179 let transformed =
3180 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3181 let original = compile_and_run(source, "original-assert-message");
3182 let instrumented = compile_and_run(
3183 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3184 "instrumented-assert-message",
3185 );
3186 assert_eq!(instrumented.status, original.status);
3187 assert_eq!(instrumented.stdout, original.stdout);
3188 assert!(
3189 String::from_utf8_lossy(&instrumented.stdout)
3190 .contains("assertion failed: new_capacity >= len")
3191 );
3192 assert!(
3193 String::from_utf8_lossy(&instrumented.stdout).contains("assertion failed: was_seen")
3194 );
3195 }
3196
3197 #[test]
3198 fn files_that_predate_a_reserved_word_still_instrument() {
3199 let source = r#"struct Rng(u64);
3201impl Rng {
3202 fn gen(&mut self) -> u64 {
3203 self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1);
3204 self.0 >> 33
3205 }
3206}
3207
3208fn main() {
3209 let mut rng = Rng(7);
3210 let mut odd = 0;
3211 for _ in 0..10 {
3212 if rng.gen() % 2 == 1 {
3213 odd += 1;
3214 }
3215 }
3216 println!("{odd}");
3217}
3218"#;
3219 let transformed =
3220 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3221 assert!(
3222 transformed
3223 .manifest
3224 .decisions
3225 .iter()
3226 .any(|decision| decision.line == 13)
3227 );
3228 let original = compile_and_run_edition(source, "original-gen", "2021");
3229 let instrumented = compile_and_run_edition(
3230 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3231 "instrumented-gen",
3232 "2021",
3233 );
3234 assert_eq!(instrumented.status, original.status);
3235 assert_eq!(instrumented.stdout, original.stdout);
3236 }
3237
3238 #[test]
3239 fn lone_let_conditions_compile_before_edition_2024_and_record_breaks() {
3240 let source = r#"fn first_even(values: &[i32]) -> Option<i32> {
3244 let mut it = values.iter();
3245 'scan: while let Some(value) = it.next() {
3246 if *value < 0 {
3247 break;
3248 }
3249 for _ in 0..1 {
3250 if *value == 99 {
3251 break 'scan;
3252 }
3253 if *value == 98 {
3254 break;
3255 }
3256 }
3257 if *value % 2 == 0 {
3258 return Some(*value);
3259 }
3260 }
3261 None
3262}
3263
3264fn describe(value: Option<i32>) -> &'static str {
3265 if let Some(inner) = value {
3266 if inner > 0 { "positive" } else { "non-positive" }
3267 } else if let None = value {
3268 "none"
3269 } else {
3270 "unreachable"
3271 }
3272}
3273
3274fn count(values: &[Option<i32>]) -> usize {
3275 let mut total = 0;
3276 for value in values {
3277 if let Some(_) = value {
3278 total += 1;
3279 }
3280 }
3281 total
3282}
3283
3284fn main() {
3285 println!("{:?} {:?} {:?} {:?}", first_even(&[1, 3, 4]), first_even(&[1, -1, 4]), first_even(&[99, 4]), first_even(&[98, 3, 6]));
3286 println!("{} {} {}", describe(Some(2)), describe(Some(-2)), describe(None));
3287 println!("{}", count(&[Some(1), None, Some(3)]));
3288}
3289"#;
3290 let transformed =
3291 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3292 assert!(!transformed.code.contains("&& let"));
3293 assert!(transformed.code.contains("__supercov_broke_"));
3294 assert_eq!(transformed.code.matches("= true; break").count(), 2);
3295 for edition in ["2021", "2024"] {
3296 let original =
3297 compile_and_run_edition(source, &format!("original-lone-let-{edition}"), edition);
3298 let instrumented = compile_and_run_edition(
3299 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3300 &format!("instrumented-lone-let-{edition}"),
3301 edition,
3302 );
3303 assert_eq!(instrumented.status, original.status);
3304 assert_eq!(instrumented.stdout, original.stdout);
3305 assert_eq!(instrumented.stderr, original.stderr);
3306 }
3307 }
3308
3309 #[test]
3310 fn let_chains_keep_their_behavior() {
3311 let source = r#"fn describe(value: Option<i32>, flag: bool) -> &'static str {
3312 if let Some(inner) = value && inner > 0 && flag {
3313 "positive"
3314 } else if let Some(inner) = value && (inner < 0 || flag) {
3315 "negative-or-flagged"
3316 } else {
3317 "other"
3318 }
3319}
3320
3321fn count_pairs(values: &[(Option<i32>, i32)]) -> i32 {
3322 let mut total = 0;
3323 let mut it = values.iter();
3324 while let Some((first, second)) = it.next() && let Some(inner) = first && *second > 0 {
3325 total += inner * second;
3326 if total > 100 {
3327 break;
3328 }
3329 }
3330 total
3331}
3332
3333fn tail(value: Option<&str>) -> usize {
3334 let pick = |v: Option<&str>| if let Some(text) = v && !text.is_empty() { text.len() } else { 0 };
3335 if let Some(text) = value && text.starts_with('x') {
3336 println!("x-prefixed");
3337 }
3338 pick(value)
3339}
3340
3341fn main() {
3342 for value in [Some(3), Some(-3), Some(0), None] {
3343 for flag in [true, false] {
3344 println!("{value:?} {flag} {}", describe(value, flag));
3345 }
3346 }
3347 println!("{}", count_pairs(&[(Some(2), 3), (Some(4), 5), (None, 1), (Some(9), 9)]));
3348 println!("{}", count_pairs(&[(Some(50), 3), (Some(4), 5)]));
3349 println!("{} {} {}", tail(Some("xyz")), tail(Some("")), tail(None));
3350}
3351"#;
3352 let transformed =
3353 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3354 assert_eq!(
3355 transformed.code.matches("const __SUPERCOV_CHAIN_").count(),
3356 5
3357 );
3358 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
3359 limitation.get("id").and_then(|id| id.as_str())
3360 == Some("rust-let-chain-probes-not-injected")
3361 }));
3362 let original = compile_and_run(source, "original-chains");
3363 let instrumented = compile_and_run(
3364 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3365 "instrumented-chains",
3366 );
3367 assert_eq!(instrumented.status, original.status);
3368 assert_eq!(instrumented.stdout, original.stdout);
3369 assert_eq!(instrumented.stderr, original.stderr);
3370 }
3371
3372 #[test]
3373 fn instrumented_const_and_static_initialisers_still_compile() {
3374 let source = r#"const DIRECT: usize = if cfg!(unix) { 100 } else { 1_000 };
3380static WIDTH: usize = if cfg!(unix) { 2 } else { 4 };
3381
3382enum Mode {
3383 Narrow = if cfg!(unix) { 1 } else { 2 },
3384}
3385
3386struct Buffer([u8; if cfg!(unix) { 4 } else { 8 }]);
3387
3388impl Buffer {
3389 const SPAN: usize = if cfg!(unix) { 5 } else { 9 };
3390}
3391
3392fn scaled(flag: bool) -> usize {
3393 const LOCAL: usize = if cfg!(unix) { 3 } else { 6 };
3394 if flag { LOCAL + Buffer::SPAN } else { DIRECT + WIDTH }
3395}
3396
3397fn main() {
3398 let buffer = Buffer([0; if cfg!(unix) { 4 } else { 8 }]);
3399 println!(
3400 "{} {} {} {}",
3401 scaled(true),
3402 scaled(false),
3403 Mode::Narrow as usize,
3404 buffer.0.len()
3405 );
3406}
3407"#;
3408 let transformed =
3409 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3410 assert!(transformed.code.contains("::decision("));
3413 let ids = transformed
3414 .manifest
3415 .limitations
3416 .iter()
3417 .filter_map(limitation_kind_of)
3418 .collect::<BTreeSet<_>>();
3419 assert!(ids.contains("rust-const-context-not-instrumented"));
3420
3421 let original = compile_and_run(source, "const-original");
3422 let instrumented = compile_and_run(
3423 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3424 "const-instrumented",
3425 );
3426 assert_eq!(instrumented.status, original.status);
3427 assert_eq!(instrumented.stdout, original.stdout);
3428 assert_eq!(instrumented.stderr, original.stderr);
3429 }
3430
3431 #[test]
3432 fn a_probed_global_allocator_would_recurse_into_itself() {
3433 let source = r#"use std::alloc::{GlobalAlloc, Layout, System};
3439
3440struct Odd;
3441
3442unsafe impl GlobalAlloc for Odd {
3443 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
3444 if layout.align() == 1 && layout.size() > 0 {
3445 System.alloc(layout)
3446 } else {
3447 System.alloc(layout)
3448 }
3449 }
3450
3451 unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
3452 System.dealloc(pointer, layout);
3453 }
3454}
3455
3456#[global_allocator]
3457static ODD: Odd = Odd;
3458
3459fn classify(flag: bool) -> usize {
3460 if flag { 1 } else { 2 }
3461}
3462
3463fn main() {
3464 let held = std::vec![7u8; 32];
3465 println!("{} {}", classify(!held.is_empty()), held.len());
3466}
3467"#;
3468 let transformed =
3469 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3470 let allocator = transformed
3472 .code
3473 .split("unsafe impl GlobalAlloc for Odd")
3474 .nth(1)
3475 .and_then(|rest| rest.split("#[global_allocator]").next())
3476 .expect("the instrumented source still contains the allocator impl");
3477 assert!(
3478 !allocator.contains("__supercov_runtime_v1"),
3479 "probe injected into a GlobalAlloc impl:\n{allocator}"
3480 );
3481 assert!(transformed.code.contains("::decision("));
3483 let ids = transformed
3484 .manifest
3485 .limitations
3486 .iter()
3487 .filter_map(limitation_kind_of)
3488 .collect::<BTreeSet<_>>();
3489 assert!(ids.contains("rust-global-allocator-not-instrumented"));
3490
3491 let original = compile_and_run(source, "alloc-original");
3492 let instrumented = compile_and_run(
3493 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3494 "alloc-instrumented",
3495 );
3496 assert_eq!(instrumented.status, original.status);
3497 assert_eq!(instrumented.stdout, original.stdout);
3498 assert_eq!(instrumented.stderr, original.stderr);
3499 }
3500
3501 #[test]
3502 fn match_arms_record_selection_without_changing_behavior() {
3503 let source = r#"#[derive(Debug)]
3504enum Shape { Dot, Line(i32), Box { w: i32, h: i32 } }
3505
3506fn area(shape: &Shape) -> i32 {
3507 match shape {
3508 Shape::Dot => 0,
3509 Shape::Line(length) if *length < 0 => -length,
3510 Shape::Line(length) => *length,
3511 Shape::Box { w, h } => {
3512 let area = w * h;
3513 area
3514 }
3515 }
3516}
3517
3518fn describe(value: i32) -> &'static str {
3519 let inner = |v: i32| match v { 0 => "none", 1 => "one", _ => "many" };
3520 match value {
3521 0 => inner(value),
3522 n if n < 0 => unsafe { std::hint::unreachable_unchecked() },
3523 n => match n % 2 {
3524 0 => "even",
3525 _ => inner(n),
3526 },
3527 }
3528}
3529
3530fn main() {
3531 for shape in [Shape::Dot, Shape::Line(-3), Shape::Line(4), Shape::Box { w: 2, h: 5 }] {
3532 println!("{shape:?}={}", area(&shape));
3533 }
3534 for value in [0, 1, 3, 8] {
3535 println!("{value}:{}", describe(value));
3536 }
3537}
3538"#;
3539 let transformed =
3540 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3541 assert!(transformed.code.contains("::arms(__SUPERCOV_ARMS_"));
3542 assert_eq!(
3543 transformed.code.matches("const __SUPERCOV_ARMS_").count(),
3544 4
3545 );
3546 let arms = transformed
3547 .manifest
3548 .branches
3549 .iter()
3550 .filter(|branch| branch.kind == "match-arm")
3551 .count();
3552 assert_eq!(arms, 4 + 3 + 3 + 2);
3553 for branch in transformed
3556 .manifest
3557 .branches
3558 .iter()
3559 .filter(|branch| branch.kind == "match-arm")
3560 {
3561 for alternative in &branch.alternatives {
3562 assert!(
3563 transformed.code.contains(&format!("{:?}", alternative.id)),
3564 "{} is not in any table",
3565 alternative.id
3566 );
3567 }
3568 }
3569 let original = compile_and_run(source, "original-arms");
3570 let instrumented = compile_and_run(
3571 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3572 "instrumented-arms",
3573 );
3574 assert_eq!(instrumented.status, original.status);
3575 assert_eq!(instrumented.stdout, original.stdout);
3576 assert_eq!(instrumented.stderr, original.stderr);
3577 }
3578
3579 #[test]
3580 fn loops_logic_and_try_record_their_branches_without_changing_behavior() {
3581 let source = r#"use std::ops::ControlFlow;
3582
3583fn total(values: &[i32]) -> i32 {
3584 let mut sum = 0;
3585 for value in values {
3586 sum += value;
3587 }
3588 'outer: for row in 0..3 {
3589 for column in 0..3 {
3590 if column > row {
3591 continue 'outer;
3592 }
3593 sum += row * column;
3594 }
3595 }
3596 sum
3597}
3598
3599fn first_even(values: &[i32]) -> Option<i32> {
3600 let mut index = 0;
3601 'scan: while index < values.len() {
3602 if values[index] % 2 == 0 {
3603 break 'scan;
3604 }
3605 index += 1;
3606 }
3607 let mut it = values.iter().skip(index);
3608 while let Some(value) = it.next() {
3609 return Some(*value);
3610 }
3611 None
3612}
3613
3614fn parse_twice(text: &str) -> Result<i32, String> {
3615 let value: i32 = text.trim().parse().map_err(|_| "bad".to_string())?;
3616 let doubled = Some(value).map(|v| v * 2).ok_or("none")?;
3617 Ok(doubled)
3618}
3619
3620fn halve(value: i32) -> Option<i32> {
3621 let even = (value % 2 == 0).then_some(value)?;
3622 Some(even / 2)
3623}
3624
3625fn flow(values: &[i32]) -> ControlFlow<i32, i32> {
3626 let mut sum = 0;
3627 for value in values {
3628 let step: ControlFlow<i32, i32> = if *value < 0 { ControlFlow::Break(*value) } else { ControlFlow::Continue(*value) };
3629 sum += step?;
3630 }
3631 ControlFlow::Continue(sum)
3632}
3633
3634fn gate(a: bool, b: bool, c: bool) -> bool {
3635 let both = a && b;
3636 let either = a || b || c;
3637 both || (either && !c) || (c && a && (b || !b))
3638}
3639
3640fn main() {
3641 println!("{} {}", total(&[]), total(&[1, 2, 3]));
3642 println!("{:?} {:?} {:?}", first_even(&[]), first_even(&[1, 3]), first_even(&[1, 4, 6]));
3643 println!("{:?} {:?}", parse_twice(" 21 "), parse_twice("x"));
3644 println!("{:?} {:?}", halve(8), halve(7));
3645 println!("{:?} {:?}", flow(&[1, 2]), flow(&[1, -5, 2]));
3646 for a in [false, true] {
3647 for b in [false, true] {
3648 for c in [false, true] {
3649 print!("{}", gate(a, b, c) as u8);
3650 }
3651 }
3652 }
3653 println!();
3654}
3655"#;
3656 let transformed =
3657 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3658 for marker in [
3659 "::logical((",
3660 "::for_loop((",
3661 "::entered(&mut __supercov_loop_",
3662 "::zero_iterations(__supercov_loop_",
3663 "::TryProbe::probe((",
3664 ] {
3665 assert!(transformed.code.contains(marker), "{marker} missing");
3666 }
3667 let kinds = |kind: &str| {
3668 transformed
3669 .manifest
3670 .branches
3671 .iter()
3672 .filter(|branch| branch.kind == kind)
3673 .count()
3674 };
3675 assert_eq!(kinds("for-loop"), 3 + 1 + 3);
3676 assert_eq!(kinds("while-loop"), 2);
3677 assert_eq!(kinds("try-operator"), 4);
3678 assert_eq!(kinds("logical-and"), 4);
3679 assert_eq!(kinds("logical-or"), 5);
3680 assert!(!transformed.manifest.limitations.iter().any(|limitation| {
3681 limitation.get("id").and_then(|id| id.as_str())
3682 == Some("rust-structural-branch-probes-not-yet-injected")
3683 }));
3684 let original = compile_and_run(source, "original-structural");
3685 let instrumented = compile_and_run(
3686 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3687 "instrumented-structural",
3688 );
3689 assert_eq!(instrumented.status, original.status);
3690 assert_eq!(instrumented.stdout, original.stdout);
3691 assert_eq!(instrumented.stderr, original.stderr);
3692 }
3693
3694 #[test]
3695 fn cfg_gated_sibling_blocks_keep_their_tail_position() {
3696 let source = r#"pub fn is_available() -> bool {
3702 #[cfg(target_endian = "little")]
3703 {
3704 true
3705 }
3706 #[cfg(not(target_endian = "little"))]
3707 {
3708 false
3709 }
3710}
3711
3712fn main() {
3713 println!("{}", is_available());
3714}
3715"#;
3716 let transformed =
3717 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3718 let original = compile_and_run(source, "cfg-original");
3719 let instrumented = compile_and_run(
3720 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3721 "cfg-instrumented",
3722 );
3723 assert_eq!(instrumented.status, original.status);
3724 assert_eq!(instrumented.stdout, original.stdout);
3725 assert!(
3727 transformed
3728 .code
3729 .contains("{\n\ncrate::__supercov_runtime_v1::hit(")
3730 || transformed
3731 .code
3732 .contains("{\ncrate::__supercov_runtime_v1::hit(")
3733 );
3734
3735 let attributed_let = r#"fn main() {
3739 #[cfg(target_endian = "little")]
3740 let value = 1;
3741 #[cfg(not(target_endian = "little"))]
3742 let value = 2;
3743 #[cfg(target_endian = "little")]
3744 let borrowed: &String = &String::from("little");
3745 #[cfg(not(target_endian = "little"))]
3746 let borrowed: &String = &String::from("big");
3747 #[cfg(target_endian = "little")]
3748 print!("le ");
3749 #[cfg(not(target_endian = "little"))]
3750 print!("be ");
3751 #[allow(unused_assignments)]
3752 let mut later;
3753 later = value + 1;
3754 println!("{value} {borrowed} {later}");
3755}
3756"#;
3757 let transformed = instrument_rust_source(
3758 "src/main.rs",
3759 attributed_let,
3760 "crate::__supercov_runtime_v1",
3761 )
3762 .unwrap();
3763 let ids = transformed
3764 .manifest
3765 .limitations
3766 .iter()
3767 .filter_map(limitation_kind_of)
3768 .collect::<BTreeSet<_>>();
3769 assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
3771 assert!(
3772 transformed
3773 .code
3774 .contains("let value = { crate::__supercov_runtime_v1::hit(")
3775 );
3776 assert!(
3777 transformed
3778 .code
3779 .contains("let borrowed: &String = { crate::__supercov_runtime_v1::hit(")
3780 );
3781 assert!(
3782 transformed
3783 .code
3784 .contains("] { crate::__supercov_runtime_v1::hit(")
3785 );
3786 let original = compile_and_run(attributed_let, "cfg-let-original");
3787 let instrumented = compile_and_run(
3788 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3789 "cfg-let-instrumented",
3790 );
3791 assert_eq!(instrumented.status, original.status);
3792 assert_eq!(instrumented.stdout, original.stdout);
3793 assert_eq!(instrumented.stderr, original.stderr);
3794
3795 let trailing_macro = r#"macro_rules! pick { ($e:expr) => { $e } }
3800fn value() -> i32 {
3801 let base = 20;
3802 #[rustfmt::skip]
3803 pick! { base + 1 }
3804}
3805fn effect() {
3806 #[rustfmt::skip]
3807 println! { "effect" }
3808}
3809fn gated() {
3810 #[cfg(target_endian = "little")]
3811 println! { "little" }
3812}
3813fn main() {
3814 effect();
3815 gated();
3816 println!("{}", value());
3817}
3818"#;
3819 let transformed = instrument_rust_source(
3820 "src/main.rs",
3821 trailing_macro,
3822 "crate::__supercov_runtime_v1",
3823 )
3824 .unwrap();
3825 assert!(
3826 transformed
3827 .code
3828 .contains("{ crate::__supercov_runtime_v1::hit(\"rs:statement:")
3829 );
3830 assert!(
3831 transformed
3832 .code
3833 .contains("); #[rustfmt::skip]\n pick! { base + 1 } }")
3834 );
3835 assert!(
3836 transformed
3837 .code
3838 .contains("); #[rustfmt::skip]\n println! { \"effect\" } }")
3839 );
3840 assert!(
3841 transformed.code.contains(
3842 "\n #[cfg(target_endian = \"little\")]\n println! { \"little\" }\n"
3843 )
3844 );
3845 let ids = transformed
3846 .manifest
3847 .limitations
3848 .iter()
3849 .filter_map(limitation_kind_of)
3850 .collect::<BTreeSet<_>>();
3851 assert!(ids.contains("rust-attributed-statement-probes-not-injected"));
3852 let original = compile_and_run(trailing_macro, "trailing-macro-original");
3853 let instrumented = compile_and_run(
3854 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3855 "trailing-macro-instrumented",
3856 );
3857 assert_eq!(
3858 instrumented.status,
3859 original.status,
3860 "{}",
3861 String::from_utf8_lossy(&instrumented.stderr)
3862 );
3863 assert_eq!(instrumented.stdout, original.stdout);
3864 assert_eq!(instrumented.stderr, original.stderr);
3865 }
3866
3867 #[test]
3868 fn an_attributed_macro_statement_keeps_its_macro_in_statement_position() {
3869 let source = r#"macro_rules! trace {
3874 ($($arg:tt)*) => {
3875 #[cfg(target_endian = "little")]
3876 {
3877 println!($($arg)+);
3878 }
3879 }
3880}
3881fn manual() {
3882 #[cfg(any(target_endian = "little", target_endian = "big"))]
3883 trace!("manual");
3884 let _ = 1;
3885}
3886fn main() {
3887 manual();
3888}
3889"#;
3890 let transformed =
3891 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3892 assert!(
3895 transformed.code.contains(r#"trace!("manual"); }"#),
3896 "{}",
3897 transformed.code
3898 );
3899 let original = compile_and_run(source, "attributed-macro-statement-original");
3900 let instrumented = compile_and_run(
3901 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3902 "attributed-macro-statement-instrumented",
3903 );
3904 assert_eq!(
3905 instrumented.status,
3906 original.status,
3907 "{}",
3908 String::from_utf8_lossy(&instrumented.stderr)
3909 );
3910 assert_eq!(instrumented.stdout, original.stdout);
3911 assert_eq!(instrumented.stderr, original.stderr);
3912 }
3913
3914 #[test]
3915 fn an_attributed_lets_macro_initialiser_keeps_its_value() {
3916 let source = r#"macro_rules! first {
3922 ($e:expr) => { $e }
3923}
3924fn value(flag: bool) -> i32 {
3925 #[cfg(any(target_endian = "little", target_endian = "big"))]
3926 let chosen = first!(if flag { 7 } else { 3 });
3927 chosen + 1
3928}
3929fn main() {
3930 println!("{}", value(true));
3931}
3932"#;
3933 let transformed =
3934 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
3935 assert!(
3938 !transformed
3939 .code
3940 .contains("first!(if flag { 7 } else { 3 }); }"),
3941 "{}",
3942 transformed.code
3943 );
3944 let original = compile_and_run(source, "attributed-let-macro-original");
3945 let instrumented = compile_and_run(
3946 &format!("{}\n{NOOP_RUNTIME}", transformed.code),
3947 "attributed-let-macro-instrumented",
3948 );
3949 assert_eq!(
3950 instrumented.status,
3951 original.status,
3952 "{}",
3953 String::from_utf8_lossy(&instrumented.stderr)
3954 );
3955 assert_eq!(instrumented.stdout, original.stdout);
3956 assert_eq!(instrumented.stdout, b"8\n");
3957 }
3958
3959 #[test]
3960 fn obligations_no_probe_can_reach_are_declined() {
3961 let source = r#"pub struct Tagged(usize);
3967impl Tagged {
3968 pub const fn new(len: usize, on_heap: bool) -> Self {
3969 Self(if on_heap { len << 1 } else { len })
3970 }
3971 pub fn plain(len: usize) -> Self {
3972 Self(if len > 0 { len } else { 0 })
3973 }
3974}
3975"#;
3976 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
3977 let declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
3978 assert!(!declined.is_empty(), "the const fn was not declined");
3979
3980 let line_of = |id: &str| {
3982 manifest
3983 .points
3984 .iter()
3985 .find(|point| point.id == id)
3986 .map(|point| point.line)
3987 .or_else(|| {
3988 manifest
3989 .decisions
3990 .iter()
3991 .find(|decision| decision.id == id)
3992 .map(|decision| decision.line)
3993 })
3994 .or_else(|| {
3995 manifest
3996 .branches
3997 .iter()
3998 .find(|branch| branch.id == id)
3999 .map(|branch| branch.line)
4000 })
4001 };
4002 for id in &declined {
4003 let line = line_of(id).unwrap_or_else(|| panic!("no obligation {id}"));
4004 assert!(
4005 (3..=5).contains(&line),
4006 "declined an obligation outside the const fn, at line {line}"
4007 );
4008 }
4009 let measured = manifest
4010 .points
4011 .iter()
4012 .map(|point| (point.id.clone(), point.line))
4013 .filter(|(id, _)| !declined.contains(id))
4014 .collect::<Vec<_>>();
4015 assert!(
4016 measured.iter().any(|(_, line)| (6..=8).contains(line)),
4017 "the plain fn must stay measured: {measured:?}"
4018 );
4019 }
4020
4021 #[test]
4022 fn a_documented_function_is_reported_where_it_starts() {
4023 let source = r#"/// Documented.
4026/// Twice.
4027#[inline]
4028pub fn documented(value: i32) -> i32 {
4029 value + 1
4030}
4031"#;
4032 let manifest = build_rust_manifest("src/lib.rs", source).unwrap();
4033 let point = manifest
4034 .points
4035 .iter()
4036 .find(|point| {
4037 point.kind == PointKind::Function && point.label.as_deref() == Some("documented")
4038 })
4039 .unwrap();
4040 assert_eq!(point.line, 4, "{point:?}");
4041 assert!(
4042 point.source.starts_with("pub fn documented"),
4043 "{}",
4044 point.source
4045 );
4046 }
4047
4048 #[test]
4049 fn rejects_non_crate_local_runtime_paths() {
4050 assert_eq!(
4051 instrument_rust_source("src/lib.rs", "fn okay() {}", "supercov::runtime"),
4052 Err(RustInstrumenterError::InvalidRuntimePath)
4053 );
4054 }
4055
4056 #[test]
4057 fn rejects_invalid_rust_without_partial_obligations() {
4058 assert!(matches!(
4059 build_rust_manifest("src/lib.rs", "fn broken( {\n"),
4060 Err(RustInstrumenterError::Parse(_))
4061 ));
4062 }
4063}