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.
783                            let at = node.end_byte().saturating_sub(1);
784                            self.edit(at, 100, format!("\ndefault:\n{alias}.A({probe})\n"));
785                        }
786                    }
787                }
788            }
789            kind if is_statement(kind) && in_statement_position(node) => {
790                self.add_point(node, PointKind::Statement, None);
791            }
792            _ => {}
793        }
794        // `if`, `for` and `switch` are statements too, and their own point is
795        // what says the construct was reached at all.
796        if in_statement_position(node)
797            && matches!(
798                node.kind(),
799                "if_statement"
800                    | "for_statement"
801                    | "expression_switch_statement"
802                    | "type_switch_statement"
803                    | "select_statement"
804            )
805        {
806            self.add_point(node, PointKind::Statement, None);
807        }
808        let mut cursor = node.walk();
809        for child in node.children(&mut cursor) {
810            if child.is_named() {
811                self.walk(child);
812            }
813        }
814    }
815}
816
817/// Flatten a boolean expression into its independent conditions.
818///
819/// `&&` and `||` are the only short-circuiting operators Go has, so they are
820/// the only ones that split a decision. `!` negates a condition rather than
821/// introducing one, and a parenthesised group is transparent.
822fn condition_nodes<'t>(node: Node<'t>, source: &str, out: &mut Vec<Node<'t>>) {
823    match node.kind() {
824        "binary_expression" => {
825            let operator = node
826                .child_by_field_name("operator")
827                .map(|op| &source[op.byte_range()])
828                .unwrap_or("");
829            if operator == "&&" || operator == "||" {
830                if let Some(left) = node.child_by_field_name("left") {
831                    condition_nodes(left, source, out);
832                }
833                if let Some(right) = node.child_by_field_name("right") {
834                    condition_nodes(right, source, out);
835                }
836                return;
837            }
838            out.push(node);
839        }
840        "parenthesized_expression" => {
841            let mut cursor = node.walk();
842            match node.children(&mut cursor).find(|child| child.is_named()) {
843                Some(inner) => condition_nodes(inner, source, out),
844                None => out.push(node),
845            }
846        }
847        _ => out.push(node),
848    }
849}
850
851/// One source edit. Every probe is an insertion at a byte offset; wrapping an
852/// expression is two of them, at its start and its end.
853///
854/// `rank` orders edits landing on the same offset. Applying right to left, the
855/// edit inserted last ends up leftmost, so an outer wrapper carries a lower
856/// rank than the inner one it encloses.
857#[derive(Debug, Clone, PartialEq, Eq)]
858pub struct GoEdit {
859    pub at: usize,
860    pub rank: i32,
861    pub text: String,
862}
863
864/// Apply edits to source, right to left so earlier offsets stay valid.
865pub fn rewrite(source: &str, edits: &[GoEdit]) -> String {
866    let mut ordered = edits.to_vec();
867    ordered.sort_by(|a, b| b.at.cmp(&a.at).then(b.rank.cmp(&a.rank)));
868    let mut out = source.to_owned();
869    for edit in ordered {
870        if edit.at > out.len() {
871            continue;
872        }
873        out.insert_str(edit.at, &edit.text);
874    }
875    out
876}
877
878/// The import rewritten source needs, placed straight after the package
879/// clause. Go rejects an unused import, so this is only added to a file that
880/// gained at least one probe.
881pub fn import_edit(source: &str, alias: &str, path: &str) -> Option<GoEdit> {
882    let tree = parse(source).ok()?;
883    let mut cursor = tree.root_node().walk();
884    let package = tree
885        .root_node()
886        .children(&mut cursor)
887        .find(|child| child.kind() == "package_clause")?;
888    Some(GoEdit {
889        at: package.end_byte(),
890        rank: 0,
891        text: format!("\nimport {alias} \"{path}\""),
892    })
893}
894
895pub fn build_go_obligations(
896    file: &str,
897    source: &str,
898    next_probe: &mut u64,
899    next_decision: &mut u32,
900) -> Result<GoFileObligations, GoInstrumenterError> {
901    build_go_obligations_with_alias(file, source, next_probe, next_decision, RUNTIME_ALIAS)
902}
903
904/// The name rewritten source calls the runtime by. Deliberately unlikely to
905/// collide with an identifier a project already uses.
906pub const RUNTIME_ALIAS: &str = "__supercov";
907
908/// The package-level array every probe stores into. Declared once per package
909/// by a generated file, so a probe is an array index rather than a call.
910pub const HITS_VARIABLE: &str = "__supercovHits";
911
912/// The module path the rewritten source imports the runtime from.
913pub const RUNTIME_IMPORT: &str = "github.com/supercorp-ai/supercov/runtime/go/supercov";
914
915pub fn build_go_obligations_with_alias(
916    file: &str,
917    source: &str,
918    next_probe: &mut u64,
919    next_decision: &mut u32,
920    alias: &str,
921) -> Result<GoFileObligations, GoInstrumenterError> {
922    let tree = parse(source)?;
923    let decision_base = *next_decision;
924    let mut collector = Collector {
925        file,
926        source,
927        alias,
928        next_probe,
929        decision_base,
930        edits: Vec::new(),
931        points: Vec::new(),
932        branches: Vec::new(),
933        decisions: Vec::new(),
934        probes: BTreeMap::new(),
935        limitations: Vec::new(),
936        widths: Vec::new(),
937    };
938    let mut cursor = tree.root_node().walk();
939    for child in tree.root_node().children(&mut cursor) {
940        if child.is_named() {
941            collector.walk(child);
942        }
943    }
944    *next_decision += collector.widths.len() as u32;
945    let mut edits = collector.edits;
946    // The import is only needed by files that call the runtime — decisions and
947    // branches do, a file of plain statements does not, and Go rejects an
948    // unused import.
949    let calls_runtime = edits
950        .iter()
951        .any(|edit| edit.text.contains(&format!("{alias}.")));
952    if calls_runtime && let Some(import) = import_edit(source, alias, RUNTIME_IMPORT) {
953        edits.push(import);
954    }
955    Ok(GoFileObligations {
956        manifest: CoverageManifest {
957            decisions: collector.decisions,
958            points: collector.points,
959            branches: collector.branches,
960            limitations: collector.limitations,
961            unmeasured: Vec::new(),
962            scope: None,
963            assertion_sites: Vec::new(),
964        },
965        probes: collector.probes,
966        edits,
967        decision_widths: collector.widths,
968    })
969}
970
971#[cfg(test)]
972mod tests {
973    use super::*;
974
975    /// Every field the coverage index stores for a limitation.
976    ///
977    /// A limitation missing one of these is written into a run that then
978    /// cannot be opened at all -- `invalid coverage index: coverage
979    /// limitation`, with no coverage report and nothing naming the file that
980    /// caused it. These were writing `detail` where the index reads `reason`,
981    /// and none of them wrote `source`, so any run that measured a for-each
982    /// loop was unreadable.
983    fn assert_indexable(limitations: &[serde_json::Value]) -> Vec<String> {
984        assert!(!limitations.is_empty(), "nothing to check");
985        for limitation in limitations {
986            for field in ["id", "kind", "file", "source", "reason"] {
987                assert!(
988                    limitation.get(field).and_then(|v| v.as_str()).is_some(),
989                    "a limitation needs a string {field}: {limitation}"
990                );
991            }
992            for field in ["line", "column"] {
993                assert!(
994                    limitation.get(field).and_then(|v| v.as_u64()).is_some(),
995                    "a limitation needs a number {field}: {limitation}"
996                );
997            }
998        }
999        let mut kinds = limitations
1000            .iter()
1001            .filter_map(|limitation| limitation["kind"].as_str().map(str::to_owned))
1002            .collect::<Vec<_>>();
1003        kinds.sort();
1004        kinds.dedup();
1005        kinds
1006    }
1007
1008    /// A byte offset says a file is broken without saying where to look.
1009    #[test]
1010    fn a_file_that_does_not_parse_says_where() {
1011        let broken = "package main\n\nfunc f() int {\n\treturn 1 )\n}\n";
1012        let message = parse(broken).expect_err("does not parse").to_string();
1013        assert!(message.contains("line 4"), "{message}");
1014        assert!(message.contains("return 1"), "{message}");
1015        assert!(
1016            !message.contains("byte"),
1017            "an offset is not somewhere a reader can look: {message}"
1018        );
1019    }
1020
1021    #[test]
1022    fn every_limitation_carries_what_the_index_stores() {
1023        const EVERY: &str = r#"package main
1024
1025func walk(items []int) int {
1026	sum := 0
1027	for _, item := range items {
1028		sum += item
1029	}
1030	for {
1031		break
1032	}
1033	return sum
1034}
1035"#;
1036        let mut next = 0;
1037        let mut decisions = 0;
1038        let obligations =
1039            build_go_obligations("walk.go", EVERY, &mut next, &mut decisions).expect("go");
1040        assert_eq!(
1041            assert_indexable(&obligations.manifest.limitations),
1042            ["loop-without-condition"]
1043        );
1044    }
1045
1046    const SAMPLE: &str = r#"package main
1047
1048import "fmt"
1049
1050func classify(a int, b bool) string {
1051	if a > 10 && b {
1052		return "big"
1053	}
1054	for i := 0; i < a; i++ {
1055		fmt.Println(i)
1056	}
1057	switch {
1058	case a == 0:
1059		return "zero"
1060	default:
1061		return "small"
1062	}
1063}
1064"#;
1065
1066    fn obligations(source: &str) -> GoFileObligations {
1067        let mut next = 0;
1068        let mut decisions = 0;
1069        build_go_obligations("main.go", source, &mut next, &mut decisions).expect("obligations")
1070    }
1071
1072    /// Rewriting must never produce source Go cannot compile. Re-parsing the
1073    /// output catches that without a toolchain, on every machine, every run.
1074    fn rewritten(source: &str) -> String {
1075        let mut next = 0;
1076        let mut decisions = 0;
1077        let go =
1078            build_go_obligations("x.go", source, &mut next, &mut decisions).expect("obligations");
1079        let out = rewrite(source, &go.edits);
1080        parse(&out)
1081            .unwrap_or_else(|error| panic!("rewritten source does not parse: {error}\n{out}"));
1082        out
1083    }
1084
1085    #[test]
1086    fn a_probe_never_lands_where_go_does_not_allow_a_statement() {
1087        // `for i := 0; c; i++` has two slots that hold a statement but are not
1088        // statement positions; a probe there makes a four-clause loop that does
1089        // not compile. Same for an `if` or `switch` initialiser.
1090        let out = rewritten(
1091            "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",
1092        );
1093        assert!(
1094            !out.contains("for __supercov"),
1095            "probe in a for-clause init:\n{out}"
1096        );
1097        assert!(
1098            !out.contains("; __supercov.P"),
1099            "probe in a for-clause post:\n{out}"
1100        );
1101        assert!(
1102            out.contains("if b := a;"),
1103            "the if initialiser survived intact:\n{out}"
1104        );
1105        assert!(
1106            out.contains("switch c := a;"),
1107            "the switch initialiser survived intact:\n{out}"
1108        );
1109    }
1110
1111    #[test]
1112    fn wrapping_a_condition_preserves_short_circuit_order() {
1113        // Go evaluates a call argument only when the call is reached, so a
1114        // wrapped right-hand operand runs exactly when the unwrapped one would
1115        // have. The branch wrapper must enclose the conditions, or the decision
1116        // would be closed before its operands had been observed.
1117        let out = rewritten(
1118            "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",
1119        );
1120        let condition = out
1121            .lines()
1122            .find(|line| line.contains("if "))
1123            .expect("the if survived");
1124        let branch = condition.find(".BD(").expect("branch and decision wrapper");
1125        let first = condition.find(".C(").expect("first condition wrapper");
1126        assert!(
1127            branch < first,
1128            "the branch must enclose its conditions: {condition}"
1129        );
1130        assert_eq!(
1131            condition.matches(".C(").count(),
1132            2,
1133            "one wrapper per condition: {condition}"
1134        );
1135        assert!(
1136            condition.contains("&&"),
1137            "the operator itself is untouched: {condition}"
1138        );
1139    }
1140
1141    #[test]
1142    fn a_single_condition_branch_uses_the_wrapper_that_inlines() {
1143        // Most branches in real code have one operand and no independence
1144        // obligation. They take the form with no decision argument, which is
1145        // the one small enough for the Go compiler to inline.
1146        let out = rewritten(
1147            "package main\nfunc f(a int) bool {\n\tif a > 10 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1148        );
1149        assert!(out.contains(".B("), "{out}");
1150        assert!(!out.contains(".BD("), "no decision here to close:\n{out}");
1151    }
1152
1153    #[test]
1154    fn a_switch_without_a_default_gains_one_so_matching_nothing_is_observable() {
1155        // The outcome exists whether or not the author wrote a clause for it,
1156        // and it cannot be seen without one.
1157        let out = rewritten(
1158            "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1159        );
1160        assert!(out.contains("default:"), "{out}");
1161
1162        // A switch that already has one is left alone.
1163        let existing = rewritten(
1164            "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\tdefault:\n\t\treturn\n\t}\n}\n",
1165        );
1166        assert_eq!(existing.matches("default:").count(), 1, "{existing}");
1167    }
1168
1169    #[test]
1170    fn a_loop_with_no_condition_records_a_limitation_not_an_obligation() {
1171        // A `range` loop and a bare `for {}` have no expression to observe.
1172        // Declaring a branch nothing can measure would put an obligation in the
1173        // denominator that no test could ever satisfy.
1174        let mut next = 0;
1175        let mut decisions = 0;
1176        let go = build_go_obligations(
1177            "x.go",
1178            "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",
1179            &mut next,
1180            &mut decisions,
1181        )
1182        .unwrap();
1183        assert!(
1184            go.manifest.branches.iter().all(|b| b.kind != "loop"),
1185            "{:?}",
1186            go.manifest.branches
1187        );
1188        assert_eq!(
1189            go.manifest.limitations.len(),
1190            2,
1191            "{:?}",
1192            go.manifest.limitations
1193        );
1194        assert_eq!(go.manifest.limitations[0]["kind"], "loop-without-condition");
1195    }
1196
1197    #[test]
1198    fn only_a_file_that_gained_a_probe_imports_the_runtime() {
1199        // Go rejects an unused import, so a file with nothing to observe must
1200        // not get one.
1201        let out = rewritten("package main\nfunc f(a int) int {\n\treturn a\n}\n");
1202        assert!(
1203            out.contains(RUNTIME_ALIAS),
1204            "a function is itself an obligation:\n{out}"
1205        );
1206
1207        let mut next = 0;
1208        let mut decisions = 0;
1209        let bare = build_go_obligations(
1210            "t.go",
1211            "package main\n\ntype T struct{}\n",
1212            &mut next,
1213            &mut decisions,
1214        )
1215        .unwrap();
1216        assert!(bare.edits.is_empty(), "{:?}", bare.edits);
1217        assert_eq!(
1218            rewrite("package main\n\ntype T struct{}\n", &bare.edits),
1219            "package main\n\ntype T struct{}\n"
1220        );
1221    }
1222
1223    #[test]
1224    fn a_short_circuiting_operator_splits_a_decision_and_nothing_else_does() {
1225        // `&&` and `||` are the only operators that let one condition decide
1226        // the outcome without the other running, which is what MC/DC is about.
1227        // A comparison is one condition however many operands it reads.
1228        let go = obligations(SAMPLE);
1229        let decision = go
1230            .manifest
1231            .decisions
1232            .iter()
1233            .find(|d| d.kind == "if")
1234            .expect("the if carries a decision");
1235        assert_eq!(decision.conditions, ["a > 10", "b"]);
1236
1237        // A single condition needs no independence obligation: the branch
1238        // outcomes already say everything it could.
1239        let simple = obligations(
1240            "package main\nfunc f(a int) bool {\n\tif a > 1 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1241        );
1242        assert!(
1243            simple.manifest.decisions.is_empty(),
1244            "{:?}",
1245            simple.manifest.decisions
1246        );
1247    }
1248
1249    #[test]
1250    fn negation_and_parentheses_do_not_invent_conditions() {
1251        // `!b` is `b` negated, not a second condition, and a parenthesised
1252        // group is transparent. Counting either as extra would inflate the
1253        // MC/DC denominator with obligations no test can satisfy separately.
1254        let go = obligations(
1255            "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",
1256        );
1257        let decision = &go.manifest.decisions[0];
1258        assert_eq!(decision.conditions, ["a > 1", "!b", "c"]);
1259    }
1260
1261    #[test]
1262    fn every_branching_construct_records_the_outcome_that_was_not_taken() {
1263        // An `if` with no else, a loop that never runs, and a switch that
1264        // matches nothing are all outcomes a reader has to see. Recording only
1265        // the arm that executed would make an untested guard look exercised.
1266        let go = obligations(SAMPLE);
1267        let by_kind = |kind: &str| {
1268            go.manifest
1269                .branches
1270                .iter()
1271                .find(|b| b.kind == kind)
1272                .map(|b| {
1273                    b.alternatives
1274                        .iter()
1275                        .map(|a| a.label.clone())
1276                        .collect::<Vec<_>>()
1277                })
1278        };
1279        assert_eq!(by_kind("if").unwrap(), ["true", "false"]);
1280        // A loop's condition is true on every iteration and false when it
1281        // stops, so the honest labels are the condition's own outcomes rather
1282        // than "entered" and "skipped".
1283        assert_eq!(by_kind("loop").unwrap(), ["true", "false"]);
1284        let switch = by_kind("switch").unwrap();
1285        assert!(switch.contains(&"default".to_owned()), "{switch:?}");
1286        assert!(
1287            !switch.contains(&"no case matched".to_owned()),
1288            "a default already covers it"
1289        );
1290
1291        // Without a default, the fall-through outcome is named explicitly.
1292        let open = obligations(
1293            "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1294        );
1295        let labels = open
1296            .manifest
1297            .branches
1298            .iter()
1299            .find(|b| b.kind == "switch")
1300            .unwrap()
1301            .alternatives
1302            .iter()
1303            .map(|a| a.label.clone())
1304            .collect::<Vec<_>>();
1305        assert!(labels.contains(&"no case matched".to_owned()), "{labels:?}");
1306    }
1307
1308    #[test]
1309    fn functions_and_statements_are_separate_obligations_with_locations() {
1310        let go = obligations(SAMPLE);
1311        let functions = go
1312            .manifest
1313            .points
1314            .iter()
1315            .filter(|p| p.kind == PointKind::Function)
1316            .collect::<Vec<_>>();
1317        assert_eq!(functions.len(), 1);
1318        assert_eq!(functions[0].label.as_deref(), Some("classify"));
1319        assert_eq!(functions[0].line, 5);
1320
1321        let statements = go
1322            .manifest
1323            .points
1324            .iter()
1325            .filter(|p| p.kind == PointKind::Statement)
1326            .count();
1327        assert!(
1328            statements >= 6,
1329            "expected the body's statements, got {statements}"
1330        );
1331        // Every obligation is anchored; an unanchorable line is absent rather
1332        // than counted as uncovered.
1333        assert!(
1334            go.manifest
1335                .points
1336                .iter()
1337                .all(|p| p.line > 0 && p.column > 0)
1338        );
1339        // Imports and type declarations carry no coverage question.
1340        assert!(
1341            !go.manifest
1342                .points
1343                .iter()
1344                .any(|p| p.source.starts_with("import"))
1345        );
1346    }
1347
1348    #[test]
1349    fn every_obligation_has_a_probe_and_malformed_source_is_refused() {
1350        let go = obligations(SAMPLE);
1351        let expected = go.manifest.points.len()
1352            + go.manifest
1353                .branches
1354                .iter()
1355                .map(|b| b.alternatives.len())
1356                .sum::<usize>();
1357        // Points and branch alternatives carry probes. Conditions and outcomes
1358        // do not: their vector already says which ran and what the decision
1359        // came to, so a probe would be the same fact stored twice.
1360        assert_eq!(go.probes.len(), expected);
1361        // A decision is still an obligation, answered by the width the runtime
1362        // sizes its vector from rather than by a probe.
1363        assert_eq!(go.decision_widths.len(), go.manifest.decisions.len());
1364
1365        // Guessing at a file that does not parse would put obligations on lines
1366        // that may not exist.
1367        let mut next = 0;
1368        let mut decisions = 0;
1369        assert!(matches!(
1370            build_go_obligations(
1371                "broken.go",
1372                "package main\nfunc f( {",
1373                &mut next,
1374                &mut decisions
1375            ),
1376            Err(GoInstrumenterError::Parse(_))
1377        ));
1378    }
1379
1380    #[test]
1381    fn go_1_26_new_of_a_value_is_measured_rather_than_dropped() {
1382        // Go 1.26 let `new` take a value, not only a type: `new(x)` returns a
1383        // pointer to a copy of `x`. tree-sitter-go still models new and make
1384        // as a call whose one argument must be a type, so every file holding
1385        // one failed to parse -- and a file that does not parse carries no
1386        // obligations, so a package using the construct was measured as
1387        // though it held nothing. It appears 913 times in one codebase this
1388        // was reported from, and code generators emit it, so a project can
1389        // reintroduce it on every `go generate`.
1390        let go = obligations(
1391            "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",
1392        );
1393        assert_eq!(
1394            go.manifest
1395                .points
1396                .iter()
1397                .filter(|p| p.kind == PointKind::Function)
1398                .count(),
1399            3,
1400            "every function in the file is an obligation: {:?}",
1401            go.manifest.points
1402        );
1403        assert_eq!(
1404            go.manifest.branches.len(),
1405            1,
1406            "the `if` beside it is still a branch with both arms to answer for"
1407        );
1408        // And the obligation quotes the source the author wrote. The parse
1409        // reads a stand-in for the construct tree-sitter cannot express; what
1410        // the manifest records has to be the file itself, or the report shows
1411        // the reader a line that is not in their repository.
1412        assert!(
1413            go.manifest
1414                .points
1415                .iter()
1416                .any(|point| point.source.contains("new(\"hello\")")),
1417            "{:?}",
1418            go.manifest.points
1419        );
1420        assert!(
1421            !go.manifest
1422                .points
1423                .iter()
1424                .any(|point| point.source.contains("nEw")),
1425            "{:?}",
1426            go.manifest.points
1427        );
1428    }
1429
1430    #[test]
1431    fn new_of_a_type_is_left_exactly_as_it_was() {
1432        // Only the value form needs the stand-in. Every type form already
1433        // parses, and rewriting those would turn `new(map[string]int)` --
1434        // whose argument is a type and nothing else -- into something that
1435        // does not parse at all.
1436        let go = obligations(
1437            "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",
1438        );
1439        assert_eq!(
1440            go.manifest
1441                .points
1442                .iter()
1443                .filter(|p| p.kind == PointKind::Function)
1444                .count(),
1445            1
1446        );
1447        // The same file gaining one value form must not cost the type forms
1448        // their parse: the stand-in is chosen per call site, not per file.
1449        let mixed = obligations(
1450            "package p\n\nfunc Mixed(v int) {\n\t_ = new(map[string][]chan int)\n\t_ = new(v)\n\t_ = new(\"x\")\n}\n",
1451        );
1452        assert_eq!(
1453            mixed
1454                .manifest
1455                .points
1456                .iter()
1457                .filter(|p| p.kind == PointKind::Function)
1458                .count(),
1459            1
1460        );
1461    }
1462
1463    #[test]
1464    fn a_method_called_new_is_not_the_builtin() {
1465        // `new` is not a keyword, so `x.new(v)` is an ordinary method call and
1466        // always parsed. A stand-in written over it would be a rewrite of
1467        // somebody's own API for no reason -- and the file still has to parse
1468        // because of the builtin call beside it.
1469        let go = obligations(
1470            "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",
1471        );
1472        assert!(
1473            go.manifest
1474                .points
1475                .iter()
1476                .any(|point| point.source.contains("p.new(2)")),
1477            "{:?}",
1478            go.manifest.points
1479        );
1480    }
1481
1482    #[test]
1483    fn a_file_with_runes_outside_ascii_is_scanned_without_splitting_one() {
1484        // The scan that finds a `new(` call site walks bytes, and Go source is
1485        // UTF-8: a comment or a string with a rune in it puts multi-byte
1486        // sequences between the scanner and the call. Splitting one is not a
1487        // wrong answer, it is a panic -- Supercov would take down the suite it
1488        // was asked to measure, on a file whose only crime is a name with an
1489        // accent in it.
1490        let go = obligations(
1491            "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",
1492        );
1493        assert_eq!(
1494            go.manifest
1495                .points
1496                .iter()
1497                .filter(|p| p.kind == PointKind::Function)
1498                .count(),
1499            1
1500        );
1501    }
1502
1503    #[test]
1504    fn the_scan_never_mistakes_text_for_code() {
1505        // The scan that finds a `new(` call site has to know when Go source is
1506        // not Go: a `)` inside a string closes nothing, `// new(` is a remark,
1507        // and a backquoted string has no escapes at all. Reading any of them
1508        // as code finds a call site that is not there, and a stand-in written
1509        // over one is a rewrite of somebody's string.
1510        let go = obligations(concat!(
1511            "package p\n\n",
1512            "func f(v int) *int {\n",
1513            // Every one of these holds the text `new(` and none is a call.
1514            "\t_ = \"new(\\\"x\\\") and an unclosed ( paren\"\n",
1515            "\t_ = `raw new(\"y\") with ) and \\ backslash`\n",
1516            "\t_ = '('\n",
1517            "\t// new(\"comment\") and a stray )\n",
1518            "\t/* new(\"block\") ) */\n",
1519            // And this one is.
1520            "\treturn new(v + 1)\n}\n",
1521        ));
1522        assert_eq!(
1523            go.manifest
1524                .points
1525                .iter()
1526                .filter(|p| p.kind == PointKind::Function)
1527                .count(),
1528            1
1529        );
1530
1531        // The same again where the only `new(` is inside text, so the file
1532        // parses without the stand-in ever being spent.
1533        let untouched =
1534            obligations("package p\n\nfunc g() string {\n\treturn `new(\"only in text\")`\n}\n");
1535        assert!(
1536            untouched
1537                .manifest
1538                .points
1539                .iter()
1540                .any(|point| point.source.contains("new(\"only in text\")")),
1541            "{:?}",
1542            untouched.manifest.points
1543        );
1544    }
1545
1546    #[test]
1547    fn no_shape_of_input_can_make_the_scan_panic() {
1548        // The scan walks bytes, and every byte position is reachable: a file
1549        // can end inside a string, inside a comment, inside the argument list
1550        // of a `new(` that is never closed. A wrong answer there is a file
1551        // reported as unparseable; a panic is Supercov taking down the suite
1552        // it was asked to measure. So every truncation of a file holding one
1553        // of each is parsed, and the only claim made is that it returns.
1554        let awkward = concat!(
1555            "package p\n\n",
1556            "// gr\u{fc}\u{df}e \u{2014} new(\"in a comment\")\n",
1557            "func f(sch\u{f6}n int) *int {\n",
1558            "\t_ = \"\u{fc}ber new(\\\"in a string\\\"\"\n",
1559            "\t_ = `\u{e4}\u{f6}\u{fc} new(\n",
1560            "\t_ = '\u{fc}'\n",
1561            "\t/* new(\"unterminated\n",
1562            "\treturn new(sch\u{f6}n + 1)\n}\n",
1563        );
1564        for end in 0..=awkward.len() {
1565            // Only on a rune boundary: a `&str` cannot hold half a rune, so a
1566            // split elsewhere is not an input Supercov could ever be given.
1567            if !awkward.is_char_boundary(end) {
1568                continue;
1569            }
1570            let _ = parse(&awkward[..end]);
1571            // And the same read from the other end, so a construct that opens
1572            // before the window is covered too.
1573            if awkward.is_char_boundary(end) {
1574                let _ = parse(&awkward[end..]);
1575            }
1576        }
1577
1578        // A `new(` whose argument list never closes, and one at the very end
1579        // of a file, are the two positions the scan's own bounds depend on.
1580        for source in [
1581            "package p\n\nfunc f() {\n\t_ = new(\n",
1582            "package p\n\nfunc f() {\n\t_ = new",
1583            "package p\n\nfunc f() {\n\t_ = new(",
1584            "package p\n\nfunc f() {\n\t_ = new()\n}\n",
1585            "new",
1586            "new(",
1587            "",
1588        ] {
1589            let _ = parse(source);
1590        }
1591    }
1592
1593    #[test]
1594    fn the_scan_reads_new_the_way_go_spells_it() {
1595        // Every shape of the call site the scanner has to recognise or leave
1596        // alone, gathered because the measurement said so: the whitespace
1597        // Go permits between a callee and its arguments was written for and
1598        // never exercised, and neither was an identifier that merely ends in
1599        // the word.
1600        let go = obligations(concat!(
1601            "package p\n\n",
1602            "type pool struct{}\n\n",
1603            "func (p pool) new(v int) *int {\n\treturn &v\n}\n\n",
1604            "func renew(v string) *string {\n\treturn &v\n}\n\n",
1605            "func f(p pool, v int) *int {\n",
1606            // Go allows space, and a newline, between `new` and its argument.
1607            "\t_ = new (\"spaced\")\n",
1608            "\t_ = new\t(\"tabbed\")\n",
1609            // A method of somebody's own, which is not the builtin.
1610            "\t_ = p.new(2)\n",
1611            // An identifier that ends in the word, which is not it either.
1612            "\t_ = renew(\"x\")\n",
1613            // And an identifier that starts with it.
1614            "\tnewValue := v\n",
1615            "\treturn new(newValue)\n}\n",
1616        ));
1617        assert_eq!(
1618            go.manifest
1619                .points
1620                .iter()
1621                .filter(|p| p.kind == PointKind::Function)
1622                .count(),
1623            3,
1624            "{:?}",
1625            go.manifest.points
1626        );
1627        // What the author wrote is what the obligations quote, spacing and all.
1628        assert!(
1629            go.manifest
1630                .points
1631                .iter()
1632                .any(|point| point.source.contains("new (\"spaced\")")),
1633            "{:?}",
1634            go.manifest.points
1635        );
1636        assert!(
1637            !go.manifest
1638                .points
1639                .iter()
1640                .any(|point| point.source.contains("nEw")),
1641            "{:?}",
1642            go.manifest.points
1643        );
1644    }
1645
1646    #[test]
1647    fn a_file_that_is_simply_broken_still_does_not_parse() {
1648        // The stand-in must not become a way for any unparseable file to slip
1649        // through: what it cannot read is still a hole in the denominator, and
1650        // a hole reported as measured is a wrong number.
1651        let mut next = 0;
1652        let mut decisions = 0;
1653        assert!(matches!(
1654            build_go_obligations(
1655                "broken.go",
1656                "package p\n\nfunc f() {\n\t_ = new(1)\n\tif {\n}\n",
1657                &mut next,
1658                &mut decisions
1659            ),
1660            Err(GoInstrumenterError::Parse(_))
1661        ));
1662    }
1663
1664    #[test]
1665    fn decisions_are_numbered_across_the_module_not_within_a_file() {
1666        // The runtime holds one decision-state array for the whole module, so
1667        // an index meaning "the first decision in this file" would land on
1668        // every other file's first decision: two files would share condition
1669        // state, and the vectors both produced would describe neither.
1670        let mut next = 0;
1671        let mut decisions = 0;
1672        let first = build_go_obligations(
1673            "a.go",
1674            "package p\n\nfunc A(x, y bool) bool {\n\tif x && y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1675            &mut next,
1676            &mut decisions,
1677        )
1678        .unwrap();
1679        let second = build_go_obligations(
1680            "b.go",
1681            "package p\n\nfunc B(x, y bool) bool {\n\tif x || y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1682            &mut next,
1683            &mut decisions,
1684        )
1685        .unwrap();
1686
1687        let referenced = |obligations: &GoFileObligations| {
1688            obligations
1689                .edits
1690                .iter()
1691                .filter_map(|edit| {
1692                    let at = edit.text.find(".C(")?;
1693                    edit.text[at + 3..]
1694                        .split(',')
1695                        .next()?
1696                        .trim()
1697                        .parse::<u32>()
1698                        .ok()
1699                })
1700                .collect::<std::collections::BTreeSet<_>>()
1701        };
1702        assert_eq!(referenced(&first), [0].into());
1703        assert_eq!(referenced(&second), [1].into());
1704        assert_eq!(decisions, 2, "the module numbered two decisions in all");
1705        // And the widths each file reports stay in the order the ids assume,
1706        // so the concatenated table lines up with the concatenated manifest.
1707        assert_eq!(first.decision_widths, [2]);
1708        assert_eq!(second.decision_widths, [2]);
1709    }
1710}