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