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