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}
428
429/// A branch whose condition is a single operand needs no independence
430/// obligation; the runtime's sentinel says so.
431/// The wrapper a branch needs. A single-operand condition has no independence
432/// obligation, so it gets the form with no decision argument — which is the
433/// one small enough for Go to inline, and the majority of branches in real
434/// code.
435fn branch_wrapper(alias: &str, when_true: u64, when_false: u64, decision: Option<usize>) -> String {
436    match decision {
437        Some(index) => format!("{alias}.BD({when_true}, {when_false}, {index}, "),
438        None => format!("{alias}.B({when_true}, {when_false}, "),
439    }
440}
441
442/// Whether a probe may be placed before this node.
443///
444/// Go has slots that hold a statement but are not statement positions: the
445/// init and post clauses of a `for`, and the initialiser of an `if` or a
446/// `switch`. A probe there turns `for i := 0; c; i++` into a four-clause loop
447/// that does not compile. Every real statement is a child of a `statement_list`,
448/// so that is the rule rather than a list of exceptions to remember.
449/// The `fallthrough` a case clause ends with, if it ends with one.
450fn ending_fallthrough(clause: Node) -> Option<Node> {
451    let body = clause
452        .children(&mut clause.walk())
453        .find(|child| child.kind() == "statement_list")?;
454    let last = body.named_child(body.named_child_count().checked_sub(1)?)?;
455    (last.kind() == "fallthrough_statement").then_some(last)
456}
457
458fn in_statement_position(node: Node) -> bool {
459    node.parent()
460        .is_some_and(|parent| parent.kind() == "statement_list")
461}
462
463/// The condition of a `for` loop, when it has one. A `range` clause and a bare
464/// `for {}` have none.
465fn loop_condition<'t>(node: Node<'t>) -> Option<Node<'t>> {
466    if let Some(condition) = node.child_by_field_name("condition") {
467        return Some(condition);
468    }
469    let mut cursor = node.walk();
470    let clause = node
471        .children(&mut cursor)
472        .find(|child| child.kind() == "for_clause")?;
473    let mut inner = clause.walk();
474    clause
475        .children(&mut inner)
476        .find(|child| child.is_named() && child.kind().ends_with("_expression"))
477}
478
479impl<'a> Collector<'a> {
480    fn id(&mut self, node: Node, kind: &str) -> String {
481        stable_obligation_id("go", self.file, kind, node.start_byte(), node.end_byte())
482    }
483
484    /// A limitation's identity. The contract requires one, and requires it to
485    /// be unique: a manifest whose limitations cannot be told apart cannot say
486    /// which surface each one is about, so the reader refuses the run rather
487    /// than present a list nobody can act on.
488    fn limitation_id(&mut self, node: Node, kind: &str) -> String {
489        self.id(node, kind)
490    }
491
492    fn probe(&mut self, target: GoProbeTarget, at: usize) -> u64 {
493        *self.next_probe += 1;
494        let id = *self.next_probe;
495        self.probes.insert(id, GoProbe { id, target, at });
496        id
497    }
498
499    fn edit(&mut self, at: usize, rank: i32, text: String) {
500        self.edits.push(GoEdit { at, rank, text });
501    }
502
503    /// Insert a statement-position call. Go allows `a(); b()` on one line, so a
504    /// probe never changes which line a statement reports as its own.
505    fn call_before(&mut self, at: usize, call: String) {
506        self.edit(at, 100, format!("{call}; "));
507    }
508
509    /// Line and column are one-based, matching every other frontend.
510    fn position(&self, node: Node) -> (usize, usize) {
511        let start = node.start_position();
512        (start.row + 1, start.column + 1)
513    }
514
515    fn text(&self, node: Node) -> String {
516        let raw = &self.source[node.byte_range()];
517        let line = raw.lines().next().unwrap_or("");
518        line.trim().to_owned()
519    }
520
521    fn add_point(&mut self, node: Node, kind: PointKind, label: Option<String>) {
522        let (line, column) = self.position(node);
523        let id = self.id(
524            node,
525            match kind {
526                PointKind::Function => "function",
527                PointKind::Statement => "statement",
528            },
529        );
530        let target = match kind {
531            PointKind::Function => GoProbeTarget::Function { id: id.clone() },
532            PointKind::Statement => GoProbeTarget::Statement { id: id.clone() },
533        };
534        // A function is observed just inside its body, because the declaration
535        // itself is not a place a statement may go.
536        let at = match kind {
537            PointKind::Function => match node.child_by_field_name("body") {
538                Some(body) => body.start_byte() + 1,
539                None => return,
540            },
541            PointKind::Statement => node.start_byte(),
542        };
543        let probe = self.probe(target, at);
544        // A direct store into the array this package already holds, not a call
545        // that would have to find it first. This is the shape Go's own cover
546        // tool and JaCoCo both settled on: one move instruction.
547        self.call_before(at, format!("{HITS_VARIABLE}[{probe}] = 2"));
548        self.points.push(PointMeta {
549            id,
550            kind,
551            file: self.file.to_owned(),
552            line,
553            column,
554            source: self.text(node),
555            label,
556        });
557    }
558
559    fn add_branch(&mut self, node: Node, kind: &str, labels: &[&str]) -> Vec<u64> {
560        let (line, column) = self.position(node);
561        let id = self.id(node, "branch");
562        let mut probes = Vec::new();
563        let alternatives = labels
564            .iter()
565            .map(|label| {
566                let alternative = format!("{id}.{label}");
567                probes.push(self.probe(
568                    GoProbeTarget::Alternative {
569                        branch: id.clone(),
570                        alternative: alternative.clone(),
571                    },
572                    node.start_byte(),
573                ));
574                BranchAlternativeMeta {
575                    id: alternative,
576                    label: (*label).to_owned(),
577                }
578            })
579            .collect();
580        self.branches.push(BranchMeta {
581            id,
582            kind: kind.to_owned(),
583            file: self.file.to_owned(),
584            line,
585            column,
586            source: self.text(node),
587            alternatives,
588        });
589        probes
590    }
591
592    /// Record a boolean expression as a decision when it has more than one
593    /// condition. A single condition needs no MC/DC obligation: its branch
594    /// outcomes already say everything independence could.
595    /// Returns the runtime's index for this decision, or `None` when the
596    /// expression has a single condition and needs no independence obligation.
597    fn add_decision(&mut self, node: Node, kind: &str) -> Option<usize> {
598        let mut leaves = Vec::new();
599        condition_nodes(node, self.source, &mut leaves);
600        if leaves.len() < 2 {
601            return None;
602        }
603        let conditions = leaves
604            .iter()
605            .map(|leaf| self.source[leaf.byte_range()].trim().to_owned())
606            .collect::<Vec<_>>();
607        let (line, column) = self.position(node);
608        let id = self.id(node, "decision");
609        // The runtime indexes decisions by position across the whole module,
610        // so the index a wrapper carries is this decision's place in the
611        // module's width table, not in this file's.
612        let index_of_decision = self.decision_base as usize + self.widths.len();
613        self.widths.push(leaves.len().min(64) as u8);
614        let alias = self.alias.to_owned();
615        // No probe per condition or outcome. The recorded vector already says
616        // which conditions were evaluated and what the decision came to, so a
617        // probe beside it would store the same fact twice and charge for it on
618        // every evaluation.
619        for (index, leaf) in leaves.iter().enumerate() {
620            // Wrapping an operand keeps short-circuiting intact: Go evaluates a
621            // call argument only when the call is reached, so the right-hand
622            // wrapper runs exactly when the unwrapped operand would have.
623            self.edit(
624                leaf.start_byte(),
625                20,
626                format!("{alias}.C({index_of_decision}, {index}, "),
627            );
628            self.edit(leaf.end_byte(), 20, ")".to_owned());
629        }
630        self.decisions.push(DecisionMeta {
631            id,
632            file: self.file.to_owned(),
633            line,
634            column,
635            source: self.text(node),
636            conditions,
637            kind: kind.to_owned(),
638        });
639        Some(index_of_decision)
640    }
641
642    fn walk(&mut self, node: Node) {
643        let alias = self.alias.to_owned();
644        match node.kind() {
645            "function_declaration" | "method_declaration" | "func_literal" => {
646                let label = node
647                    .child_by_field_name("name")
648                    .map(|name| self.source[name.byte_range()].to_owned());
649                self.add_point(node, PointKind::Function, label);
650            }
651            "if_statement" => {
652                if let Some(condition) = node.child_by_field_name("condition") {
653                    // An `if` without an else still has two outcomes: the body
654                    // ran, or control passed it by. Wrapping the condition
655                    // records the arm that was *not* taken by never setting its
656                    // bit, which is what makes an untested guard visible.
657                    let probes = self.add_branch(node, "if", &["true", "false"]);
658                    let decision = self.add_decision(condition, "if");
659                    self.edit(
660                        condition.start_byte(),
661                        5,
662                        branch_wrapper(&alias, probes[0], probes[1], decision),
663                    );
664                    self.edit(condition.end_byte(), 5, ")".to_owned());
665                }
666            }
667            "for_statement" => {
668                // A `for` with a condition branches on it. A `range` loop and a
669                // bare `for {}` have no expression to observe, so Supercov
670                // records the limitation rather than an obligation it cannot
671                // measure.
672                match loop_condition(node) {
673                    Some(condition) => {
674                        let probes = self.add_branch(node, "loop", &["true", "false"]);
675                        let decision = self.add_decision(condition, "loop");
676                        self.edit(
677                            condition.start_byte(),
678                            5,
679                            branch_wrapper(&alias, probes[0], probes[1], decision),
680                        );
681                        self.edit(condition.end_byte(), 5, ")".to_owned());
682                    }
683                    None => {
684                        let (line, column) = self.position(node);
685                        let limitation = self.limitation_id(node, "loop-without-condition");
686                        self.limitations.push(serde_json::json!({
687                            "id": limitation,
688                            "kind": "loop-without-condition",
689                            "file": self.file,
690                            "source": self.text(node),
691                            "line": line,
692                            "column": column,
693                            "reason": "a range or unconditional loop has no condition to observe, so no branch obligation is recorded for it",
694                        }));
695                    }
696                }
697            }
698            "expression_switch_statement" | "type_switch_statement" | "select_statement" => {
699                let kind = match node.kind() {
700                    "expression_switch_statement" => "switch",
701                    "type_switch_statement" => "type-switch",
702                    _ => "select",
703                };
704                let mut cases = Vec::new();
705                let mut has_default = false;
706                let mut cursor = node.walk();
707                for child in node.children(&mut cursor) {
708                    match child.kind() {
709                        "expression_case" | "type_case" | "communication_case" => {
710                            cases.push((self.text(child), Some(child)))
711                        }
712                        "default_case" => {
713                            has_default = true;
714                            cases.push(("default".to_owned(), Some(child)));
715                        }
716                        _ => {}
717                    }
718                }
719                // A switch with no default can match nothing, which is an
720                // outcome a reader has to see, and synthesising the clause is
721                // the only way to observe it: an added `default` holding
722                // nothing but a probe is the path the program already took.
723                //
724                // A select is not a switch. Without a default it *blocks*
725                // until one of its cases is ready; with one it returns
726                // immediately. So there is no "no case matched" outcome to
727                // record -- a select always selects -- and adding the clause
728                // to observe one would stop it blocking. That is not an
729                // instrument changing what is known about a program, it is an
730                // instrument changing what the program does: a loop that waited
731                // for values spins instead, and the code returns an empty
732                // buffer it reports as full.
733                if !has_default && node.kind() != "select_statement" {
734                    cases.push(("no case matched".to_owned(), None));
735                }
736                let labels = cases
737                    .iter()
738                    .map(|(label, _)| label.as_str())
739                    .collect::<Vec<_>>();
740                let probes = self.add_branch(node, kind, &labels);
741                // A case the one before it falls through into runs without its
742                // own expression being matched: `case n > 10: ...; fallthrough`
743                // runs `case n > 5`'s body for 20, and crediting that case as
744                // selected claimed a test of `n > 5` none made. The case before
745                // sets a flag as it falls through; the next one records only
746                // when the flag is clear, and clears it.
747                let fallthroughs = cases
748                    .iter()
749                    .map(|(_, clause)| clause.and_then(|clause| ending_fallthrough(clause)))
750                    .collect::<Vec<_>>();
751                let flag = format!("_scvFell{}", node.start_byte());
752                if fallthroughs.iter().any(Option::is_some) {
753                    // Before a label too, so `break L` still names the switch.
754                    let statement = node
755                        .parent()
756                        .filter(|parent| parent.kind() == "labeled_statement")
757                        .unwrap_or(node);
758                    self.edit(statement.start_byte(), 90, format!("var {flag} bool; "));
759                    for fallthrough in fallthroughs.iter().flatten() {
760                        self.call_before(fallthrough.start_byte(), format!("{flag} = true"));
761                    }
762                }
763                for (index, (probe, (_, clause))) in probes.iter().zip(cases.iter()).enumerate() {
764                    match clause {
765                        Some(clause) => {
766                            let at = clause
767                                .children(&mut clause.walk())
768                                .find(|child| child.kind() == "statement_list")
769                                .map(|body| body.start_byte())
770                                .unwrap_or_else(|| clause.end_byte());
771                            if index > 0 && fallthroughs[index - 1].is_some() {
772                                self.edit(
773                                    at,
774                                    100,
775                                    format!("if {flag} {{ {flag} = false }} else {{ {alias}.A({probe}) }}; "),
776                                );
777                            } else {
778                                self.call_before(at, format!("{alias}.A({probe})"));
779                            }
780                        }
781                        None => {
782                            // Before the switch's closing brace, and on its
783                            // line: three lines of its own moved every line
784                            // below the switch, so a failure further down
785                            // the file was reported three lines late. The
786                            // `;` ends whatever the last clause holds when
787                            // the switch is written on one line; a switch
788                            // with no clause has nothing to end.
789                            let at = node.end_byte().saturating_sub(1);
790                            let after_a_clause = cases.iter().any(|(_, clause)| clause.is_some());
791                            self.edit(
792                                at,
793                                100,
794                                format!(
795                                    "{}default: {alias}.A({probe}) ",
796                                    if after_a_clause { "; " } else { "" }
797                                ),
798                            );
799                        }
800                    }
801                }
802            }
803            kind if is_statement(kind) && in_statement_position(node) => {
804                self.add_point(node, PointKind::Statement, None);
805            }
806            _ => {}
807        }
808        // `if`, `for` and `switch` are statements too, and their own point is
809        // what says the construct was reached at all.
810        if in_statement_position(node)
811            && matches!(
812                node.kind(),
813                "if_statement"
814                    | "for_statement"
815                    | "expression_switch_statement"
816                    | "type_switch_statement"
817                    | "select_statement"
818            )
819        {
820            self.add_point(node, PointKind::Statement, None);
821        }
822        let mut cursor = node.walk();
823        for child in node.children(&mut cursor) {
824            if child.is_named() {
825                self.walk(child);
826            }
827        }
828    }
829}
830
831/// Flatten a boolean expression into its independent conditions.
832///
833/// `&&` and `||` are the only short-circuiting operators Go has, so they are
834/// the only ones that split a decision. `!` negates a condition rather than
835/// introducing one, and a parenthesised group is transparent.
836fn condition_nodes<'t>(node: Node<'t>, source: &str, out: &mut Vec<Node<'t>>) {
837    match node.kind() {
838        "binary_expression" => {
839            let operator = node
840                .child_by_field_name("operator")
841                .map(|op| &source[op.byte_range()])
842                .unwrap_or("");
843            if operator == "&&" || operator == "||" {
844                if let Some(left) = node.child_by_field_name("left") {
845                    condition_nodes(left, source, out);
846                }
847                if let Some(right) = node.child_by_field_name("right") {
848                    condition_nodes(right, source, out);
849                }
850                return;
851            }
852            out.push(node);
853        }
854        "parenthesized_expression" => {
855            let mut cursor = node.walk();
856            match node.children(&mut cursor).find(|child| child.is_named()) {
857                Some(inner) => condition_nodes(inner, source, out),
858                None => out.push(node),
859            }
860        }
861        _ => out.push(node),
862    }
863}
864
865/// One source edit. Every probe is an insertion at a byte offset; wrapping an
866/// expression is two of them, at its start and its end.
867///
868/// `rank` orders edits landing on the same offset. Applying right to left, the
869/// edit inserted last ends up leftmost, so an outer wrapper carries a lower
870/// rank than the inner one it encloses.
871#[derive(Debug, Clone, PartialEq, Eq)]
872pub struct GoEdit {
873    pub at: usize,
874    pub rank: i32,
875    pub text: String,
876}
877
878/// Apply edits to source, right to left so earlier offsets stay valid.
879///
880/// What is inserted is followed by a comment that tells Go where the text
881/// after it was written, `/*line :4:9*/`. Go reports a compile error, and
882/// `go vet` a finding, by line and column, and a probe earlier on the line
883/// moved the column: `./crypto.go:4:38: undefined` for `4:15`. With no file
884/// named, the comment keeps the name the file already has.
885pub fn rewrite(source: &str, edits: &[GoEdit]) -> String {
886    let mut ordered = edits.to_vec();
887    ordered.sort_by(|a, b| b.at.cmp(&a.at).then(b.rank.cmp(&a.rank)));
888    let lines = std::iter::once(0)
889        .chain(source.match_indices('\n').map(|(at, _)| at + 1))
890        .collect::<Vec<_>>();
891    let mut out = source.to_owned();
892    let mut placed = None;
893    for edit in ordered {
894        if edit.at > source.len() {
895            continue;
896        }
897        if placed != Some(edit.at) {
898            placed = Some(edit.at);
899            let line = lines.partition_point(|start| *start <= edit.at);
900            let column = edit.at - lines[line - 1] + 1;
901            out.insert_str(edit.at, &format!("/*line :{line}:{column}*/"));
902        }
903        out.insert_str(edit.at, &edit.text);
904    }
905    out
906}
907
908/// The import rewritten source needs, placed straight after the package
909/// clause and on its line: a line of its own moved every line below it, and
910/// with it every line Go reports, `crypto.go:24` in a panic for line 23. Go
911/// rejects an unused import, so this is only added to a file that gained at
912/// least one probe.
913pub fn import_edit(source: &str, alias: &str, path: &str) -> Option<GoEdit> {
914    let tree = parse(source).ok()?;
915    let mut cursor = tree.root_node().walk();
916    let package = tree
917        .root_node()
918        .children(&mut cursor)
919        .find(|child| child.kind() == "package_clause")?;
920    Some(GoEdit {
921        at: package.end_byte(),
922        rank: 0,
923        text: format!("; import {alias} \"{path}\""),
924    })
925}
926
927pub fn build_go_obligations(
928    file: &str,
929    source: &str,
930    next_probe: &mut u64,
931    next_decision: &mut u32,
932) -> Result<GoFileObligations, GoInstrumenterError> {
933    build_go_obligations_with_alias(file, source, next_probe, next_decision, RUNTIME_ALIAS)
934}
935
936/// The name rewritten source calls the runtime by. Deliberately unlikely to
937/// collide with an identifier a project already uses.
938pub const RUNTIME_ALIAS: &str = "__supercov";
939
940/// The package-level array every probe stores into. Declared once per package
941/// by a generated file, so a probe is an array index rather than a call.
942pub const HITS_VARIABLE: &str = "__supercovHits";
943
944/// The module path the rewritten source imports the runtime from.
945pub const RUNTIME_IMPORT: &str = "github.com/supercorp-ai/supercov/runtime/go/supercov";
946
947pub fn build_go_obligations_with_alias(
948    file: &str,
949    source: &str,
950    next_probe: &mut u64,
951    next_decision: &mut u32,
952    alias: &str,
953) -> Result<GoFileObligations, GoInstrumenterError> {
954    let tree = parse(source)?;
955    let decision_base = *next_decision;
956    let mut collector = Collector {
957        file,
958        source,
959        alias,
960        next_probe,
961        decision_base,
962        edits: Vec::new(),
963        points: Vec::new(),
964        branches: Vec::new(),
965        decisions: Vec::new(),
966        probes: BTreeMap::new(),
967        limitations: Vec::new(),
968        widths: Vec::new(),
969    };
970    let mut cursor = tree.root_node().walk();
971    for child in tree.root_node().children(&mut cursor) {
972        if child.is_named() {
973            collector.walk(child);
974        }
975    }
976    *next_decision += collector.widths.len() as u32;
977    let mut edits = collector.edits;
978    // The import is only needed by files that call the runtime — decisions and
979    // branches do, a file of plain statements does not, and Go rejects an
980    // unused import.
981    let calls_runtime = edits
982        .iter()
983        .any(|edit| edit.text.contains(&format!("{alias}.")));
984    if calls_runtime && let Some(import) = import_edit(source, alias, RUNTIME_IMPORT) {
985        edits.push(import);
986    }
987    Ok(GoFileObligations {
988        manifest: CoverageManifest {
989            decisions: collector.decisions,
990            points: collector.points,
991            branches: collector.branches,
992            limitations: collector.limitations,
993            unmeasured: Vec::new(),
994            scope: None,
995            assertion_sites: Vec::new(),
996        },
997        probes: collector.probes,
998        edits,
999        decision_widths: collector.widths,
1000    })
1001}
1002
1003#[cfg(test)]
1004mod tests {
1005    use super::*;
1006
1007    /// Every field the coverage index stores for a limitation.
1008    ///
1009    /// A limitation missing one of these is written into a run that then
1010    /// cannot be opened at all -- `invalid coverage index: coverage
1011    /// limitation`, with no coverage report and nothing naming the file that
1012    /// caused it. These were writing `detail` where the index reads `reason`,
1013    /// and none of them wrote `source`, so any run that measured a for-each
1014    /// loop was unreadable.
1015    fn assert_indexable(limitations: &[serde_json::Value]) -> Vec<String> {
1016        assert!(!limitations.is_empty(), "nothing to check");
1017        for limitation in limitations {
1018            for field in ["id", "kind", "file", "source", "reason"] {
1019                assert!(
1020                    limitation.get(field).and_then(|v| v.as_str()).is_some(),
1021                    "a limitation needs a string {field}: {limitation}"
1022                );
1023            }
1024            for field in ["line", "column"] {
1025                assert!(
1026                    limitation.get(field).and_then(|v| v.as_u64()).is_some(),
1027                    "a limitation needs a number {field}: {limitation}"
1028                );
1029            }
1030        }
1031        let mut kinds = limitations
1032            .iter()
1033            .filter_map(|limitation| limitation["kind"].as_str().map(str::to_owned))
1034            .collect::<Vec<_>>();
1035        kinds.sort();
1036        kinds.dedup();
1037        kinds
1038    }
1039
1040    /// A byte offset says a file is broken without saying where to look.
1041    #[test]
1042    fn a_file_that_does_not_parse_says_where() {
1043        let broken = "package main\n\nfunc f() int {\n\treturn 1 )\n}\n";
1044        let message = parse(broken).expect_err("does not parse").to_string();
1045        assert!(message.contains("line 4"), "{message}");
1046        assert!(message.contains("return 1"), "{message}");
1047        assert!(
1048            !message.contains("byte"),
1049            "an offset is not somewhere a reader can look: {message}"
1050        );
1051    }
1052
1053    #[test]
1054    fn every_limitation_carries_what_the_index_stores() {
1055        const EVERY: &str = r#"package main
1056
1057func walk(items []int) int {
1058	sum := 0
1059	for _, item := range items {
1060		sum += item
1061	}
1062	for {
1063		break
1064	}
1065	return sum
1066}
1067"#;
1068        let mut next = 0;
1069        let mut decisions = 0;
1070        let obligations =
1071            build_go_obligations("walk.go", EVERY, &mut next, &mut decisions).expect("go");
1072        assert_eq!(
1073            assert_indexable(&obligations.manifest.limitations),
1074            ["loop-without-condition"]
1075        );
1076    }
1077
1078    const SAMPLE: &str = r#"package main
1079
1080import "fmt"
1081
1082func classify(a int, b bool) string {
1083	if a > 10 && b {
1084		return "big"
1085	}
1086	for i := 0; i < a; i++ {
1087		fmt.Println(i)
1088	}
1089	switch {
1090	case a == 0:
1091		return "zero"
1092	default:
1093		return "small"
1094	}
1095}
1096"#;
1097
1098    fn obligations(source: &str) -> GoFileObligations {
1099        let mut next = 0;
1100        let mut decisions = 0;
1101        build_go_obligations("main.go", source, &mut next, &mut decisions).expect("obligations")
1102    }
1103
1104    /// Rewriting must never produce source Go cannot compile. Re-parsing the
1105    /// output catches that without a toolchain, on every machine, every run.
1106    fn rewritten(source: &str) -> String {
1107        let mut next = 0;
1108        let mut decisions = 0;
1109        let go =
1110            build_go_obligations("x.go", source, &mut next, &mut decisions).expect("obligations");
1111        let out = rewrite(source, &go.edits);
1112        parse(&out)
1113            .unwrap_or_else(|error| panic!("rewritten source does not parse: {error}\n{out}"));
1114        out
1115    }
1116
1117    #[test]
1118    fn what_follows_an_insertion_is_where_it_was_written() {
1119        // Go reports a compile error by line and column, and a probe earlier
1120        // on the line moved the column: `./crypto.go:4:38` for `4:15`.
1121        const SOURCE: &str =
1122            "package main\n\nfunc seal(key string) string {\n\treturn key + missing\n}\n";
1123        let mut next = 0;
1124        let mut decisions = 0;
1125        let obligations = build_go_obligations("seal.go", SOURCE, &mut next, &mut decisions)
1126            .expect("obligations");
1127        let rewritten = rewrite(SOURCE, &obligations.edits);
1128        // The statement on line 4 begins at column 2, after the tab.
1129        assert!(
1130            rewritten.contains("/*line :4:2*/return key + missing"),
1131            "{rewritten}"
1132        );
1133        assert_eq!(
1134            rewritten.lines().count(),
1135            SOURCE.lines().count(),
1136            "{rewritten}"
1137        );
1138    }
1139
1140    #[test]
1141    fn a_switch_given_a_default_keeps_its_lines() {
1142        // The `default` Supercov adds to a switch that has none took three
1143        // lines, so every line below the switch was three further down:
1144        // go-version's constraint.go grew from 307 lines to 310.
1145        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";
1146        let mut next = 0;
1147        let mut decisions = 0;
1148        let obligations = build_go_obligations("kind.go", SOURCE, &mut next, &mut decisions)
1149            .expect("obligations");
1150        let rewritten = rewrite(SOURCE, &obligations.edits);
1151        assert_eq!(
1152            rewritten.lines().count(),
1153            SOURCE.lines().count(),
1154            "{rewritten}"
1155        );
1156        assert_eq!(rewritten.matches("default: ").count(), 3, "{rewritten}");
1157        // One that follows a clause ends it; one in an empty switch has
1158        // nothing to end, and `;` alone is not a clause.
1159        assert_eq!(rewritten.matches("; default: ").count(), 2, "{rewritten}");
1160        parse(&rewritten.replace(RUNTIME_IMPORT, "example.com/kind/supercov")).expect("still Go");
1161    }
1162
1163    #[test]
1164    fn a_probe_never_lands_where_go_does_not_allow_a_statement() {
1165        // `for i := 0; c; i++` has two slots that hold a statement but are not
1166        // statement positions; a probe there makes a four-clause loop that does
1167        // not compile. Same for an `if` or `switch` initialiser.
1168        let out = rewritten(
1169            "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",
1170        );
1171        assert!(
1172            !out.contains("for __supercov"),
1173            "probe in a for-clause init:\n{out}"
1174        );
1175        assert!(
1176            !out.contains("; __supercov.P"),
1177            "probe in a for-clause post:\n{out}"
1178        );
1179        assert!(
1180            out.contains("if b := a;"),
1181            "the if initialiser survived intact:\n{out}"
1182        );
1183        assert!(
1184            out.contains("switch c := a;"),
1185            "the switch initialiser survived intact:\n{out}"
1186        );
1187    }
1188
1189    #[test]
1190    fn wrapping_a_condition_preserves_short_circuit_order() {
1191        // Go evaluates a call argument only when the call is reached, so a
1192        // wrapped right-hand operand runs exactly when the unwrapped one would
1193        // have. The branch wrapper must enclose the conditions, or the decision
1194        // would be closed before its operands had been observed.
1195        let out = rewritten(
1196            "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",
1197        );
1198        let condition = out
1199            .lines()
1200            .find(|line| line.contains("if "))
1201            .expect("the if survived");
1202        let branch = condition.find(".BD(").expect("branch and decision wrapper");
1203        let first = condition.find(".C(").expect("first condition wrapper");
1204        assert!(
1205            branch < first,
1206            "the branch must enclose its conditions: {condition}"
1207        );
1208        assert_eq!(
1209            condition.matches(".C(").count(),
1210            2,
1211            "one wrapper per condition: {condition}"
1212        );
1213        assert!(
1214            condition.contains("&&"),
1215            "the operator itself is untouched: {condition}"
1216        );
1217    }
1218
1219    #[test]
1220    fn a_single_condition_branch_uses_the_wrapper_that_inlines() {
1221        // Most branches in real code have one operand and no independence
1222        // obligation. They take the form with no decision argument, which is
1223        // the one small enough for the Go compiler to inline.
1224        let out = rewritten(
1225            "package main\nfunc f(a int) bool {\n\tif a > 10 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1226        );
1227        assert!(out.contains(".B("), "{out}");
1228        assert!(!out.contains(".BD("), "no decision here to close:\n{out}");
1229    }
1230
1231    #[test]
1232    fn a_switch_without_a_default_gains_one_so_matching_nothing_is_observable() {
1233        // The outcome exists whether or not the author wrote a clause for it,
1234        // and it cannot be seen without one.
1235        let out = rewritten(
1236            "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1237        );
1238        assert!(out.contains("default:"), "{out}");
1239
1240        // A switch that already has one is left alone.
1241        let existing = rewritten(
1242            "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\tdefault:\n\t\treturn\n\t}\n}\n",
1243        );
1244        assert_eq!(existing.matches("default:").count(), 1, "{existing}");
1245    }
1246
1247    #[test]
1248    fn a_loop_with_no_condition_records_a_limitation_not_an_obligation() {
1249        // A `range` loop and a bare `for {}` have no expression to observe.
1250        // Declaring a branch nothing can measure would put an obligation in the
1251        // denominator that no test could ever satisfy.
1252        let mut next = 0;
1253        let mut decisions = 0;
1254        let go = build_go_obligations(
1255            "x.go",
1256            "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}\n",
1257            &mut next,
1258            &mut decisions,
1259        )
1260        .unwrap();
1261        assert!(
1262            go.manifest.branches.iter().all(|b| b.kind != "loop"),
1263            "{:?}",
1264            go.manifest.branches
1265        );
1266        assert_eq!(
1267            go.manifest.limitations.len(),
1268            2,
1269            "{:?}",
1270            go.manifest.limitations
1271        );
1272        assert_eq!(go.manifest.limitations[0]["kind"], "loop-without-condition");
1273    }
1274
1275    #[test]
1276    fn only_a_file_that_gained_a_probe_imports_the_runtime() {
1277        // Go rejects an unused import, so a file with nothing to observe must
1278        // not get one.
1279        let out = rewritten("package main\nfunc f(a int) int {\n\treturn a\n}\n");
1280        assert!(
1281            out.contains(RUNTIME_ALIAS),
1282            "a function is itself an obligation:\n{out}"
1283        );
1284
1285        let mut next = 0;
1286        let mut decisions = 0;
1287        let bare = build_go_obligations(
1288            "t.go",
1289            "package main\n\ntype T struct{}\n",
1290            &mut next,
1291            &mut decisions,
1292        )
1293        .unwrap();
1294        assert!(bare.edits.is_empty(), "{:?}", bare.edits);
1295        assert_eq!(
1296            rewrite("package main\n\ntype T struct{}\n", &bare.edits),
1297            "package main\n\ntype T struct{}\n"
1298        );
1299    }
1300
1301    #[test]
1302    fn a_short_circuiting_operator_splits_a_decision_and_nothing_else_does() {
1303        // `&&` and `||` are the only operators that let one condition decide
1304        // the outcome without the other running, which is what MC/DC is about.
1305        // A comparison is one condition however many operands it reads.
1306        let go = obligations(SAMPLE);
1307        let decision = go
1308            .manifest
1309            .decisions
1310            .iter()
1311            .find(|d| d.kind == "if")
1312            .expect("the if carries a decision");
1313        assert_eq!(decision.conditions, ["a > 10", "b"]);
1314
1315        // A single condition needs no independence obligation: the branch
1316        // outcomes already say everything it could.
1317        let simple = obligations(
1318            "package main\nfunc f(a int) bool {\n\tif a > 1 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1319        );
1320        assert!(
1321            simple.manifest.decisions.is_empty(),
1322            "{:?}",
1323            simple.manifest.decisions
1324        );
1325    }
1326
1327    #[test]
1328    fn negation_and_parentheses_do_not_invent_conditions() {
1329        // `!b` is `b` negated, not a second condition, and a parenthesised
1330        // group is transparent. Counting either as extra would inflate the
1331        // MC/DC denominator with obligations no test can satisfy separately.
1332        let go = obligations(
1333            "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",
1334        );
1335        let decision = &go.manifest.decisions[0];
1336        assert_eq!(decision.conditions, ["a > 1", "!b", "c"]);
1337    }
1338
1339    #[test]
1340    fn every_branching_construct_records_the_outcome_that_was_not_taken() {
1341        // An `if` with no else, a loop that never runs, and a switch that
1342        // matches nothing are all outcomes a reader has to see. Recording only
1343        // the arm that executed would make an untested guard look exercised.
1344        let go = obligations(SAMPLE);
1345        let by_kind = |kind: &str| {
1346            go.manifest
1347                .branches
1348                .iter()
1349                .find(|b| b.kind == kind)
1350                .map(|b| {
1351                    b.alternatives
1352                        .iter()
1353                        .map(|a| a.label.clone())
1354                        .collect::<Vec<_>>()
1355                })
1356        };
1357        assert_eq!(by_kind("if").unwrap(), ["true", "false"]);
1358        // A loop's condition is true on every iteration and false when it
1359        // stops, so the honest labels are the condition's own outcomes rather
1360        // than "entered" and "skipped".
1361        assert_eq!(by_kind("loop").unwrap(), ["true", "false"]);
1362        let switch = by_kind("switch").unwrap();
1363        assert!(switch.contains(&"default".to_owned()), "{switch:?}");
1364        assert!(
1365            !switch.contains(&"no case matched".to_owned()),
1366            "a default already covers it"
1367        );
1368
1369        // Without a default, the fall-through outcome is named explicitly.
1370        let open = obligations(
1371            "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1372        );
1373        let labels = open
1374            .manifest
1375            .branches
1376            .iter()
1377            .find(|b| b.kind == "switch")
1378            .unwrap()
1379            .alternatives
1380            .iter()
1381            .map(|a| a.label.clone())
1382            .collect::<Vec<_>>();
1383        assert!(labels.contains(&"no case matched".to_owned()), "{labels:?}");
1384    }
1385
1386    #[test]
1387    fn functions_and_statements_are_separate_obligations_with_locations() {
1388        let go = obligations(SAMPLE);
1389        let functions = go
1390            .manifest
1391            .points
1392            .iter()
1393            .filter(|p| p.kind == PointKind::Function)
1394            .collect::<Vec<_>>();
1395        assert_eq!(functions.len(), 1);
1396        assert_eq!(functions[0].label.as_deref(), Some("classify"));
1397        assert_eq!(functions[0].line, 5);
1398
1399        let statements = go
1400            .manifest
1401            .points
1402            .iter()
1403            .filter(|p| p.kind == PointKind::Statement)
1404            .count();
1405        assert!(
1406            statements >= 6,
1407            "expected the body's statements, got {statements}"
1408        );
1409        // Every obligation is anchored; an unanchorable line is absent rather
1410        // than counted as uncovered.
1411        assert!(
1412            go.manifest
1413                .points
1414                .iter()
1415                .all(|p| p.line > 0 && p.column > 0)
1416        );
1417        // Imports and type declarations carry no coverage question.
1418        assert!(
1419            !go.manifest
1420                .points
1421                .iter()
1422                .any(|p| p.source.starts_with("import"))
1423        );
1424    }
1425
1426    #[test]
1427    fn every_obligation_has_a_probe_and_malformed_source_is_refused() {
1428        let go = obligations(SAMPLE);
1429        let expected = go.manifest.points.len()
1430            + go.manifest
1431                .branches
1432                .iter()
1433                .map(|b| b.alternatives.len())
1434                .sum::<usize>();
1435        // Points and branch alternatives carry probes. Conditions and outcomes
1436        // do not: their vector already says which ran and what the decision
1437        // came to, so a probe would be the same fact stored twice.
1438        assert_eq!(go.probes.len(), expected);
1439        // A decision is still an obligation, answered by the width the runtime
1440        // sizes its vector from rather than by a probe.
1441        assert_eq!(go.decision_widths.len(), go.manifest.decisions.len());
1442
1443        // Guessing at a file that does not parse would put obligations on lines
1444        // that may not exist.
1445        let mut next = 0;
1446        let mut decisions = 0;
1447        assert!(matches!(
1448            build_go_obligations(
1449                "broken.go",
1450                "package main\nfunc f( {",
1451                &mut next,
1452                &mut decisions
1453            ),
1454            Err(GoInstrumenterError::Parse(_))
1455        ));
1456    }
1457
1458    #[test]
1459    fn go_1_26_new_of_a_value_is_measured_rather_than_dropped() {
1460        // Go 1.26 let `new` take a value, not only a type: `new(x)` returns a
1461        // pointer to a copy of `x`. tree-sitter-go still models new and make
1462        // as a call whose one argument must be a type, so every file holding
1463        // one failed to parse -- and a file that does not parse carries no
1464        // obligations, so a package using the construct was measured as
1465        // though it held nothing. It appears 913 times in one codebase this
1466        // was reported from, and code generators emit it, so a project can
1467        // reintroduce it on every `go generate`.
1468        let go = obligations(
1469            "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",
1470        );
1471        assert_eq!(
1472            go.manifest
1473                .points
1474                .iter()
1475                .filter(|p| p.kind == PointKind::Function)
1476                .count(),
1477            3,
1478            "every function in the file is an obligation: {:?}",
1479            go.manifest.points
1480        );
1481        assert_eq!(
1482            go.manifest.branches.len(),
1483            1,
1484            "the `if` beside it is still a branch with both arms to answer for"
1485        );
1486        // And the obligation quotes the source the author wrote. The parse
1487        // reads a stand-in for the construct tree-sitter cannot express; what
1488        // the manifest records has to be the file itself, or the report shows
1489        // the reader a line that is not in their repository.
1490        assert!(
1491            go.manifest
1492                .points
1493                .iter()
1494                .any(|point| point.source.contains("new(\"hello\")")),
1495            "{:?}",
1496            go.manifest.points
1497        );
1498        assert!(
1499            !go.manifest
1500                .points
1501                .iter()
1502                .any(|point| point.source.contains("nEw")),
1503            "{:?}",
1504            go.manifest.points
1505        );
1506    }
1507
1508    #[test]
1509    fn new_of_a_type_is_left_exactly_as_it_was() {
1510        // Only the value form needs the stand-in. Every type form already
1511        // parses, and rewriting those would turn `new(map[string]int)` --
1512        // whose argument is a type and nothing else -- into something that
1513        // does not parse at all.
1514        let go = obligations(
1515            "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",
1516        );
1517        assert_eq!(
1518            go.manifest
1519                .points
1520                .iter()
1521                .filter(|p| p.kind == PointKind::Function)
1522                .count(),
1523            1
1524        );
1525        // The same file gaining one value form must not cost the type forms
1526        // their parse: the stand-in is chosen per call site, not per file.
1527        let mixed = obligations(
1528            "package p\n\nfunc Mixed(v int) {\n\t_ = new(map[string][]chan int)\n\t_ = new(v)\n\t_ = new(\"x\")\n}\n",
1529        );
1530        assert_eq!(
1531            mixed
1532                .manifest
1533                .points
1534                .iter()
1535                .filter(|p| p.kind == PointKind::Function)
1536                .count(),
1537            1
1538        );
1539    }
1540
1541    #[test]
1542    fn a_method_called_new_is_not_the_builtin() {
1543        // `new` is not a keyword, so `x.new(v)` is an ordinary method call and
1544        // always parsed. A stand-in written over it would be a rewrite of
1545        // somebody's own API for no reason -- and the file still has to parse
1546        // because of the builtin call beside it.
1547        let go = obligations(
1548            "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",
1549        );
1550        assert!(
1551            go.manifest
1552                .points
1553                .iter()
1554                .any(|point| point.source.contains("p.new(2)")),
1555            "{:?}",
1556            go.manifest.points
1557        );
1558    }
1559
1560    #[test]
1561    fn a_file_with_runes_outside_ascii_is_scanned_without_splitting_one() {
1562        // The scan that finds a `new(` call site walks bytes, and Go source is
1563        // UTF-8: a comment or a string with a rune in it puts multi-byte
1564        // sequences between the scanner and the call. Splitting one is not a
1565        // wrong answer, it is a panic -- Supercov would take down the suite it
1566        // was asked to measure, on a file whose only crime is a name with an
1567        // accent in it.
1568        let go = obligations(
1569            "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",
1570        );
1571        assert_eq!(
1572            go.manifest
1573                .points
1574                .iter()
1575                .filter(|p| p.kind == PointKind::Function)
1576                .count(),
1577            1
1578        );
1579    }
1580
1581    #[test]
1582    fn the_scan_never_mistakes_text_for_code() {
1583        // The scan that finds a `new(` call site has to know when Go source is
1584        // not Go: a `)` inside a string closes nothing, `// new(` is a remark,
1585        // and a backquoted string has no escapes at all. Reading any of them
1586        // as code finds a call site that is not there, and a stand-in written
1587        // over one is a rewrite of somebody's string.
1588        let go = obligations(concat!(
1589            "package p\n\n",
1590            "func f(v int) *int {\n",
1591            // Every one of these holds the text `new(` and none is a call.
1592            "\t_ = \"new(\\\"x\\\") and an unclosed ( paren\"\n",
1593            "\t_ = `raw new(\"y\") with ) and \\ backslash`\n",
1594            "\t_ = '('\n",
1595            "\t// new(\"comment\") and a stray )\n",
1596            "\t/* new(\"block\") ) */\n",
1597            // And this one is.
1598            "\treturn new(v + 1)\n}\n",
1599        ));
1600        assert_eq!(
1601            go.manifest
1602                .points
1603                .iter()
1604                .filter(|p| p.kind == PointKind::Function)
1605                .count(),
1606            1
1607        );
1608
1609        // The same again where the only `new(` is inside text, so the file
1610        // parses without the stand-in ever being spent.
1611        let untouched =
1612            obligations("package p\n\nfunc g() string {\n\treturn `new(\"only in text\")`\n}\n");
1613        assert!(
1614            untouched
1615                .manifest
1616                .points
1617                .iter()
1618                .any(|point| point.source.contains("new(\"only in text\")")),
1619            "{:?}",
1620            untouched.manifest.points
1621        );
1622    }
1623
1624    #[test]
1625    fn no_shape_of_input_can_make_the_scan_panic() {
1626        // The scan walks bytes, and every byte position is reachable: a file
1627        // can end inside a string, inside a comment, inside the argument list
1628        // of a `new(` that is never closed. A wrong answer there is a file
1629        // reported as unparseable; a panic is Supercov taking down the suite
1630        // it was asked to measure. So every truncation of a file holding one
1631        // of each is parsed, and the only claim made is that it returns.
1632        let awkward = concat!(
1633            "package p\n\n",
1634            "// gr\u{fc}\u{df}e \u{2014} new(\"in a comment\")\n",
1635            "func f(sch\u{f6}n int) *int {\n",
1636            "\t_ = \"\u{fc}ber new(\\\"in a string\\\"\"\n",
1637            "\t_ = `\u{e4}\u{f6}\u{fc} new(\n",
1638            "\t_ = '\u{fc}'\n",
1639            "\t/* new(\"unterminated\n",
1640            "\treturn new(sch\u{f6}n + 1)\n}\n",
1641        );
1642        for end in 0..=awkward.len() {
1643            // Only on a rune boundary: a `&str` cannot hold half a rune, so a
1644            // split elsewhere is not an input Supercov could ever be given.
1645            if !awkward.is_char_boundary(end) {
1646                continue;
1647            }
1648            let _ = parse(&awkward[..end]);
1649            // And the same read from the other end, so a construct that opens
1650            // before the window is covered too.
1651            if awkward.is_char_boundary(end) {
1652                let _ = parse(&awkward[end..]);
1653            }
1654        }
1655
1656        // A `new(` whose argument list never closes, and one at the very end
1657        // of a file, are the two positions the scan's own bounds depend on.
1658        for source in [
1659            "package p\n\nfunc f() {\n\t_ = new(\n",
1660            "package p\n\nfunc f() {\n\t_ = new",
1661            "package p\n\nfunc f() {\n\t_ = new(",
1662            "package p\n\nfunc f() {\n\t_ = new()\n}\n",
1663            "new",
1664            "new(",
1665            "",
1666        ] {
1667            let _ = parse(source);
1668        }
1669    }
1670
1671    #[test]
1672    fn the_scan_reads_new_the_way_go_spells_it() {
1673        // Every shape of the call site the scanner has to recognise or leave
1674        // alone, gathered because the measurement said so: the whitespace
1675        // Go permits between a callee and its arguments was written for and
1676        // never exercised, and neither was an identifier that merely ends in
1677        // the word.
1678        let go = obligations(concat!(
1679            "package p\n\n",
1680            "type pool struct{}\n\n",
1681            "func (p pool) new(v int) *int {\n\treturn &v\n}\n\n",
1682            "func renew(v string) *string {\n\treturn &v\n}\n\n",
1683            "func f(p pool, v int) *int {\n",
1684            // Go allows space, and a newline, between `new` and its argument.
1685            "\t_ = new (\"spaced\")\n",
1686            "\t_ = new\t(\"tabbed\")\n",
1687            // A method of somebody's own, which is not the builtin.
1688            "\t_ = p.new(2)\n",
1689            // An identifier that ends in the word, which is not it either.
1690            "\t_ = renew(\"x\")\n",
1691            // And an identifier that starts with it.
1692            "\tnewValue := v\n",
1693            "\treturn new(newValue)\n}\n",
1694        ));
1695        assert_eq!(
1696            go.manifest
1697                .points
1698                .iter()
1699                .filter(|p| p.kind == PointKind::Function)
1700                .count(),
1701            3,
1702            "{:?}",
1703            go.manifest.points
1704        );
1705        // What the author wrote is what the obligations quote, spacing and all.
1706        assert!(
1707            go.manifest
1708                .points
1709                .iter()
1710                .any(|point| point.source.contains("new (\"spaced\")")),
1711            "{:?}",
1712            go.manifest.points
1713        );
1714        assert!(
1715            !go.manifest
1716                .points
1717                .iter()
1718                .any(|point| point.source.contains("nEw")),
1719            "{:?}",
1720            go.manifest.points
1721        );
1722    }
1723
1724    #[test]
1725    fn a_file_that_is_simply_broken_still_does_not_parse() {
1726        // The stand-in must not become a way for any unparseable file to slip
1727        // through: what it cannot read is still a hole in the denominator, and
1728        // a hole reported as measured is a wrong number.
1729        let mut next = 0;
1730        let mut decisions = 0;
1731        assert!(matches!(
1732            build_go_obligations(
1733                "broken.go",
1734                "package p\n\nfunc f() {\n\t_ = new(1)\n\tif {\n}\n",
1735                &mut next,
1736                &mut decisions
1737            ),
1738            Err(GoInstrumenterError::Parse(_))
1739        ));
1740    }
1741
1742    #[test]
1743    fn decisions_are_numbered_across_the_module_not_within_a_file() {
1744        // The runtime holds one decision-state array for the whole module, so
1745        // an index meaning "the first decision in this file" would land on
1746        // every other file's first decision: two files would share condition
1747        // state, and the vectors both produced would describe neither.
1748        let mut next = 0;
1749        let mut decisions = 0;
1750        let first = build_go_obligations(
1751            "a.go",
1752            "package p\n\nfunc A(x, y bool) bool {\n\tif x && y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1753            &mut next,
1754            &mut decisions,
1755        )
1756        .unwrap();
1757        let second = build_go_obligations(
1758            "b.go",
1759            "package p\n\nfunc B(x, y bool) bool {\n\tif x || y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1760            &mut next,
1761            &mut decisions,
1762        )
1763        .unwrap();
1764
1765        let referenced = |obligations: &GoFileObligations| {
1766            obligations
1767                .edits
1768                .iter()
1769                .filter_map(|edit| {
1770                    let at = edit.text.find(".C(")?;
1771                    edit.text[at + 3..]
1772                        .split(',')
1773                        .next()?
1774                        .trim()
1775                        .parse::<u32>()
1776                        .ok()
1777                })
1778                .collect::<std::collections::BTreeSet<_>>()
1779        };
1780        assert_eq!(referenced(&first), [0].into());
1781        assert_eq!(referenced(&second), [1].into());
1782        assert_eq!(decisions, 2, "the module numbered two decisions in all");
1783        // And the widths each file reports stay in the order the ids assume,
1784        // so the concatenated table lines up with the concatenated manifest.
1785        assert_eq!(first.decision_widths, [2]);
1786        assert_eq!(second.decision_widths, [2]);
1787    }
1788}