Skip to main content

supercov_engine/
rust_instrumenter.rs

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