Skip to main content

supercov_engine/
go_instrumenter.rs

1//! Supercov-owned Go parsing and obligation discovery.
2//!
3//! Go's own `go test -cover` is statement-level and knows nothing about which
4//! test reached a line, which branch arm was taken, or whether a condition
5//! independently affected its decision. It stays a development oracle, exactly
6//! as LLVM does for Rust; the product measures from this tree.
7//!
8//! The denominator comes from a lossless concrete syntax tree, so every
9//! obligation carries the byte range it was discovered at. A line Supercov
10//! cannot anchor is not silently dropped: it is either an obligation with a
11//! location or it is absent from the manifest entirely.
12
13use std::collections::BTreeMap;
14
15use tree_sitter::{Node, Parser};
16
17use crate::coverage_analysis::PointKind;
18use crate::coverage_report::{
19    BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
20};
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum GoInstrumenterError {
24    Parse(String),
25}
26
27impl std::fmt::Display for GoInstrumenterError {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match self {
30            GoInstrumenterError::Parse(detail) => write!(f, "Go parse error: {detail}"),
31        }
32    }
33}
34
35/// Where a probe has to observe, paired with the obligation it answers for.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum GoProbeTarget {
38    Statement {
39        id: String,
40    },
41    Function {
42        id: String,
43    },
44    /// One arm of a branch: the runtime records which alternative ran.
45    ///
46    /// Conditions and decision outcomes deliberately have no probe. The
47    /// recorded vector already says which conditions were evaluated and what
48    /// the decision came to, so a probe beside it would store the same fact
49    /// twice and charge for it on the hottest path instrumentation has.
50    Alternative {
51        branch: String,
52        alternative: String,
53    },
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct GoProbe {
58    pub id: u64,
59    pub target: GoProbeTarget,
60    /// Byte offset the probe observes, for the rewriter that comes next.
61    pub at: usize,
62}
63
64#[derive(Debug, Clone, PartialEq)]
65pub struct GoFileObligations {
66    pub manifest: CoverageManifest,
67    pub probes: BTreeMap<u64, GoProbe>,
68    /// Everything the rewriter must insert to observe these obligations.
69    pub edits: Vec<GoEdit>,
70    /// Conditions per decision, in the order the runtime indexes them. The
71    /// harness passes this to `Arm` so the runtime can size its vectors.
72    pub decision_widths: Vec<u8>,
73}
74
75/// An obligation's identity, derived from what it is rather than from how many
76/// came before it.
77///
78/// A counter per file collides the moment a project has two of them: every
79/// file starts again at 1, two obligations share an id with different
80/// metadata, and the reader refuses the whole archive. A counter across the
81/// project would be unique but would shift every id after any insertion, so
82/// adding one statement would invalidate every acknowledgement below it.
83///
84/// The file, the kind and the byte range answer both: unique across a project,
85/// and unchanged by edits to any other file.
86pub(crate) fn stable_obligation_id(
87    language: &str,
88    file: &str,
89    kind: &str,
90    start: usize,
91    end: usize,
92) -> String {
93    use sha2::{Digest, Sha256};
94    let mut hash = Sha256::new();
95    for value in [file, kind, &start.to_string(), &end.to_string()] {
96        hash.update(value.as_bytes());
97        hash.update([0]);
98    }
99    let digest = hash.finalize();
100    let mut encoded = String::with_capacity(24);
101    for byte in &digest[..12] {
102        use std::fmt::Write as _;
103        write!(&mut encoded, "{byte:02x}").expect("writing to a string cannot fail");
104    }
105    format!("{language}:{kind}:{encoded}")
106}
107
108/// One parse, with no attempt to read past anything the grammar refuses.
109fn parse_exactly(source: &str) -> Result<tree_sitter::Tree, GoInstrumenterError> {
110    let mut parser = Parser::new();
111    parser
112        .set_language(&tree_sitter_go::LANGUAGE.into())
113        .map_err(|error| GoInstrumenterError::Parse(error.to_string()))?;
114    let tree = parser
115        .parse(source, None)
116        .ok_or_else(|| GoInstrumenterError::Parse("parser returned no tree".into()))?;
117    if tree.root_node().has_error() {
118        return Err(GoInstrumenterError::Parse(parse_failure(&tree, source)));
119    }
120    Ok(tree)
121}
122
123/// The three-byte stand-in written over `new` where its argument is a value.
124///
125/// Any identifier of the same length would do; the parse never resolves it and
126/// the stand-in is never written to disk. Three bytes is the whole point: byte
127/// ranges are what every obligation, probe and edit is anchored by, so the
128/// tree read from the stand-in has to describe the author's file offset for
129/// offset.
130const NEW_VALUE_STAND_IN: &str = "nEw";
131
132/// Go 1.26 extended `new` to take a value: `new(x)` is a pointer to a copy of
133/// `x`, where before the argument could only be a type.
134///
135/// tree-sitter-go still models `new` and `make` as a call whose argument list
136/// holds a type, so `new("hello")` does not parse -- and a Go file Supercov
137/// cannot parse carries no obligations, which is how a package using the
138/// construct came to be measured as though it held nothing at all. It is not a
139/// rare corner: one codebase this was reported from has 913 call sites across
140/// 94 files, and code generators emit it, so a project reintroduces it on
141/// every `go generate`.
142///
143/// The grammar cannot express it, so the parse reads a stand-in: at each call
144/// site whose argument is not a type, the three bytes of `new` become another
145/// identifier, which makes the call an ordinary one the grammar does accept.
146/// Nothing else changes -- not a byte of length, not a token beside it -- so
147/// the tree is the author's file in every respect that is measured, and the
148/// text every obligation quotes is still sliced from the file itself.
149///
150/// Per call site rather than per file, because the type forms must be left
151/// alone: `new(map[string]int)` parses only while `new` is the builtin, and
152/// `new(v)` is a type to the grammar and a value to the compiler without
153/// either of them being able to tell from the syntax. Whichever it is, both
154/// readings parse, so the stand-in is spent only where the argument is
155/// something no type could be.
156fn stand_in_for_new_values(source: &str) -> Option<String> {
157    let bytes = source.as_bytes();
158    let mut masked: Option<String> = None;
159    let mut at = 0;
160    while at < bytes.len() {
161        if let Some(past) = skip_literal_or_comment(bytes, at) {
162            at = past;
163            continue;
164        }
165        let Some(open) = builtin_new_at(bytes, at) else {
166            at += 1;
167            continue;
168        };
169        let Some(close) = closing_parenthesis(bytes, open) else {
170            at += 3;
171            continue;
172        };
173        if !parses_as_a_type(&source[open + 1..close]) {
174            masked
175                .get_or_insert_with(|| source.to_owned())
176                .replace_range(at..at + 3, NEW_VALUE_STAND_IN);
177        }
178        at += 3;
179    }
180    masked
181}
182
183/// Whether `new` starts here as the builtin being called, and where its
184/// argument list opens.
185///
186/// `new` is not a keyword in Go. `pool.new(v)` is somebody's own method and
187/// `renewed` is an ordinary identifier, and the grammar reads both perfectly
188/// well already; only a bare `new` immediately followed by `(` is the builtin
189/// the grammar has a special rule for.
190///
191/// Read as bytes rather than as text, and the whole scan with it. Go source is
192/// UTF-8, and a `&str` sliced part way through a rune does not return a wrong
193/// answer, it panics -- which would mean Supercov breaking the suite it was
194/// asked to measure over an accent in a comment. Bytes are safe to walk
195/// because no byte of a multi-byte rune is ever ASCII, so nothing the scan
196/// looks for can be found inside one.
197fn builtin_new_at(bytes: &[u8], at: usize) -> Option<usize> {
198    if !bytes[at..].starts_with(b"new") {
199        return None;
200    }
201    if at > 0 && (is_identifier_byte(bytes[at - 1]) || bytes[at - 1] == b'.') {
202        return None;
203    }
204    // Go permits whitespace between the callee and its arguments.
205    let mut open = at + 3;
206    while open < bytes.len() && bytes[open].is_ascii_whitespace() {
207        open += 1;
208    }
209    (bytes.get(open) == Some(&b'(')).then_some(open)
210}
211
212fn is_identifier_byte(byte: u8) -> bool {
213    // Anything outside ASCII is a continuation or lead byte of a rune, and Go
214    // identifiers may hold those; treating them as part of a name is what
215    // keeps `naïvenew` from reading as the builtin.
216    byte.is_ascii_alphanumeric() || byte == b'_' || byte >= 0x80
217}
218
219/// The parenthesis that closes the one opened at `open`, counting only what is
220/// outside a comment or a literal.
221fn closing_parenthesis(bytes: &[u8], open: usize) -> Option<usize> {
222    let mut depth = 0_usize;
223    let mut at = open;
224    while at < bytes.len() {
225        if let Some(past) = skip_literal_or_comment(bytes, at) {
226            at = past;
227            continue;
228        }
229        match bytes[at] {
230            b'(' => depth += 1,
231            b')' => {
232                depth = depth.saturating_sub(1);
233                if depth == 0 {
234                    return Some(at);
235                }
236            }
237            _ => {}
238        }
239        at += 1;
240    }
241    None
242}
243
244/// Whether this argument is something Go could name a type by.
245///
246/// Asked of the grammar rather than guessed at, because the two readings are
247/// not distinguishable by shape: `new(int)` and `new(someVar)` are the same
248/// syntax, and only the type checker knows which is which. That ambiguity does
249/// no harm here -- both readings parse -- so the only question worth asking is
250/// whether the argument is something no type could be, which a declaration
251/// answers exactly.
252fn parses_as_a_type(argument: &str) -> bool {
253    // `parse_exactly`, never `parse`: an argument may hold a `new(...)` of its
254    // own, and asking the tolerant parser would start again from the top.
255    parse_exactly(&format!("package p\n\nvar _ {}\n", argument.trim())).is_ok()
256}
257
258/// Just past the comment or literal starting here, or None if one does not.
259///
260/// Scanning for a construct in Go text means knowing when the text is not
261/// code: a `)` inside a string closes nothing, and `// new(` is a remark.
262fn skip_literal_or_comment(bytes: &[u8], at: usize) -> Option<usize> {
263    let rest = &bytes[at..];
264    if rest.starts_with(b"//") {
265        return Some(
266            bytes[at..]
267                .iter()
268                .position(|byte| *byte == b'\n')
269                .map_or(bytes.len(), |end| at + end),
270        );
271    }
272    if rest.starts_with(b"/*") {
273        return Some(
274            bytes[at + 2..]
275                .windows(2)
276                .position(|pair| pair == b"*/")
277                .map_or(bytes.len(), |end| at + 4 + end),
278        );
279    }
280    // A raw string runs to its next backquote and holds no escapes at all.
281    if bytes[at] == b'`' {
282        return Some(
283            bytes[at + 1..]
284                .iter()
285                .position(|byte| *byte == b'`')
286                .map_or(bytes.len(), |end| at + 2 + end),
287        );
288    }
289    let quote = match bytes[at] {
290        b'"' | b'\'' => bytes[at],
291        _ => return None,
292    };
293    let mut cursor = at + 1;
294    while cursor < bytes.len() {
295        match bytes[cursor] {
296            // An escape consumes whatever follows it, so `"\""` is one string
297            // and not two.
298            b'\\' => cursor += 2,
299            // An unterminated literal is not Go; the file will not parse
300            // either way, and stopping at the line keeps the scan bounded.
301            b'\n' => return Some(cursor),
302            byte if byte == quote => return Some(cursor + 1),
303            _ => cursor += 1,
304        }
305    }
306    Some(bytes.len())
307}
308
309pub fn parse(source: &str) -> Result<tree_sitter::Tree, GoInstrumenterError> {
310    let refusal = match parse_exactly(source) {
311        Ok(tree) => return Ok(tree),
312        Err(refusal) => refusal,
313    };
314    // Only now, so every file the grammar already reads costs nothing extra
315    // and reads exactly as it did before.
316    let stand_in = stand_in_for_new_values(source).ok_or_else(|| refusal.clone())?;
317    // And the refusal reported is the one about the author's own file: a
318    // stand-in that did not help must not put its own name in the message.
319    parse_exactly(&stand_in).map_err(|_| refusal)
320}
321
322/// Where a parse went wrong, phrased so a reader knows where to look.
323///
324/// "source does not parse" for a nine-hundred-line file, or a byte offset into
325/// it, says only that something is wrong. tree-sitter recovers as it goes, so
326/// the outermost error node routinely spans a whole class while the token it
327/// actually choked on is deep inside: the innermost one is the one worth
328/// naming.
329pub(crate) fn parse_failure(tree: &tree_sitter::Tree, source: &str) -> String {
330    let mut deepest: Option<(usize, Node)> = None;
331    let mut stack = vec![(0_usize, tree.root_node())];
332    while let Some((depth, node)) = stack.pop() {
333        if (node.is_error() || node.is_missing()) && deepest.is_none_or(|(found, _)| depth > found)
334        {
335            deepest = Some((depth, node));
336        }
337        // Every child, not only those that report an error of their own: an
338        // ERROR node holds the tokens it did manage to read, and the one that
339        // actually failed is among them.
340        let mut cursor = node.walk();
341        for child in node.children(&mut cursor) {
342            stack.push((depth + 1, child));
343        }
344    }
345    let Some((_, node)) = deepest else {
346        return "source does not parse".to_owned();
347    };
348    let start = node.start_position();
349    let mut line = source
350        .lines()
351        .nth(start.row)
352        .unwrap_or_default()
353        .trim()
354        .to_owned();
355    if line.chars().count() > 120 {
356        line = line.chars().take(117).collect::<String>() + "...";
357    }
358    let what = if node.is_missing() {
359        "missing syntax"
360    } else {
361        "unexpected syntax"
362    };
363    // A recovered ERROR node can swallow everything from the construct it
364    // opened to the end of it, and the reader needs to know how far that is:
365    // "line 174" alone reads as a one-line problem when the parser gave up on
366    // four hundred.
367    let end = node.end_position().row + 1;
368    let through = if end > start.row + 1 {
369        format!(" (through line {end})")
370    } else {
371        String::new()
372    };
373    format!(
374        "{what} at line {}, column {}{through}: {line}",
375        start.row + 1,
376        start.column + 1,
377    )
378}
379
380/// Statements Go nests directly inside a block. Declarations that cannot
381/// execute -- an import, a type -- carry no coverage question and are absent
382/// rather than counted as uncovered.
383fn is_statement(kind: &str) -> bool {
384    matches!(
385        kind,
386        "assignment_statement"
387            | "break_statement"
388            | "const_declaration"
389            | "continue_statement"
390            | "dec_statement"
391            | "defer_statement"
392            | "expression_statement"
393            | "expression_switch_statement"
394            | "fallthrough_statement"
395            | "for_statement"
396            | "go_statement"
397            | "goto_statement"
398            | "if_statement"
399            | "inc_statement"
400            | "labeled_statement"
401            | "return_statement"
402            | "select_statement"
403            | "send_statement"
404            | "short_var_declaration"
405            | "type_switch_statement"
406            | "var_declaration"
407    )
408}
409
410struct Collector<'a> {
411    file: &'a str,
412    source: &'a str,
413    alias: &'a str,
414    next_probe: &'a mut u64,
415    edits: Vec<GoEdit>,
416    points: Vec<PointMeta>,
417    branches: Vec<BranchMeta>,
418    decisions: Vec<DecisionMeta>,
419    probes: BTreeMap<u64, GoProbe>,
420    limitations: Vec<serde_json::Value>,
421    widths: Vec<u8>,
422    /// How many decisions the module already numbered before this file. The
423    /// runtime holds one decision-state array for the whole module, so an
424    /// index that meant "the first decision in this file" would land on every
425    /// other file's first decision too.
426    decision_base: u32,
427    /// Whether this file's Go has generics, which a decision used as a value
428    /// needs to keep its type.
429    generics: bool,
430    /// The conditions of `if` and `for`, unparenthesized: decisions where
431    /// they stand, so not decisions as values too.
432    conditions: std::collections::HashSet<(usize, usize)>,
433    /// Decisions used as values in a file without generics.
434    unmeasured_values: Vec<(String, usize, usize, String)>,
435}
436
437/// A branch whose condition is a single operand needs no independence
438/// obligation; the runtime's sentinel says so.
439/// The wrapper a branch needs. A single-operand condition has no independence
440/// obligation, so it gets the form with no decision argument — which is the
441/// one small enough for Go to inline, and the majority of branches in real
442/// code.
443///
444/// The condition goes in converted, `bool(c)`: a named boolean type (`type
445/// Flag bool`) is not assignable to `bool`, and a condition of one stopped the
446/// package compiling. A condition needs only to be a boolean, so the
447/// conversion changes nothing it decides.
448fn branch_wrapper(alias: &str, when_true: u64, when_false: u64, decision: Option<usize>) -> String {
449    match decision {
450        Some(index) => format!("{alias}.BD({when_true}, {when_false}, {index}, bool("),
451        None => format!("{alias}.B({when_true}, {when_false}, bool("),
452    }
453}
454
455/// Whether a probe may be placed before this node.
456///
457/// Go has slots that hold a statement but are not statement positions: the
458/// init and post clauses of a `for`, and the initialiser of an `if` or a
459/// `switch`. A probe there turns `for i := 0; c; i++` into a four-clause loop
460/// that does not compile. Every real statement is a child of a `statement_list`,
461/// so that is the rule rather than a list of exceptions to remember.
462/// The `fallthrough` a case clause ends with, if it ends with one.
463fn ending_fallthrough(clause: Node) -> Option<Node> {
464    let body = clause
465        .children(&mut clause.walk())
466        .find(|child| child.kind() == "statement_list")?;
467    let last = body.named_child(body.named_child_count().checked_sub(1)?)?;
468    (last.kind() == "fallthrough_statement").then_some(last)
469}
470
471fn in_statement_position(node: Node) -> bool {
472    node.parent()
473        .is_some_and(|parent| parent.kind() == "statement_list")
474}
475
476/// The condition of a `for` loop, when it has one. A `range` clause and a bare
477/// `for {}` have none.
478/// A `for ... range` loop, as opposed to a bare `for {}`.
479fn is_range_loop(node: Node) -> bool {
480    node.children(&mut node.walk())
481        .any(|child| child.kind() == "range_clause")
482}
483
484/// Whether the function holding `node` has a `goto` anywhere in it.
485fn in_function_with_goto(node: Node) -> bool {
486    let mut function = node;
487    while !matches!(
488        function.kind(),
489        "function_declaration" | "method_declaration" | "func_literal" | "source_file"
490    ) {
491        match function.parent() {
492            Some(parent) => function = parent,
493            None => break,
494        }
495    }
496    let mut stack = vec![function];
497    while let Some(next) = stack.pop() {
498        if next.kind() == "goto_statement" {
499            return true;
500        }
501        stack.extend(next.children(&mut next.walk()));
502    }
503    false
504}
505
506fn loop_condition<'t>(node: Node<'t>) -> Option<Node<'t>> {
507    if let Some(condition) = node.child_by_field_name("condition") {
508        return Some(condition);
509    }
510    let mut cursor = node.walk();
511    let clause = node
512        .children(&mut cursor)
513        .find(|child| child.kind() == "for_clause")?;
514    let mut inner = clause.walk();
515    clause
516        .children(&mut inner)
517        .find(|child| child.is_named() && child.kind().ends_with("_expression"))
518}
519
520impl<'a> Collector<'a> {
521    fn id(&mut self, node: Node, kind: &str) -> String {
522        stable_obligation_id("go", self.file, kind, node.start_byte(), node.end_byte())
523    }
524
525    /// A limitation's identity. The contract requires one, and requires it to
526    /// be unique: a manifest whose limitations cannot be told apart cannot say
527    /// which surface each one is about, so the reader refuses the run rather
528    /// than present a list nobody can act on.
529    fn limitation_id(&mut self, node: Node, kind: &str) -> String {
530        self.id(node, kind)
531    }
532
533    fn probe(&mut self, target: GoProbeTarget, at: usize) -> u64 {
534        *self.next_probe += 1;
535        let id = *self.next_probe;
536        self.probes.insert(id, GoProbe { id, target, at });
537        id
538    }
539
540    fn edit(&mut self, at: usize, rank: i32, text: String) {
541        self.edits.push(GoEdit { at, rank, text });
542    }
543
544    /// Insert a statement-position call. Go allows `a(); b()` on one line, so a
545    /// probe never changes which line a statement reports as its own.
546    fn call_before(&mut self, at: usize, call: String) {
547        self.edit(at, 100, format!("{call}; "));
548    }
549
550    /// Line and column are one-based, matching every other frontend.
551    fn position(&self, node: Node) -> (usize, usize) {
552        let start = node.start_position();
553        (start.row + 1, start.column + 1)
554    }
555
556    fn text(&self, node: Node) -> String {
557        let raw = &self.source[node.byte_range()];
558        let line = raw.lines().next().unwrap_or("");
559        line.trim().to_owned()
560    }
561
562    fn add_point(&mut self, node: Node, kind: PointKind, label: Option<String>) {
563        let (line, column) = self.position(node);
564        let id = self.id(
565            node,
566            match kind {
567                PointKind::Function => "function",
568                PointKind::Statement => "statement",
569            },
570        );
571        let target = match kind {
572            PointKind::Function => GoProbeTarget::Function { id: id.clone() },
573            PointKind::Statement => GoProbeTarget::Statement { id: id.clone() },
574        };
575        // A function is observed just inside its body, because the declaration
576        // itself is not a place a statement may go.
577        let at = match kind {
578            PointKind::Function => match node.child_by_field_name("body") {
579                Some(body) => body.start_byte() + 1,
580                None => return,
581            },
582            PointKind::Statement => node.start_byte(),
583        };
584        let probe = self.probe(target, at);
585        // A direct store into the array this package already holds, not a call
586        // that would have to find it first. This is the shape Go's own cover
587        // tool and JaCoCo both settled on: one move instruction.
588        self.call_before(at, format!("{HITS_VARIABLE}[{probe}] = 2"));
589        self.points.push(PointMeta {
590            id,
591            kind,
592            file: self.file.to_owned(),
593            line,
594            column,
595            source: self.text(node),
596            label,
597        });
598    }
599
600    fn add_branch(&mut self, node: Node, kind: &str, labels: &[&str]) -> Vec<u64> {
601        let pairs = labels
602            .iter()
603            .map(|label| (*label, *label))
604            .collect::<Vec<_>>();
605        self.add_branch_named(node, kind, &pairs)
606    }
607
608    /// `add_branch` with an id suffix of its own for each outcome's label.
609    fn add_branch_named(&mut self, node: Node, kind: &str, labels: &[(&str, &str)]) -> Vec<u64> {
610        let (line, column) = self.position(node);
611        let id = self.id(node, "branch");
612        let mut probes = Vec::new();
613        let alternatives = labels
614            .iter()
615            .map(|(suffix, label)| {
616                let alternative = format!("{id}.{suffix}");
617                probes.push(self.probe(
618                    GoProbeTarget::Alternative {
619                        branch: id.clone(),
620                        alternative: alternative.clone(),
621                    },
622                    node.start_byte(),
623                ));
624                BranchAlternativeMeta {
625                    id: alternative,
626                    label: (*label).to_owned(),
627                }
628            })
629            .collect();
630        self.branches.push(BranchMeta {
631            id,
632            kind: kind.to_owned(),
633            file: self.file.to_owned(),
634            line,
635            column,
636            source: self.text(node),
637            alternatives,
638        });
639        probes
640    }
641
642    /// Record a boolean expression as a decision when it has more than one
643    /// condition. A single condition needs no MC/DC obligation: its branch
644    /// outcomes already say everything independence could.
645    /// Returns the runtime's index for this decision, or `None` when the
646    /// expression has a single condition and needs no independence obligation.
647    fn add_decision(&mut self, node: Node, kind: &str) -> Option<usize> {
648        self.add_decision_wrapping(node, kind, false)
649    }
650
651    /// `add_decision`, with each operand of a decision used as a value
652    /// wrapped so it keeps its type: a typed operand through the generic
653    /// `CV`, an untyped one (a comparison, `true`) as `C(...) == true`, which
654    /// is untyped again.
655    fn add_decision_wrapping(&mut self, node: Node, kind: &str, value: bool) -> Option<usize> {
656        let mut leaves = Vec::new();
657        condition_nodes(node, self.source, &mut leaves);
658        if leaves.len() < 2 {
659            return None;
660        }
661        let conditions = leaves
662            .iter()
663            .map(|leaf| self.source[leaf.byte_range()].trim().to_owned())
664            .collect::<Vec<_>>();
665        let (line, column) = self.position(node);
666        let id = self.id(node, "decision");
667        // The runtime indexes decisions by position across the whole module,
668        // so the index a wrapper carries is this decision's place in the
669        // module's width table, not in this file's.
670        let index_of_decision = self.decision_base as usize + self.widths.len();
671        self.widths.push(leaves.len().min(64) as u8);
672        let alias = self.alias.to_owned();
673        // No probe per condition or outcome. The recorded vector already says
674        // which conditions were evaluated and what the decision came to, so a
675        // probe beside it would store the same fact twice and charge for it on
676        // every evaluation.
677        for (index, leaf) in leaves.iter().enumerate() {
678            // Wrapping an operand keeps short-circuiting intact: Go evaluates a
679            // call argument only when the call is reached, so the right-hand
680            // wrapper runs exactly when the unwrapped operand would have.
681            let (open, close) = if !value {
682                (
683                    format!("{alias}.C({index_of_decision}, {index}, bool("),
684                    "))",
685                )
686            } else if untyped_boolean(*leaf, self.source) {
687                (
688                    format!("({alias}.C({index_of_decision}, {index}, "),
689                    ") == true)",
690                )
691            } else {
692                (format!("{alias}.CV({index_of_decision}, {index}, "), ")")
693            };
694            self.edit(leaf.start_byte(), 20, open);
695            self.edit(leaf.end_byte(), 20, close.to_owned());
696        }
697        self.decisions.push(DecisionMeta {
698            id,
699            file: self.file.to_owned(),
700            line,
701            column,
702            source: self.text(node),
703            conditions,
704            kind: kind.to_owned(),
705        });
706        Some(index_of_decision)
707    }
708
709    fn walk(&mut self, node: Node) {
710        let alias = self.alias.to_owned();
711        match node.kind() {
712            "function_declaration" | "method_declaration" | "func_literal" => {
713                let label = node
714                    .child_by_field_name("name")
715                    .map(|name| self.source[name.byte_range()].to_owned());
716                self.add_point(node, PointKind::Function, label);
717            }
718            "if_statement" => {
719                if let Some(condition) = node.child_by_field_name("condition") {
720                    let bare = unparenthesized(condition);
721                    self.conditions.insert((bare.start_byte(), bare.end_byte()));
722                    // An `if` without an else still has two outcomes: the body
723                    // ran, or control passed it by. Wrapping the condition
724                    // records the arm that was *not* taken by never setting its
725                    // bit, which is what makes an untested guard visible.
726                    let probes = self.add_branch(node, "if", &["true", "false"]);
727                    let decision = self.add_decision(condition, "if");
728                    self.edit(
729                        condition.start_byte(),
730                        5,
731                        branch_wrapper(&alias, probes[0], probes[1], decision),
732                    );
733                    self.edit(condition.end_byte(), 5, "))".to_owned());
734                }
735            }
736            "for_statement" => {
737                // A `for` with a condition branches on it. A `range` loop and a
738                // bare `for {}` have no expression to observe, so Supercov
739                // records the limitation rather than an obligation it cannot
740                // measure.
741                match loop_condition(node) {
742                    Some(condition) => {
743                        let bare = unparenthesized(condition);
744                        self.conditions.insert((bare.start_byte(), bare.end_byte()));
745                        let probes = self.add_branch(node, "loop", &["true", "false"]);
746                        let decision = self.add_decision(condition, "loop");
747                        self.edit(
748                            condition.start_byte(),
749                            5,
750                            branch_wrapper(&alias, probes[0], probes[1], decision),
751                        );
752                        self.edit(condition.end_byte(), 5, "))".to_owned());
753                    }
754                    None if is_range_loop(node) && !in_function_with_goto(node) => {
755                        // A range loop's outcomes are whether it ran at all,
756                        // as every other language records them: a flag the
757                        // body sets, read after the loop. Declared before a
758                        // label, so `continue L` still names the loop; not in
759                        // a function with a `goto`, which may not jump over a
760                        // declaration.
761                        let probes = self.add_branch_named(
762                            node,
763                            "for",
764                            &[("zero", "zero iterations"), ("entered", "entered")],
765                        );
766                        let flag = format!("_scvRan{}", node.start_byte());
767                        let statement = node
768                            .parent()
769                            .filter(|parent| parent.kind() == "labeled_statement")
770                            .unwrap_or(node);
771                        self.edit(statement.start_byte(), 90, format!("var {flag} bool; "));
772                        if let Some(body) = node.child_by_field_name("body") {
773                            self.edit(
774                                body.start_byte() + 1,
775                                100,
776                                format!(
777                                    " if !{flag} {{ {flag} = true; {alias}.A({}) }};",
778                                    probes[1]
779                                ),
780                            );
781                        }
782                        self.edit(
783                            node.end_byte(),
784                            100,
785                            format!("; if !{flag} {{ {alias}.A({}) }}", probes[0]),
786                        );
787                    }
788                    None => {
789                        let (line, column) = self.position(node);
790                        let limitation = self.limitation_id(node, "loop-without-condition");
791                        self.limitations.push(serde_json::json!({
792                            "id": limitation,
793                            "kind": "loop-without-condition",
794                            "file": self.file,
795                            "source": self.text(node),
796                            "line": line,
797                            "column": column,
798                            "reason": "a range or unconditional loop has no condition to observe, so no branch obligation is recorded for it",
799                        }));
800                    }
801                }
802            }
803            "expression_switch_statement" | "type_switch_statement" | "select_statement" => {
804                let kind = match node.kind() {
805                    "expression_switch_statement" => "switch",
806                    "type_switch_statement" => "type-switch",
807                    _ => "select",
808                };
809                let mut cases = Vec::new();
810                let mut has_default = false;
811                let mut cursor = node.walk();
812                for child in node.children(&mut cursor) {
813                    match child.kind() {
814                        "expression_case" | "type_case" | "communication_case" => {
815                            cases.push((self.text(child), Some(child)))
816                        }
817                        "default_case" => {
818                            has_default = true;
819                            cases.push(("default".to_owned(), Some(child)));
820                        }
821                        _ => {}
822                    }
823                }
824                // A switch with no default can match nothing, which is an
825                // outcome a reader has to see, and synthesising the clause is
826                // the only way to observe it: an added `default` holding
827                // nothing but a probe is the path the program already took.
828                //
829                // A select is not a switch. Without a default it *blocks*
830                // until one of its cases is ready; with one it returns
831                // immediately. So there is no "no case matched" outcome to
832                // record -- a select always selects -- and adding the clause
833                // to observe one would stop it blocking. That is not an
834                // instrument changing what is known about a program, it is an
835                // instrument changing what the program does: a loop that waited
836                // for values spins instead, and the code returns an empty
837                // buffer it reports as full.
838                if !has_default && node.kind() != "select_statement" {
839                    cases.push(("no case matched".to_owned(), None));
840                }
841                let labels = cases
842                    .iter()
843                    .map(|(label, _)| label.as_str())
844                    .collect::<Vec<_>>();
845                let probes = self.add_branch(node, kind, &labels);
846                // A case the one before it falls through into runs without its
847                // own expression being matched: `case n > 10: ...; fallthrough`
848                // runs `case n > 5`'s body for 20, and crediting that case as
849                // selected claimed a test of `n > 5` none made. The case before
850                // sets a flag as it falls through; the next one records only
851                // when the flag is clear, and clears it.
852                let fallthroughs = cases
853                    .iter()
854                    .map(|(_, clause)| clause.and_then(|clause| ending_fallthrough(clause)))
855                    .collect::<Vec<_>>();
856                let flag = format!("_scvFell{}", node.start_byte());
857                if fallthroughs.iter().any(Option::is_some) {
858                    // Before a label too, so `break L` still names the switch.
859                    let statement = node
860                        .parent()
861                        .filter(|parent| parent.kind() == "labeled_statement")
862                        .unwrap_or(node);
863                    self.edit(statement.start_byte(), 90, format!("var {flag} bool; "));
864                    for fallthrough in fallthroughs.iter().flatten() {
865                        self.call_before(fallthrough.start_byte(), format!("{flag} = true"));
866                    }
867                }
868                for (index, (probe, (_, clause))) in probes.iter().zip(cases.iter()).enumerate() {
869                    match clause {
870                        Some(clause) => {
871                            let at = clause
872                                .children(&mut clause.walk())
873                                .find(|child| child.kind() == "statement_list")
874                                .map(|body| body.start_byte())
875                                .unwrap_or_else(|| clause.end_byte());
876                            if index > 0 && fallthroughs[index - 1].is_some() {
877                                self.edit(
878                                    at,
879                                    100,
880                                    format!("if {flag} {{ {flag} = false }} else {{ {alias}.A({probe}) }}; "),
881                                );
882                            } else {
883                                self.call_before(at, format!("{alias}.A({probe})"));
884                            }
885                        }
886                        None => {
887                            // Before the switch's closing brace, and on its
888                            // line: three lines of its own moved every line
889                            // below the switch, so a failure further down
890                            // the file was reported three lines late. The
891                            // `;` ends whatever the last clause holds when
892                            // the switch is written on one line; a switch
893                            // with no clause has nothing to end.
894                            let at = node.end_byte().saturating_sub(1);
895                            let after_a_clause = cases.iter().any(|(_, clause)| clause.is_some());
896                            self.edit(
897                                at,
898                                100,
899                                format!(
900                                    "{}default: {alias}.A({probe}) ",
901                                    if after_a_clause { "; " } else { "" }
902                                ),
903                            );
904                        }
905                    }
906                }
907            }
908            "binary_expression"
909                if is_logical(node, self.source)
910                    && !logical_parent(node, self.source)
911                    && !self
912                        .conditions
913                        .contains(&(node.start_byte(), node.end_byte()))
914                    && in_function_body(node) =>
915            {
916                // `&&` and `||` used as a value are decisions as much as a
917                // condition is: MC/DC asks it of every boolean expression of
918                // two or more conditions.
919                if self.generics {
920                    let probes = self.add_branch(node, "value", &["true", "false"]);
921                    let mut leaves = Vec::new();
922                    condition_nodes(node, self.source, &mut leaves);
923                    let typed = leaves
924                        .iter()
925                        .any(|leaf| !untyped_boolean(*leaf, self.source));
926                    if let Some(index) = self.add_decision_wrapping(node, "value", true) {
927                        let (open, close) = if typed {
928                            (
929                                format!("{alias}.BV({}, {}, {index}, ", probes[0], probes[1]),
930                                ")",
931                            )
932                        } else {
933                            (
934                                format!("({alias}.BD({}, {}, {index}, ", probes[0], probes[1]),
935                                ") == true)",
936                            )
937                        };
938                        self.edit(node.start_byte(), 5, open);
939                        self.edit(node.end_byte(), 5, close.to_owned());
940                    }
941                } else {
942                    let (line, column) = self.position(node);
943                    let id = self.limitation_id(node, "value-decision-before-generics");
944                    self.unmeasured_values
945                        .push((id, line, column, self.text(node)));
946                }
947            }
948            kind if is_statement(kind) && in_statement_position(node) => {
949                self.add_point(node, PointKind::Statement, None);
950            }
951            _ => {}
952        }
953        // `if`, `for` and `switch` are statements too, and their own point is
954        // what says the construct was reached at all.
955        if in_statement_position(node)
956            && matches!(
957                node.kind(),
958                "if_statement"
959                    | "for_statement"
960                    | "expression_switch_statement"
961                    | "type_switch_statement"
962                    | "select_statement"
963            )
964        {
965            self.add_point(node, PointKind::Statement, None);
966        }
967        let mut cursor = node.walk();
968        for child in node.children(&mut cursor) {
969            if child.is_named() {
970                self.walk(child);
971            }
972        }
973    }
974}
975
976/// Flatten a boolean expression into its independent conditions.
977///
978/// `&&` and `||` are the only short-circuiting operators Go has, so they are
979/// the only ones that split a decision. `!` negates a condition rather than
980/// introducing one, and a parenthesised group is transparent.
981fn condition_nodes<'t>(node: Node<'t>, source: &str, out: &mut Vec<Node<'t>>) {
982    match node.kind() {
983        "binary_expression" => {
984            let operator = node
985                .child_by_field_name("operator")
986                .map(|op| &source[op.byte_range()])
987                .unwrap_or("");
988            if operator == "&&" || operator == "||" {
989                if let Some(left) = node.child_by_field_name("left") {
990                    condition_nodes(left, source, out);
991                }
992                if let Some(right) = node.child_by_field_name("right") {
993                    condition_nodes(right, source, out);
994                }
995                return;
996            }
997            out.push(node);
998        }
999        "parenthesized_expression" => {
1000            let mut cursor = node.walk();
1001            match node.children(&mut cursor).find(|child| child.is_named()) {
1002                Some(inner) => condition_nodes(inner, source, out),
1003                None => out.push(node),
1004            }
1005        }
1006        _ => out.push(node),
1007    }
1008}
1009
1010fn is_logical(node: Node, source: &str) -> bool {
1011    node.kind() == "binary_expression"
1012        && node
1013            .child_by_field_name("operator")
1014            .is_some_and(|op| matches!(&source[op.byte_range()], "&&" | "||"))
1015}
1016
1017/// Whether the expression is an operand of an enclosing `&&` or `||`,
1018/// through parentheses, so part of that decision rather than one of its own.
1019fn logical_parent(node: Node, source: &str) -> bool {
1020    let mut parent = node.parent();
1021    while let Some(candidate) =
1022        parent.filter(|candidate| candidate.kind() == "parenthesized_expression")
1023    {
1024        parent = candidate.parent();
1025    }
1026    parent.is_some_and(|parent| is_logical(parent, source))
1027}
1028
1029fn unparenthesized(node: Node) -> Node {
1030    if node.kind() == "parenthesized_expression"
1031        && let Some(inner) = node
1032            .children(&mut node.walk())
1033            .find(|child| child.is_named())
1034    {
1035        return unparenthesized(inner);
1036    }
1037    node
1038}
1039
1040/// Inside a function's body: a probe call is allowed there, and the runtime
1041/// is armed. A constant cannot hold a call at all.
1042fn in_function_body(node: Node) -> bool {
1043    let mut current = node.parent();
1044    while let Some(ancestor) = current {
1045        match ancestor.kind() {
1046            "const_declaration" => return false,
1047            "block" => return true,
1048            _ => current = ancestor.parent(),
1049        }
1050    }
1051    false
1052}
1053
1054/// Whether Go types the expression as an untyped boolean: a comparison,
1055/// `true` or `false`, or `!`, `&&`, `||` and parentheses over only those.
1056fn untyped_boolean(node: Node, source: &str) -> bool {
1057    match node.kind() {
1058        "true" | "false" => true,
1059        "parenthesized_expression" => node
1060            .children(&mut node.walk())
1061            .find(|child| child.is_named())
1062            .is_some_and(|inner| untyped_boolean(inner, source)),
1063        "unary_expression" => {
1064            node.child_by_field_name("operator")
1065                .is_some_and(|op| &source[op.byte_range()] == "!")
1066                && node
1067                    .child_by_field_name("operand")
1068                    .is_some_and(|operand| untyped_boolean(operand, source))
1069        }
1070        "binary_expression" => match node
1071            .child_by_field_name("operator")
1072            .map(|op| &source[op.byte_range()])
1073        {
1074            Some("==" | "!=" | "<" | "<=" | ">" | ">=") => true,
1075            Some("&&" | "||") => {
1076                node.child_by_field_name("left")
1077                    .is_some_and(|left| untyped_boolean(left, source))
1078                    && node
1079                        .child_by_field_name("right")
1080                        .is_some_and(|right| untyped_boolean(right, source))
1081            }
1082            _ => false,
1083        },
1084        _ => false,
1085    }
1086}
1087
1088/// One source edit. Every probe is an insertion at a byte offset; wrapping an
1089/// expression is two of them, at its start and its end.
1090///
1091/// `rank` orders edits landing on the same offset. Applying right to left, the
1092/// edit inserted last ends up leftmost, so an outer wrapper carries a lower
1093/// rank than the inner one it encloses.
1094#[derive(Debug, Clone, PartialEq, Eq)]
1095pub struct GoEdit {
1096    pub at: usize,
1097    pub rank: i32,
1098    pub text: String,
1099}
1100
1101/// Apply edits to source, right to left so earlier offsets stay valid.
1102///
1103/// What is inserted is followed by a comment that tells Go where the text
1104/// after it was written, `/*line :4:9*/`. Go reports a compile error, and
1105/// `go vet` a finding, by line and column, and a probe earlier on the line
1106/// moved the column: `./crypto.go:4:38: undefined` for `4:15`. With no file
1107/// named, the comment keeps the name the file already has.
1108pub fn rewrite(source: &str, edits: &[GoEdit]) -> String {
1109    let mut ordered = edits.to_vec();
1110    ordered.sort_by(|a, b| b.at.cmp(&a.at).then(b.rank.cmp(&a.rank)));
1111    let lines = std::iter::once(0)
1112        .chain(source.match_indices('\n').map(|(at, _)| at + 1))
1113        .collect::<Vec<_>>();
1114    let mut out = source.to_owned();
1115    let mut placed = None;
1116    for edit in ordered {
1117        if edit.at > source.len() {
1118            continue;
1119        }
1120        if placed != Some(edit.at) {
1121            placed = Some(edit.at);
1122            let line = lines.partition_point(|start| *start <= edit.at);
1123            let column = edit.at - lines[line - 1] + 1;
1124            out.insert_str(edit.at, &format!("/*line :{line}:{column}*/"));
1125        }
1126        out.insert_str(edit.at, &edit.text);
1127    }
1128    out
1129}
1130
1131/// The import rewritten source needs, placed straight after the package
1132/// clause and on its line: a line of its own moved every line below it, and
1133/// with it every line Go reports, `crypto.go:24` in a panic for line 23. Go
1134/// rejects an unused import, so this is only added to a file that gained at
1135/// least one probe.
1136pub fn import_edit(source: &str, alias: &str, path: &str) -> Option<GoEdit> {
1137    let tree = parse(source).ok()?;
1138    let mut cursor = tree.root_node().walk();
1139    let package = tree
1140        .root_node()
1141        .children(&mut cursor)
1142        .find(|child| child.kind() == "package_clause")?;
1143    Some(GoEdit {
1144        at: package.end_byte(),
1145        rank: 0,
1146        text: format!("; import {alias} \"{path}\""),
1147    })
1148}
1149
1150pub fn build_go_obligations(
1151    file: &str,
1152    source: &str,
1153    next_probe: &mut u64,
1154    next_decision: &mut u32,
1155) -> Result<GoFileObligations, GoInstrumenterError> {
1156    build_go_obligations_with_alias(file, source, next_probe, next_decision, RUNTIME_ALIAS)
1157}
1158
1159/// The name rewritten source calls the runtime by. Deliberately unlikely to
1160/// collide with an identifier a project already uses.
1161pub const RUNTIME_ALIAS: &str = "__supercov";
1162
1163/// The package-level array every probe stores into. Declared once per package
1164/// by a generated file, so a probe is an array index rather than a call.
1165pub const HITS_VARIABLE: &str = "__supercovHits";
1166
1167/// The module path the rewritten source imports the runtime from.
1168pub const RUNTIME_IMPORT: &str = "github.com/supercorp-ai/supercov/runtime/go/supercov";
1169
1170pub fn build_go_obligations_with_alias(
1171    file: &str,
1172    source: &str,
1173    next_probe: &mut u64,
1174    next_decision: &mut u32,
1175    alias: &str,
1176) -> Result<GoFileObligations, GoInstrumenterError> {
1177    build_go_obligations_for(file, source, next_probe, next_decision, alias, true)
1178}
1179
1180/// `generics` says whether the file's language version (its module's `go`
1181/// directive, or an older `//go:build go1.N`) has them; a decision used as
1182/// a value needs them to keep its type, and is a declared limitation
1183/// without.
1184pub fn build_go_obligations_for(
1185    file: &str,
1186    source: &str,
1187    next_probe: &mut u64,
1188    next_decision: &mut u32,
1189    alias: &str,
1190    generics: bool,
1191) -> Result<GoFileObligations, GoInstrumenterError> {
1192    let tree = parse(source)?;
1193    let decision_base = *next_decision;
1194    let mut collector = Collector {
1195        file,
1196        source,
1197        alias,
1198        next_probe,
1199        decision_base,
1200        edits: Vec::new(),
1201        points: Vec::new(),
1202        branches: Vec::new(),
1203        decisions: Vec::new(),
1204        probes: BTreeMap::new(),
1205        limitations: Vec::new(),
1206        widths: Vec::new(),
1207        generics,
1208        conditions: std::collections::HashSet::new(),
1209        unmeasured_values: Vec::new(),
1210    };
1211    let mut cursor = tree.root_node().walk();
1212    for child in tree.root_node().children(&mut cursor) {
1213        if child.is_named() {
1214            collector.walk(child);
1215        }
1216    }
1217    // One limitation for the file, at the first such value, naming how many.
1218    if let Some((id, line, column, source_text)) = collector.unmeasured_values.first().cloned() {
1219        let count = collector.unmeasured_values.len();
1220        collector.limitations.push(serde_json::json!({
1221            "id": id,
1222            "kind": "value-decision-before-generics",
1223            "file": file,
1224            "source": source_text,
1225            "line": line,
1226            "column": column,
1227            "reason": format!("{count} `&&`/`||` used as a value in this file: keeping its type through a probe needs generics, and the module's go directive is older than 1.18"),
1228        }));
1229    }
1230    *next_decision += collector.widths.len() as u32;
1231    let mut edits = collector.edits;
1232    // The import is only needed by files that call the runtime — decisions and
1233    // branches do, a file of plain statements does not, and Go rejects an
1234    // unused import.
1235    let calls_runtime = edits
1236        .iter()
1237        .any(|edit| edit.text.contains(&format!("{alias}.")));
1238    if calls_runtime && let Some(import) = import_edit(source, alias, RUNTIME_IMPORT) {
1239        edits.push(import);
1240    }
1241    Ok(GoFileObligations {
1242        manifest: CoverageManifest {
1243            decisions: collector.decisions,
1244            points: collector.points,
1245            branches: collector.branches,
1246            limitations: collector.limitations,
1247            unmeasured: Vec::new(),
1248            scope: None,
1249            assertion_sites: Vec::new(),
1250        },
1251        probes: collector.probes,
1252        edits,
1253        decision_widths: collector.widths,
1254    })
1255}
1256
1257#[cfg(test)]
1258mod tests {
1259    use super::*;
1260
1261    /// Every field the coverage index stores for a limitation.
1262    ///
1263    /// A limitation missing one of these is written into a run that then
1264    /// cannot be opened at all -- `invalid coverage index: coverage
1265    /// limitation`, with no coverage report and nothing naming the file that
1266    /// caused it. These were writing `detail` where the index reads `reason`,
1267    /// and none of them wrote `source`, so any run that measured a for-each
1268    /// loop was unreadable.
1269    fn assert_indexable(limitations: &[serde_json::Value]) -> Vec<String> {
1270        assert!(!limitations.is_empty(), "nothing to check");
1271        for limitation in limitations {
1272            for field in ["id", "kind", "file", "source", "reason"] {
1273                assert!(
1274                    limitation.get(field).and_then(|v| v.as_str()).is_some(),
1275                    "a limitation needs a string {field}: {limitation}"
1276                );
1277            }
1278            for field in ["line", "column"] {
1279                assert!(
1280                    limitation.get(field).and_then(|v| v.as_u64()).is_some(),
1281                    "a limitation needs a number {field}: {limitation}"
1282                );
1283            }
1284        }
1285        let mut kinds = limitations
1286            .iter()
1287            .filter_map(|limitation| limitation["kind"].as_str().map(str::to_owned))
1288            .collect::<Vec<_>>();
1289        kinds.sort();
1290        kinds.dedup();
1291        kinds
1292    }
1293
1294    /// A byte offset says a file is broken without saying where to look.
1295    #[test]
1296    fn a_file_that_does_not_parse_says_where() {
1297        let broken = "package main\n\nfunc f() int {\n\treturn 1 )\n}\n";
1298        let message = parse(broken).expect_err("does not parse").to_string();
1299        assert!(message.contains("line 4"), "{message}");
1300        assert!(message.contains("return 1"), "{message}");
1301        assert!(
1302            !message.contains("byte"),
1303            "an offset is not somewhere a reader can look: {message}"
1304        );
1305    }
1306
1307    #[test]
1308    fn every_limitation_carries_what_the_index_stores() {
1309        const EVERY: &str = r#"package main
1310
1311func walk(items []int) int {
1312	sum := 0
1313	for _, item := range items {
1314		sum += item
1315	}
1316	for {
1317		break
1318	}
1319	return sum
1320}
1321"#;
1322        let mut next = 0;
1323        let mut decisions = 0;
1324        let obligations =
1325            build_go_obligations("walk.go", EVERY, &mut next, &mut decisions).expect("go");
1326        assert_eq!(
1327            assert_indexable(&obligations.manifest.limitations),
1328            ["loop-without-condition"]
1329        );
1330    }
1331
1332    const SAMPLE: &str = r#"package main
1333
1334import "fmt"
1335
1336func classify(a int, b bool) string {
1337	if a > 10 && b {
1338		return "big"
1339	}
1340	for i := 0; i < a; i++ {
1341		fmt.Println(i)
1342	}
1343	switch {
1344	case a == 0:
1345		return "zero"
1346	default:
1347		return "small"
1348	}
1349}
1350"#;
1351
1352    fn obligations(source: &str) -> GoFileObligations {
1353        let mut next = 0;
1354        let mut decisions = 0;
1355        build_go_obligations("main.go", source, &mut next, &mut decisions).expect("obligations")
1356    }
1357
1358    /// Rewriting must never produce source Go cannot compile. Re-parsing the
1359    /// output catches that without a toolchain, on every machine, every run.
1360    fn rewritten(source: &str) -> String {
1361        let mut next = 0;
1362        let mut decisions = 0;
1363        let go =
1364            build_go_obligations("x.go", source, &mut next, &mut decisions).expect("obligations");
1365        let out = rewrite(source, &go.edits);
1366        parse(&out)
1367            .unwrap_or_else(|error| panic!("rewritten source does not parse: {error}\n{out}"));
1368        out
1369    }
1370
1371    #[test]
1372    fn what_follows_an_insertion_is_where_it_was_written() {
1373        // Go reports a compile error by line and column, and a probe earlier
1374        // on the line moved the column: `./crypto.go:4:38` for `4:15`.
1375        const SOURCE: &str =
1376            "package main\n\nfunc seal(key string) string {\n\treturn key + missing\n}\n";
1377        let mut next = 0;
1378        let mut decisions = 0;
1379        let obligations = build_go_obligations("seal.go", SOURCE, &mut next, &mut decisions)
1380            .expect("obligations");
1381        let rewritten = rewrite(SOURCE, &obligations.edits);
1382        // The statement on line 4 begins at column 2, after the tab.
1383        assert!(
1384            rewritten.contains("/*line :4:2*/return key + missing"),
1385            "{rewritten}"
1386        );
1387        assert_eq!(
1388            rewritten.lines().count(),
1389            SOURCE.lines().count(),
1390            "{rewritten}"
1391        );
1392    }
1393
1394    #[test]
1395    fn a_switch_given_a_default_keeps_its_lines() {
1396        // The `default` Supercov adds to a switch that has none took three
1397        // lines, so every line below the switch was three further down:
1398        // go-version's constraint.go grew from 307 lines to 310.
1399        const SOURCE: &str = "package main\n\nfunc kind(key string) int {\n\tswitch {\n\tcase len(key) == 0:\n\t\treturn 0\n\tcase len(key) < 4:\n\t\treturn 1\n\t}\n\tswitch len(key) { case 4: return 2 }\n\tswitch key {\n\t}\n\treturn 3\n}\n";
1400        let mut next = 0;
1401        let mut decisions = 0;
1402        let obligations = build_go_obligations("kind.go", SOURCE, &mut next, &mut decisions)
1403            .expect("obligations");
1404        let rewritten = rewrite(SOURCE, &obligations.edits);
1405        assert_eq!(
1406            rewritten.lines().count(),
1407            SOURCE.lines().count(),
1408            "{rewritten}"
1409        );
1410        assert_eq!(rewritten.matches("default: ").count(), 3, "{rewritten}");
1411        // One that follows a clause ends it; one in an empty switch has
1412        // nothing to end, and `;` alone is not a clause.
1413        assert_eq!(rewritten.matches("; default: ").count(), 2, "{rewritten}");
1414        parse(&rewritten.replace(RUNTIME_IMPORT, "example.com/kind/supercov")).expect("still Go");
1415    }
1416
1417    #[test]
1418    fn a_probe_never_lands_where_go_does_not_allow_a_statement() {
1419        // `for i := 0; c; i++` has two slots that hold a statement but are not
1420        // statement positions; a probe there makes a four-clause loop that does
1421        // not compile. Same for an `if` or `switch` initialiser.
1422        let out = rewritten(
1423            "package main\nfunc f(a int) int {\n\tfor i := 0; i < a; i++ {\n\t\ta++\n\t}\n\tif b := a; b > 1 {\n\t\treturn b\n\t}\n\tswitch c := a; c {\n\tcase 1:\n\t\treturn 1\n\t}\n\treturn 0\n}\n",
1424        );
1425        assert!(
1426            !out.contains("for __supercov"),
1427            "probe in a for-clause init:\n{out}"
1428        );
1429        assert!(
1430            !out.contains("; __supercov.P"),
1431            "probe in a for-clause post:\n{out}"
1432        );
1433        assert!(
1434            out.contains("if b := a;"),
1435            "the if initialiser survived intact:\n{out}"
1436        );
1437        assert!(
1438            out.contains("switch c := a;"),
1439            "the switch initialiser survived intact:\n{out}"
1440        );
1441    }
1442
1443    #[test]
1444    fn wrapping_a_condition_preserves_short_circuit_order() {
1445        // Go evaluates a call argument only when the call is reached, so a
1446        // wrapped right-hand operand runs exactly when the unwrapped one would
1447        // have. The branch wrapper must enclose the conditions, or the decision
1448        // would be closed before its operands had been observed.
1449        let out = rewritten(
1450            "package main\nfunc f(a int, b bool) bool {\n\tif a > 10 && b {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1451        );
1452        let condition = out
1453            .lines()
1454            .find(|line| line.contains("if "))
1455            .expect("the if survived");
1456        let branch = condition.find(".BD(").expect("branch and decision wrapper");
1457        let first = condition.find(".C(").expect("first condition wrapper");
1458        assert!(
1459            branch < first,
1460            "the branch must enclose its conditions: {condition}"
1461        );
1462        assert_eq!(
1463            condition.matches(".C(").count(),
1464            2,
1465            "one wrapper per condition: {condition}"
1466        );
1467        assert!(
1468            condition.contains("&&"),
1469            "the operator itself is untouched: {condition}"
1470        );
1471    }
1472
1473    #[test]
1474    fn a_single_condition_branch_uses_the_wrapper_that_inlines() {
1475        // Most branches in real code have one operand and no independence
1476        // obligation. They take the form with no decision argument, which is
1477        // the one small enough for the Go compiler to inline.
1478        let out = rewritten(
1479            "package main\nfunc f(a int) bool {\n\tif a > 10 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1480        );
1481        assert!(out.contains(".B("), "{out}");
1482        assert!(!out.contains(".BD("), "no decision here to close:\n{out}");
1483    }
1484
1485    #[test]
1486    fn a_switch_without_a_default_gains_one_so_matching_nothing_is_observable() {
1487        // The outcome exists whether or not the author wrote a clause for it,
1488        // and it cannot be seen without one.
1489        let out = rewritten(
1490            "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1491        );
1492        assert!(out.contains("default:"), "{out}");
1493
1494        // A switch that already has one is left alone.
1495        let existing = rewritten(
1496            "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\tdefault:\n\t\treturn\n\t}\n}\n",
1497        );
1498        assert_eq!(existing.matches("default:").count(), 1, "{existing}");
1499    }
1500
1501    #[test]
1502    fn a_range_loop_records_whether_it_ran_and_a_bare_loop_a_limitation() {
1503        // A `range` loop's outcomes are whether it ran at all, as in every
1504        // other language. A bare `for {}` always enters and has no expression
1505        // to observe, so it records the limitation instead of an obligation no
1506        // test could satisfy. A function with a `goto` keeps the limitation
1507        // too: the flag's declaration may not be jumped over.
1508        let mut next = 0;
1509        let mut decisions = 0;
1510        let go = build_go_obligations(
1511            "x.go",
1512            "package main\nfunc f(xs []int) {\n\tfor _, x := range xs {\n\t\t_ = x\n\t}\n\tfor {\n\t\tbreak\n\t}\n}\nfunc g(xs []int) {\n\tfor range xs {\n\t}\n\tgoto end\nend:\n}\n",
1513            &mut next,
1514            &mut decisions,
1515        )
1516        .unwrap();
1517        let loops = go
1518            .manifest
1519            .branches
1520            .iter()
1521            .filter(|b| b.kind == "for")
1522            .collect::<Vec<_>>();
1523        assert_eq!(loops.len(), 1, "{:?}", go.manifest.branches);
1524        assert_eq!(
1525            loops[0]
1526                .alternatives
1527                .iter()
1528                .map(|a| a.label.as_str())
1529                .collect::<Vec<_>>(),
1530            ["zero iterations", "entered"]
1531        );
1532        assert_eq!(
1533            go.manifest.limitations.len(),
1534            2,
1535            "{:?}",
1536            go.manifest.limitations
1537        );
1538        assert!(
1539            go.manifest
1540                .limitations
1541                .iter()
1542                .all(|limitation| limitation["kind"] == "loop-without-condition")
1543        );
1544    }
1545
1546    #[test]
1547    fn only_a_file_that_gained_a_probe_imports_the_runtime() {
1548        // Go rejects an unused import, so a file with nothing to observe must
1549        // not get one.
1550        let out = rewritten("package main\nfunc f(a int) int {\n\treturn a\n}\n");
1551        assert!(
1552            out.contains(RUNTIME_ALIAS),
1553            "a function is itself an obligation:\n{out}"
1554        );
1555
1556        let mut next = 0;
1557        let mut decisions = 0;
1558        let bare = build_go_obligations(
1559            "t.go",
1560            "package main\n\ntype T struct{}\n",
1561            &mut next,
1562            &mut decisions,
1563        )
1564        .unwrap();
1565        assert!(bare.edits.is_empty(), "{:?}", bare.edits);
1566        assert_eq!(
1567            rewrite("package main\n\ntype T struct{}\n", &bare.edits),
1568            "package main\n\ntype T struct{}\n"
1569        );
1570    }
1571
1572    #[test]
1573    fn a_short_circuiting_operator_splits_a_decision_and_nothing_else_does() {
1574        // `&&` and `||` are the only operators that let one condition decide
1575        // the outcome without the other running, which is what MC/DC is about.
1576        // A comparison is one condition however many operands it reads.
1577        let go = obligations(SAMPLE);
1578        let decision = go
1579            .manifest
1580            .decisions
1581            .iter()
1582            .find(|d| d.kind == "if")
1583            .expect("the if carries a decision");
1584        assert_eq!(decision.conditions, ["a > 10", "b"]);
1585
1586        // A single condition needs no independence obligation: the branch
1587        // outcomes already say everything it could.
1588        let simple = obligations(
1589            "package main\nfunc f(a int) bool {\n\tif a > 1 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1590        );
1591        assert!(
1592            simple.manifest.decisions.is_empty(),
1593            "{:?}",
1594            simple.manifest.decisions
1595        );
1596    }
1597
1598    #[test]
1599    fn negation_and_parentheses_do_not_invent_conditions() {
1600        // `!b` is `b` negated, not a second condition, and a parenthesised
1601        // group is transparent. Counting either as extra would inflate the
1602        // MC/DC denominator with obligations no test can satisfy separately.
1603        let go = obligations(
1604            "package main\nfunc f(a int, b bool, c bool) bool {\n\tif (a > 1 || !b) && c {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1605        );
1606        let decision = &go.manifest.decisions[0];
1607        assert_eq!(decision.conditions, ["a > 1", "!b", "c"]);
1608    }
1609
1610    #[test]
1611    fn every_branching_construct_records_the_outcome_that_was_not_taken() {
1612        // An `if` with no else, a loop that never runs, and a switch that
1613        // matches nothing are all outcomes a reader has to see. Recording only
1614        // the arm that executed would make an untested guard look exercised.
1615        let go = obligations(SAMPLE);
1616        let by_kind = |kind: &str| {
1617            go.manifest
1618                .branches
1619                .iter()
1620                .find(|b| b.kind == kind)
1621                .map(|b| {
1622                    b.alternatives
1623                        .iter()
1624                        .map(|a| a.label.clone())
1625                        .collect::<Vec<_>>()
1626                })
1627        };
1628        assert_eq!(by_kind("if").unwrap(), ["true", "false"]);
1629        // A loop's condition is true on every iteration and false when it
1630        // stops, so the honest labels are the condition's own outcomes rather
1631        // than "entered" and "skipped".
1632        assert_eq!(by_kind("loop").unwrap(), ["true", "false"]);
1633        let switch = by_kind("switch").unwrap();
1634        assert!(switch.contains(&"default".to_owned()), "{switch:?}");
1635        assert!(
1636            !switch.contains(&"no case matched".to_owned()),
1637            "a default already covers it"
1638        );
1639
1640        // Without a default, the fall-through outcome is named explicitly.
1641        let open = obligations(
1642            "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1643        );
1644        let labels = open
1645            .manifest
1646            .branches
1647            .iter()
1648            .find(|b| b.kind == "switch")
1649            .unwrap()
1650            .alternatives
1651            .iter()
1652            .map(|a| a.label.clone())
1653            .collect::<Vec<_>>();
1654        assert!(labels.contains(&"no case matched".to_owned()), "{labels:?}");
1655    }
1656
1657    #[test]
1658    fn functions_and_statements_are_separate_obligations_with_locations() {
1659        let go = obligations(SAMPLE);
1660        let functions = go
1661            .manifest
1662            .points
1663            .iter()
1664            .filter(|p| p.kind == PointKind::Function)
1665            .collect::<Vec<_>>();
1666        assert_eq!(functions.len(), 1);
1667        assert_eq!(functions[0].label.as_deref(), Some("classify"));
1668        assert_eq!(functions[0].line, 5);
1669
1670        let statements = go
1671            .manifest
1672            .points
1673            .iter()
1674            .filter(|p| p.kind == PointKind::Statement)
1675            .count();
1676        assert!(
1677            statements >= 6,
1678            "expected the body's statements, got {statements}"
1679        );
1680        // Every obligation is anchored; an unanchorable line is absent rather
1681        // than counted as uncovered.
1682        assert!(
1683            go.manifest
1684                .points
1685                .iter()
1686                .all(|p| p.line > 0 && p.column > 0)
1687        );
1688        // Imports and type declarations carry no coverage question.
1689        assert!(
1690            !go.manifest
1691                .points
1692                .iter()
1693                .any(|p| p.source.starts_with("import"))
1694        );
1695    }
1696
1697    #[test]
1698    fn every_obligation_has_a_probe_and_malformed_source_is_refused() {
1699        let go = obligations(SAMPLE);
1700        let expected = go.manifest.points.len()
1701            + go.manifest
1702                .branches
1703                .iter()
1704                .map(|b| b.alternatives.len())
1705                .sum::<usize>();
1706        // Points and branch alternatives carry probes. Conditions and outcomes
1707        // do not: their vector already says which ran and what the decision
1708        // came to, so a probe would be the same fact stored twice.
1709        assert_eq!(go.probes.len(), expected);
1710        // A decision is still an obligation, answered by the width the runtime
1711        // sizes its vector from rather than by a probe.
1712        assert_eq!(go.decision_widths.len(), go.manifest.decisions.len());
1713
1714        // Guessing at a file that does not parse would put obligations on lines
1715        // that may not exist.
1716        let mut next = 0;
1717        let mut decisions = 0;
1718        assert!(matches!(
1719            build_go_obligations(
1720                "broken.go",
1721                "package main\nfunc f( {",
1722                &mut next,
1723                &mut decisions
1724            ),
1725            Err(GoInstrumenterError::Parse(_))
1726        ));
1727    }
1728
1729    #[test]
1730    fn go_1_26_new_of_a_value_is_measured_rather_than_dropped() {
1731        // Go 1.26 let `new` take a value, not only a type: `new(x)` returns a
1732        // pointer to a copy of `x`. tree-sitter-go still models new and make
1733        // as a call whose one argument must be a type, so every file holding
1734        // one failed to parse -- and a file that does not parse carries no
1735        // obligations, so a package using the construct was measured as
1736        // though it held nothing. It appears 913 times in one codebase this
1737        // was reported from, and code generators emit it, so a project can
1738        // reintroduce it on every `go generate`.
1739        let go = obligations(
1740            "package p\n\nfunc Greeting() *string {\n\treturn new(\"hello\")\n}\n\nfunc Sum(a, b int) *int {\n\treturn new(a + b)\n}\n\nfunc Copy(v int) *int {\n\tif v > 0 {\n\t\treturn new(v)\n\t}\n\treturn new(0)\n}\n",
1741        );
1742        assert_eq!(
1743            go.manifest
1744                .points
1745                .iter()
1746                .filter(|p| p.kind == PointKind::Function)
1747                .count(),
1748            3,
1749            "every function in the file is an obligation: {:?}",
1750            go.manifest.points
1751        );
1752        assert_eq!(
1753            go.manifest.branches.len(),
1754            1,
1755            "the `if` beside it is still a branch with both arms to answer for"
1756        );
1757        // And the obligation quotes the source the author wrote. The parse
1758        // reads a stand-in for the construct tree-sitter cannot express; what
1759        // the manifest records has to be the file itself, or the report shows
1760        // the reader a line that is not in their repository.
1761        assert!(
1762            go.manifest
1763                .points
1764                .iter()
1765                .any(|point| point.source.contains("new(\"hello\")")),
1766            "{:?}",
1767            go.manifest.points
1768        );
1769        assert!(
1770            !go.manifest
1771                .points
1772                .iter()
1773                .any(|point| point.source.contains("nEw")),
1774            "{:?}",
1775            go.manifest.points
1776        );
1777    }
1778
1779    #[test]
1780    fn new_of_a_type_is_left_exactly_as_it_was() {
1781        // Only the value form needs the stand-in. Every type form already
1782        // parses, and rewriting those would turn `new(map[string]int)` --
1783        // whose argument is a type and nothing else -- into something that
1784        // does not parse at all.
1785        let go = obligations(
1786            "package p\n\nfunc Types() {\n\t_ = new(int)\n\t_ = new(map[string]int)\n\t_ = new([]byte)\n\t_ = new(chan int)\n\t_ = new(struct{ A int })\n\t_ = new(func(int) error)\n\t_ = new([3]int)\n\t_ = new(interface{})\n}\n",
1787        );
1788        assert_eq!(
1789            go.manifest
1790                .points
1791                .iter()
1792                .filter(|p| p.kind == PointKind::Function)
1793                .count(),
1794            1
1795        );
1796        // The same file gaining one value form must not cost the type forms
1797        // their parse: the stand-in is chosen per call site, not per file.
1798        let mixed = obligations(
1799            "package p\n\nfunc Mixed(v int) {\n\t_ = new(map[string][]chan int)\n\t_ = new(v)\n\t_ = new(\"x\")\n}\n",
1800        );
1801        assert_eq!(
1802            mixed
1803                .manifest
1804                .points
1805                .iter()
1806                .filter(|p| p.kind == PointKind::Function)
1807                .count(),
1808            1
1809        );
1810    }
1811
1812    #[test]
1813    fn a_method_called_new_is_not_the_builtin() {
1814        // `new` is not a keyword, so `x.new(v)` is an ordinary method call and
1815        // always parsed. A stand-in written over it would be a rewrite of
1816        // somebody's own API for no reason -- and the file still has to parse
1817        // because of the builtin call beside it.
1818        let go = obligations(
1819            "package p\n\ntype pool struct{}\n\nfunc (p pool) new(v int) *int {\n\treturn new(v + 1)\n}\n\nfunc use(p pool) *int {\n\treturn p.new(2)\n}\n",
1820        );
1821        assert!(
1822            go.manifest
1823                .points
1824                .iter()
1825                .any(|point| point.source.contains("p.new(2)")),
1826            "{:?}",
1827            go.manifest.points
1828        );
1829    }
1830
1831    #[test]
1832    fn a_file_with_runes_outside_ascii_is_scanned_without_splitting_one() {
1833        // The scan that finds a `new(` call site walks bytes, and Go source is
1834        // UTF-8: a comment or a string with a rune in it puts multi-byte
1835        // sequences between the scanner and the call. Splitting one is not a
1836        // wrong answer, it is a panic -- Supercov would take down the suite it
1837        // was asked to measure, on a file whose only crime is a name with an
1838        // accent in it.
1839        let go = obligations(
1840            "package p\n\n// Prüfen: gibt \u{e4}\u{f6}\u{fc} zur\u{fc}ck.\nfunc Gr\u{fc}\u{df}en() *string {\n\tsch\u{f6}n := \"gr\u{fc}\u{df}e \u{2014} \u{fc}ber alles\"\n\treturn new(sch\u{f6}n + \"!\")\n}\n",
1841        );
1842        assert_eq!(
1843            go.manifest
1844                .points
1845                .iter()
1846                .filter(|p| p.kind == PointKind::Function)
1847                .count(),
1848            1
1849        );
1850    }
1851
1852    #[test]
1853    fn the_scan_never_mistakes_text_for_code() {
1854        // The scan that finds a `new(` call site has to know when Go source is
1855        // not Go: a `)` inside a string closes nothing, `// new(` is a remark,
1856        // and a backquoted string has no escapes at all. Reading any of them
1857        // as code finds a call site that is not there, and a stand-in written
1858        // over one is a rewrite of somebody's string.
1859        let go = obligations(concat!(
1860            "package p\n\n",
1861            "func f(v int) *int {\n",
1862            // Every one of these holds the text `new(` and none is a call.
1863            "\t_ = \"new(\\\"x\\\") and an unclosed ( paren\"\n",
1864            "\t_ = `raw new(\"y\") with ) and \\ backslash`\n",
1865            "\t_ = '('\n",
1866            "\t// new(\"comment\") and a stray )\n",
1867            "\t/* new(\"block\") ) */\n",
1868            // And this one is.
1869            "\treturn new(v + 1)\n}\n",
1870        ));
1871        assert_eq!(
1872            go.manifest
1873                .points
1874                .iter()
1875                .filter(|p| p.kind == PointKind::Function)
1876                .count(),
1877            1
1878        );
1879
1880        // The same again where the only `new(` is inside text, so the file
1881        // parses without the stand-in ever being spent.
1882        let untouched =
1883            obligations("package p\n\nfunc g() string {\n\treturn `new(\"only in text\")`\n}\n");
1884        assert!(
1885            untouched
1886                .manifest
1887                .points
1888                .iter()
1889                .any(|point| point.source.contains("new(\"only in text\")")),
1890            "{:?}",
1891            untouched.manifest.points
1892        );
1893    }
1894
1895    #[test]
1896    fn no_shape_of_input_can_make_the_scan_panic() {
1897        // The scan walks bytes, and every byte position is reachable: a file
1898        // can end inside a string, inside a comment, inside the argument list
1899        // of a `new(` that is never closed. A wrong answer there is a file
1900        // reported as unparseable; a panic is Supercov taking down the suite
1901        // it was asked to measure. So every truncation of a file holding one
1902        // of each is parsed, and the only claim made is that it returns.
1903        let awkward = concat!(
1904            "package p\n\n",
1905            "// gr\u{fc}\u{df}e \u{2014} new(\"in a comment\")\n",
1906            "func f(sch\u{f6}n int) *int {\n",
1907            "\t_ = \"\u{fc}ber new(\\\"in a string\\\"\"\n",
1908            "\t_ = `\u{e4}\u{f6}\u{fc} new(\n",
1909            "\t_ = '\u{fc}'\n",
1910            "\t/* new(\"unterminated\n",
1911            "\treturn new(sch\u{f6}n + 1)\n}\n",
1912        );
1913        for end in 0..=awkward.len() {
1914            // Only on a rune boundary: a `&str` cannot hold half a rune, so a
1915            // split elsewhere is not an input Supercov could ever be given.
1916            if !awkward.is_char_boundary(end) {
1917                continue;
1918            }
1919            let _ = parse(&awkward[..end]);
1920            // And the same read from the other end, so a construct that opens
1921            // before the window is covered too.
1922            if awkward.is_char_boundary(end) {
1923                let _ = parse(&awkward[end..]);
1924            }
1925        }
1926
1927        // A `new(` whose argument list never closes, and one at the very end
1928        // of a file, are the two positions the scan's own bounds depend on.
1929        for source in [
1930            "package p\n\nfunc f() {\n\t_ = new(\n",
1931            "package p\n\nfunc f() {\n\t_ = new",
1932            "package p\n\nfunc f() {\n\t_ = new(",
1933            "package p\n\nfunc f() {\n\t_ = new()\n}\n",
1934            "new",
1935            "new(",
1936            "",
1937        ] {
1938            let _ = parse(source);
1939        }
1940    }
1941
1942    #[test]
1943    fn the_scan_reads_new_the_way_go_spells_it() {
1944        // Every shape of the call site the scanner has to recognise or leave
1945        // alone, gathered because the measurement said so: the whitespace
1946        // Go permits between a callee and its arguments was written for and
1947        // never exercised, and neither was an identifier that merely ends in
1948        // the word.
1949        let go = obligations(concat!(
1950            "package p\n\n",
1951            "type pool struct{}\n\n",
1952            "func (p pool) new(v int) *int {\n\treturn &v\n}\n\n",
1953            "func renew(v string) *string {\n\treturn &v\n}\n\n",
1954            "func f(p pool, v int) *int {\n",
1955            // Go allows space, and a newline, between `new` and its argument.
1956            "\t_ = new (\"spaced\")\n",
1957            "\t_ = new\t(\"tabbed\")\n",
1958            // A method of somebody's own, which is not the builtin.
1959            "\t_ = p.new(2)\n",
1960            // An identifier that ends in the word, which is not it either.
1961            "\t_ = renew(\"x\")\n",
1962            // And an identifier that starts with it.
1963            "\tnewValue := v\n",
1964            "\treturn new(newValue)\n}\n",
1965        ));
1966        assert_eq!(
1967            go.manifest
1968                .points
1969                .iter()
1970                .filter(|p| p.kind == PointKind::Function)
1971                .count(),
1972            3,
1973            "{:?}",
1974            go.manifest.points
1975        );
1976        // What the author wrote is what the obligations quote, spacing and all.
1977        assert!(
1978            go.manifest
1979                .points
1980                .iter()
1981                .any(|point| point.source.contains("new (\"spaced\")")),
1982            "{:?}",
1983            go.manifest.points
1984        );
1985        assert!(
1986            !go.manifest
1987                .points
1988                .iter()
1989                .any(|point| point.source.contains("nEw")),
1990            "{:?}",
1991            go.manifest.points
1992        );
1993    }
1994
1995    #[test]
1996    fn a_file_that_is_simply_broken_still_does_not_parse() {
1997        // The stand-in must not become a way for any unparseable file to slip
1998        // through: what it cannot read is still a hole in the denominator, and
1999        // a hole reported as measured is a wrong number.
2000        let mut next = 0;
2001        let mut decisions = 0;
2002        assert!(matches!(
2003            build_go_obligations(
2004                "broken.go",
2005                "package p\n\nfunc f() {\n\t_ = new(1)\n\tif {\n}\n",
2006                &mut next,
2007                &mut decisions
2008            ),
2009            Err(GoInstrumenterError::Parse(_))
2010        ));
2011    }
2012
2013    #[test]
2014    fn decisions_are_numbered_across_the_module_not_within_a_file() {
2015        // The runtime holds one decision-state array for the whole module, so
2016        // an index meaning "the first decision in this file" would land on
2017        // every other file's first decision: two files would share condition
2018        // state, and the vectors both produced would describe neither.
2019        let mut next = 0;
2020        let mut decisions = 0;
2021        let first = build_go_obligations(
2022            "a.go",
2023            "package p\n\nfunc A(x, y bool) bool {\n\tif x && y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
2024            &mut next,
2025            &mut decisions,
2026        )
2027        .unwrap();
2028        let second = build_go_obligations(
2029            "b.go",
2030            "package p\n\nfunc B(x, y bool) bool {\n\tif x || y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
2031            &mut next,
2032            &mut decisions,
2033        )
2034        .unwrap();
2035
2036        let referenced = |obligations: &GoFileObligations| {
2037            obligations
2038                .edits
2039                .iter()
2040                .filter_map(|edit| {
2041                    let at = edit.text.find(".C(")?;
2042                    edit.text[at + 3..]
2043                        .split(',')
2044                        .next()?
2045                        .trim()
2046                        .parse::<u32>()
2047                        .ok()
2048                })
2049                .collect::<std::collections::BTreeSet<_>>()
2050        };
2051        assert_eq!(referenced(&first), [0].into());
2052        assert_eq!(referenced(&second), [1].into());
2053        assert_eq!(decisions, 2, "the module numbered two decisions in all");
2054        // And the widths each file reports stay in the order the ids assume,
2055        // so the concatenated table lines up with the concatenated manifest.
2056        assert_eq!(first.decision_widths, [2]);
2057        assert_eq!(second.decision_widths, [2]);
2058    }
2059}