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