Skip to main content

supercov_engine/
rust_instrumenter.rs

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