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