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
108pub fn parse(source: &str) -> Result<tree_sitter::Tree, GoInstrumenterError> {
109    let mut parser = Parser::new();
110    parser
111        .set_language(&tree_sitter_go::LANGUAGE.into())
112        .map_err(|error| GoInstrumenterError::Parse(error.to_string()))?;
113    let tree = parser
114        .parse(source, None)
115        .ok_or_else(|| GoInstrumenterError::Parse("parser returned no tree".into()))?;
116    if tree.root_node().has_error() {
117        return Err(GoInstrumenterError::Parse(parse_failure(&tree, source)));
118    }
119    Ok(tree)
120}
121
122/// Where a parse went wrong, phrased so a reader knows where to look.
123///
124/// "source does not parse" for a nine-hundred-line file, or a byte offset into
125/// it, says only that something is wrong. tree-sitter recovers as it goes, so
126/// the outermost error node routinely spans a whole class while the token it
127/// actually choked on is deep inside: the innermost one is the one worth
128/// naming.
129pub(crate) fn parse_failure(tree: &tree_sitter::Tree, source: &str) -> String {
130    let mut deepest: Option<(usize, Node)> = None;
131    let mut stack = vec![(0_usize, tree.root_node())];
132    while let Some((depth, node)) = stack.pop() {
133        if (node.is_error() || node.is_missing()) && deepest.is_none_or(|(found, _)| depth > found)
134        {
135            deepest = Some((depth, node));
136        }
137        // Every child, not only those that report an error of their own: an
138        // ERROR node holds the tokens it did manage to read, and the one that
139        // actually failed is among them.
140        let mut cursor = node.walk();
141        for child in node.children(&mut cursor) {
142            stack.push((depth + 1, child));
143        }
144    }
145    let Some((_, node)) = deepest else {
146        return "source does not parse".to_owned();
147    };
148    let start = node.start_position();
149    let mut line = source
150        .lines()
151        .nth(start.row)
152        .unwrap_or_default()
153        .trim()
154        .to_owned();
155    if line.chars().count() > 120 {
156        line = line.chars().take(117).collect::<String>() + "...";
157    }
158    let what = if node.is_missing() {
159        "missing syntax"
160    } else {
161        "unexpected syntax"
162    };
163    // A recovered ERROR node can swallow everything from the construct it
164    // opened to the end of it, and the reader needs to know how far that is:
165    // "line 174" alone reads as a one-line problem when the parser gave up on
166    // four hundred.
167    let end = node.end_position().row + 1;
168    let through = if end > start.row + 1 {
169        format!(" (through line {end})")
170    } else {
171        String::new()
172    };
173    format!(
174        "{what} at line {}, column {}{through}: {line}",
175        start.row + 1,
176        start.column + 1,
177    )
178}
179
180/// Statements Go nests directly inside a block. Declarations that cannot
181/// execute -- an import, a type -- carry no coverage question and are absent
182/// rather than counted as uncovered.
183fn is_statement(kind: &str) -> bool {
184    matches!(
185        kind,
186        "assignment_statement"
187            | "break_statement"
188            | "const_declaration"
189            | "continue_statement"
190            | "dec_statement"
191            | "defer_statement"
192            | "expression_statement"
193            | "expression_switch_statement"
194            | "fallthrough_statement"
195            | "for_statement"
196            | "go_statement"
197            | "goto_statement"
198            | "if_statement"
199            | "inc_statement"
200            | "labeled_statement"
201            | "return_statement"
202            | "select_statement"
203            | "send_statement"
204            | "short_var_declaration"
205            | "type_switch_statement"
206            | "var_declaration"
207    )
208}
209
210struct Collector<'a> {
211    file: &'a str,
212    source: &'a str,
213    alias: &'a str,
214    next_probe: &'a mut u64,
215    edits: Vec<GoEdit>,
216    points: Vec<PointMeta>,
217    branches: Vec<BranchMeta>,
218    decisions: Vec<DecisionMeta>,
219    probes: BTreeMap<u64, GoProbe>,
220    limitations: Vec<serde_json::Value>,
221    widths: Vec<u8>,
222    /// How many decisions the module already numbered before this file. The
223    /// runtime holds one decision-state array for the whole module, so an
224    /// index that meant "the first decision in this file" would land on every
225    /// other file's first decision too.
226    decision_base: u32,
227}
228
229/// A branch whose condition is a single operand needs no independence
230/// obligation; the runtime's sentinel says so.
231/// The wrapper a branch needs. A single-operand condition has no independence
232/// obligation, so it gets the form with no decision argument — which is the
233/// one small enough for Go to inline, and the majority of branches in real
234/// code.
235fn branch_wrapper(alias: &str, when_true: u64, when_false: u64, decision: Option<usize>) -> String {
236    match decision {
237        Some(index) => format!("{alias}.BD({when_true}, {when_false}, {index}, "),
238        None => format!("{alias}.B({when_true}, {when_false}, "),
239    }
240}
241
242/// Whether a probe may be placed before this node.
243///
244/// Go has slots that hold a statement but are not statement positions: the
245/// init and post clauses of a `for`, and the initialiser of an `if` or a
246/// `switch`. A probe there turns `for i := 0; c; i++` into a four-clause loop
247/// that does not compile. Every real statement is a child of a `statement_list`,
248/// so that is the rule rather than a list of exceptions to remember.
249fn in_statement_position(node: Node) -> bool {
250    node.parent()
251        .is_some_and(|parent| parent.kind() == "statement_list")
252}
253
254/// The condition of a `for` loop, when it has one. A `range` clause and a bare
255/// `for {}` have none.
256fn loop_condition<'t>(node: Node<'t>) -> Option<Node<'t>> {
257    if let Some(condition) = node.child_by_field_name("condition") {
258        return Some(condition);
259    }
260    let mut cursor = node.walk();
261    let clause = node
262        .children(&mut cursor)
263        .find(|child| child.kind() == "for_clause")?;
264    let mut inner = clause.walk();
265    clause
266        .children(&mut inner)
267        .find(|child| child.is_named() && child.kind().ends_with("_expression"))
268}
269
270impl<'a> Collector<'a> {
271    fn id(&mut self, node: Node, kind: &str) -> String {
272        stable_obligation_id("go", self.file, kind, node.start_byte(), node.end_byte())
273    }
274
275    /// A limitation's identity. The contract requires one, and requires it to
276    /// be unique: a manifest whose limitations cannot be told apart cannot say
277    /// which surface each one is about, so the reader refuses the run rather
278    /// than present a list nobody can act on.
279    fn limitation_id(&mut self, node: Node, kind: &str) -> String {
280        self.id(node, kind)
281    }
282
283    fn probe(&mut self, target: GoProbeTarget, at: usize) -> u64 {
284        *self.next_probe += 1;
285        let id = *self.next_probe;
286        self.probes.insert(id, GoProbe { id, target, at });
287        id
288    }
289
290    fn edit(&mut self, at: usize, rank: i32, text: String) {
291        self.edits.push(GoEdit { at, rank, text });
292    }
293
294    /// Insert a statement-position call. Go allows `a(); b()` on one line, so a
295    /// probe never changes which line a statement reports as its own.
296    fn call_before(&mut self, at: usize, call: String) {
297        self.edit(at, 100, format!("{call}; "));
298    }
299
300    /// Line and column are one-based, matching every other frontend.
301    fn position(&self, node: Node) -> (usize, usize) {
302        let start = node.start_position();
303        (start.row + 1, start.column + 1)
304    }
305
306    fn text(&self, node: Node) -> String {
307        let raw = &self.source[node.byte_range()];
308        let line = raw.lines().next().unwrap_or("");
309        line.trim().to_owned()
310    }
311
312    fn add_point(&mut self, node: Node, kind: PointKind, label: Option<String>) {
313        let (line, column) = self.position(node);
314        let id = self.id(
315            node,
316            match kind {
317                PointKind::Function => "function",
318                PointKind::Statement => "statement",
319            },
320        );
321        let target = match kind {
322            PointKind::Function => GoProbeTarget::Function { id: id.clone() },
323            PointKind::Statement => GoProbeTarget::Statement { id: id.clone() },
324        };
325        // A function is observed just inside its body, because the declaration
326        // itself is not a place a statement may go.
327        let at = match kind {
328            PointKind::Function => match node.child_by_field_name("body") {
329                Some(body) => body.start_byte() + 1,
330                None => return,
331            },
332            PointKind::Statement => node.start_byte(),
333        };
334        let probe = self.probe(target, at);
335        // A direct store into the array this package already holds, not a call
336        // that would have to find it first. This is the shape Go's own cover
337        // tool and JaCoCo both settled on: one move instruction.
338        self.call_before(at, format!("{HITS_VARIABLE}[{probe}] = 2"));
339        self.points.push(PointMeta {
340            id,
341            kind,
342            file: self.file.to_owned(),
343            line,
344            column,
345            source: self.text(node),
346            label,
347        });
348    }
349
350    fn add_branch(&mut self, node: Node, kind: &str, labels: &[&str]) -> Vec<u64> {
351        let (line, column) = self.position(node);
352        let id = self.id(node, "branch");
353        let mut probes = Vec::new();
354        let alternatives = labels
355            .iter()
356            .map(|label| {
357                let alternative = format!("{id}.{label}");
358                probes.push(self.probe(
359                    GoProbeTarget::Alternative {
360                        branch: id.clone(),
361                        alternative: alternative.clone(),
362                    },
363                    node.start_byte(),
364                ));
365                BranchAlternativeMeta {
366                    id: alternative,
367                    label: (*label).to_owned(),
368                }
369            })
370            .collect();
371        self.branches.push(BranchMeta {
372            id,
373            kind: kind.to_owned(),
374            file: self.file.to_owned(),
375            line,
376            column,
377            source: self.text(node),
378            alternatives,
379        });
380        probes
381    }
382
383    /// Record a boolean expression as a decision when it has more than one
384    /// condition. A single condition needs no MC/DC obligation: its branch
385    /// outcomes already say everything independence could.
386    /// Returns the runtime's index for this decision, or `None` when the
387    /// expression has a single condition and needs no independence obligation.
388    fn add_decision(&mut self, node: Node, kind: &str) -> Option<usize> {
389        let mut leaves = Vec::new();
390        condition_nodes(node, self.source, &mut leaves);
391        if leaves.len() < 2 {
392            return None;
393        }
394        let conditions = leaves
395            .iter()
396            .map(|leaf| self.source[leaf.byte_range()].trim().to_owned())
397            .collect::<Vec<_>>();
398        let (line, column) = self.position(node);
399        let id = self.id(node, "decision");
400        // The runtime indexes decisions by position across the whole module,
401        // so the index a wrapper carries is this decision's place in the
402        // module's width table, not in this file's.
403        let index_of_decision = self.decision_base as usize + self.widths.len();
404        self.widths.push(leaves.len().min(64) as u8);
405        let alias = self.alias.to_owned();
406        // No probe per condition or outcome. The recorded vector already says
407        // which conditions were evaluated and what the decision came to, so a
408        // probe beside it would store the same fact twice and charge for it on
409        // every evaluation.
410        for (index, leaf) in leaves.iter().enumerate() {
411            // Wrapping an operand keeps short-circuiting intact: Go evaluates a
412            // call argument only when the call is reached, so the right-hand
413            // wrapper runs exactly when the unwrapped operand would have.
414            self.edit(
415                leaf.start_byte(),
416                20,
417                format!("{alias}.C({index_of_decision}, {index}, "),
418            );
419            self.edit(leaf.end_byte(), 20, ")".to_owned());
420        }
421        self.decisions.push(DecisionMeta {
422            id,
423            file: self.file.to_owned(),
424            line,
425            column,
426            source: self.text(node),
427            conditions,
428            kind: kind.to_owned(),
429        });
430        Some(index_of_decision)
431    }
432
433    fn walk(&mut self, node: Node) {
434        let alias = self.alias.to_owned();
435        match node.kind() {
436            "function_declaration" | "method_declaration" | "func_literal" => {
437                let label = node
438                    .child_by_field_name("name")
439                    .map(|name| self.source[name.byte_range()].to_owned());
440                self.add_point(node, PointKind::Function, label);
441            }
442            "if_statement" => {
443                if let Some(condition) = node.child_by_field_name("condition") {
444                    // An `if` without an else still has two outcomes: the body
445                    // ran, or control passed it by. Wrapping the condition
446                    // records the arm that was *not* taken by never setting its
447                    // bit, which is what makes an untested guard visible.
448                    let probes = self.add_branch(node, "if", &["true", "false"]);
449                    let decision = self.add_decision(condition, "if");
450                    self.edit(
451                        condition.start_byte(),
452                        5,
453                        branch_wrapper(&alias, probes[0], probes[1], decision),
454                    );
455                    self.edit(condition.end_byte(), 5, ")".to_owned());
456                }
457            }
458            "for_statement" => {
459                // A `for` with a condition branches on it. A `range` loop and a
460                // bare `for {}` have no expression to observe, so Supercov
461                // records the limitation rather than an obligation it cannot
462                // measure.
463                match loop_condition(node) {
464                    Some(condition) => {
465                        let probes = self.add_branch(node, "loop", &["true", "false"]);
466                        let decision = self.add_decision(condition, "loop");
467                        self.edit(
468                            condition.start_byte(),
469                            5,
470                            branch_wrapper(&alias, probes[0], probes[1], decision),
471                        );
472                        self.edit(condition.end_byte(), 5, ")".to_owned());
473                    }
474                    None => {
475                        let (line, column) = self.position(node);
476                        let limitation = self.limitation_id(node, "loop-without-condition");
477                        self.limitations.push(serde_json::json!({
478                            "id": limitation,
479                            "kind": "loop-without-condition",
480                            "file": self.file,
481                            "source": self.text(node),
482                            "line": line,
483                            "column": column,
484                            "reason": "a range or unconditional loop has no condition to observe, so no branch obligation is recorded for it",
485                        }));
486                    }
487                }
488            }
489            "expression_switch_statement" | "type_switch_statement" | "select_statement" => {
490                let kind = match node.kind() {
491                    "expression_switch_statement" => "switch",
492                    "type_switch_statement" => "type-switch",
493                    _ => "select",
494                };
495                let mut cases = Vec::new();
496                let mut has_default = false;
497                let mut cursor = node.walk();
498                for child in node.children(&mut cursor) {
499                    match child.kind() {
500                        "expression_case" | "type_case" | "communication_case" => {
501                            cases.push((self.text(child), Some(child)))
502                        }
503                        "default_case" => {
504                            has_default = true;
505                            cases.push(("default".to_owned(), Some(child)));
506                        }
507                        _ => {}
508                    }
509                }
510                // A switch with no default can match nothing, which is an
511                // outcome a reader has to see, and synthesising the clause is
512                // the only way to observe it: an added `default` holding
513                // nothing but a probe is the path the program already took.
514                //
515                // A select is not a switch. Without a default it *blocks*
516                // until one of its cases is ready; with one it returns
517                // immediately. So there is no "no case matched" outcome to
518                // record -- a select always selects -- and adding the clause
519                // to observe one would stop it blocking. That is not an
520                // instrument changing what is known about a program, it is an
521                // instrument changing what the program does: a loop that waited
522                // for values spins instead, and the code returns an empty
523                // buffer it reports as full.
524                if !has_default && node.kind() != "select_statement" {
525                    cases.push(("no case matched".to_owned(), None));
526                }
527                let labels = cases
528                    .iter()
529                    .map(|(label, _)| label.as_str())
530                    .collect::<Vec<_>>();
531                let probes = self.add_branch(node, kind, &labels);
532                for (probe, (_, clause)) in probes.iter().zip(cases.iter()) {
533                    match clause {
534                        Some(clause) => {
535                            let at = clause
536                                .children(&mut clause.walk())
537                                .find(|child| child.kind() == "statement_list")
538                                .map(|body| body.start_byte())
539                                .unwrap_or_else(|| clause.end_byte());
540                            self.call_before(at, format!("{alias}.A({probe})"));
541                        }
542                        None => {
543                            // Before the switch's closing brace.
544                            let at = node.end_byte().saturating_sub(1);
545                            self.edit(at, 100, format!("\ndefault:\n{alias}.A({probe})\n"));
546                        }
547                    }
548                }
549            }
550            kind if is_statement(kind) && in_statement_position(node) => {
551                self.add_point(node, PointKind::Statement, None);
552            }
553            _ => {}
554        }
555        // `if`, `for` and `switch` are statements too, and their own point is
556        // what says the construct was reached at all.
557        if in_statement_position(node)
558            && matches!(
559                node.kind(),
560                "if_statement"
561                    | "for_statement"
562                    | "expression_switch_statement"
563                    | "type_switch_statement"
564                    | "select_statement"
565            )
566        {
567            self.add_point(node, PointKind::Statement, None);
568        }
569        let mut cursor = node.walk();
570        for child in node.children(&mut cursor) {
571            if child.is_named() {
572                self.walk(child);
573            }
574        }
575    }
576}
577
578/// Flatten a boolean expression into its independent conditions.
579///
580/// `&&` and `||` are the only short-circuiting operators Go has, so they are
581/// the only ones that split a decision. `!` negates a condition rather than
582/// introducing one, and a parenthesised group is transparent.
583fn condition_nodes<'t>(node: Node<'t>, source: &str, out: &mut Vec<Node<'t>>) {
584    match node.kind() {
585        "binary_expression" => {
586            let operator = node
587                .child_by_field_name("operator")
588                .map(|op| &source[op.byte_range()])
589                .unwrap_or("");
590            if operator == "&&" || operator == "||" {
591                if let Some(left) = node.child_by_field_name("left") {
592                    condition_nodes(left, source, out);
593                }
594                if let Some(right) = node.child_by_field_name("right") {
595                    condition_nodes(right, source, out);
596                }
597                return;
598            }
599            out.push(node);
600        }
601        "parenthesized_expression" => {
602            let mut cursor = node.walk();
603            match node.children(&mut cursor).find(|child| child.is_named()) {
604                Some(inner) => condition_nodes(inner, source, out),
605                None => out.push(node),
606            }
607        }
608        _ => out.push(node),
609    }
610}
611
612/// One source edit. Every probe is an insertion at a byte offset; wrapping an
613/// expression is two of them, at its start and its end.
614///
615/// `rank` orders edits landing on the same offset. Applying right to left, the
616/// edit inserted last ends up leftmost, so an outer wrapper carries a lower
617/// rank than the inner one it encloses.
618#[derive(Debug, Clone, PartialEq, Eq)]
619pub struct GoEdit {
620    pub at: usize,
621    pub rank: i32,
622    pub text: String,
623}
624
625/// Apply edits to source, right to left so earlier offsets stay valid.
626pub fn rewrite(source: &str, edits: &[GoEdit]) -> String {
627    let mut ordered = edits.to_vec();
628    ordered.sort_by(|a, b| b.at.cmp(&a.at).then(b.rank.cmp(&a.rank)));
629    let mut out = source.to_owned();
630    for edit in ordered {
631        if edit.at > out.len() {
632            continue;
633        }
634        out.insert_str(edit.at, &edit.text);
635    }
636    out
637}
638
639/// The import rewritten source needs, placed straight after the package
640/// clause. Go rejects an unused import, so this is only added to a file that
641/// gained at least one probe.
642pub fn import_edit(source: &str, alias: &str, path: &str) -> Option<GoEdit> {
643    let tree = parse(source).ok()?;
644    let mut cursor = tree.root_node().walk();
645    let package = tree
646        .root_node()
647        .children(&mut cursor)
648        .find(|child| child.kind() == "package_clause")?;
649    Some(GoEdit {
650        at: package.end_byte(),
651        rank: 0,
652        text: format!("\nimport {alias} \"{path}\""),
653    })
654}
655
656pub fn build_go_obligations(
657    file: &str,
658    source: &str,
659    next_probe: &mut u64,
660    next_decision: &mut u32,
661) -> Result<GoFileObligations, GoInstrumenterError> {
662    build_go_obligations_with_alias(file, source, next_probe, next_decision, RUNTIME_ALIAS)
663}
664
665/// The name rewritten source calls the runtime by. Deliberately unlikely to
666/// collide with an identifier a project already uses.
667pub const RUNTIME_ALIAS: &str = "__supercov";
668
669/// The package-level array every probe stores into. Declared once per package
670/// by a generated file, so a probe is an array index rather than a call.
671pub const HITS_VARIABLE: &str = "__supercovHits";
672
673/// The module path the rewritten source imports the runtime from.
674pub const RUNTIME_IMPORT: &str = "github.com/supercorp-ai/supercov/runtime/go/supercov";
675
676pub fn build_go_obligations_with_alias(
677    file: &str,
678    source: &str,
679    next_probe: &mut u64,
680    next_decision: &mut u32,
681    alias: &str,
682) -> Result<GoFileObligations, GoInstrumenterError> {
683    let tree = parse(source)?;
684    let decision_base = *next_decision;
685    let mut collector = Collector {
686        file,
687        source,
688        alias,
689        next_probe,
690        decision_base,
691        edits: Vec::new(),
692        points: Vec::new(),
693        branches: Vec::new(),
694        decisions: Vec::new(),
695        probes: BTreeMap::new(),
696        limitations: Vec::new(),
697        widths: Vec::new(),
698    };
699    let mut cursor = tree.root_node().walk();
700    for child in tree.root_node().children(&mut cursor) {
701        if child.is_named() {
702            collector.walk(child);
703        }
704    }
705    *next_decision += collector.widths.len() as u32;
706    let mut edits = collector.edits;
707    // The import is only needed by files that call the runtime — decisions and
708    // branches do, a file of plain statements does not, and Go rejects an
709    // unused import.
710    let calls_runtime = edits
711        .iter()
712        .any(|edit| edit.text.contains(&format!("{alias}.")));
713    if calls_runtime && let Some(import) = import_edit(source, alias, RUNTIME_IMPORT) {
714        edits.push(import);
715    }
716    Ok(GoFileObligations {
717        manifest: CoverageManifest {
718            decisions: collector.decisions,
719            points: collector.points,
720            branches: collector.branches,
721            limitations: collector.limitations,
722            unmeasured: Vec::new(),
723            scope: None,
724        },
725        probes: collector.probes,
726        edits,
727        decision_widths: collector.widths,
728    })
729}
730
731#[cfg(test)]
732mod tests {
733    use super::*;
734
735    /// Every field the coverage index stores for a limitation.
736    ///
737    /// A limitation missing one of these is written into a run that then
738    /// cannot be opened at all -- `invalid coverage index: coverage
739    /// limitation`, with no coverage report and nothing naming the file that
740    /// caused it. These were writing `detail` where the index reads `reason`,
741    /// and none of them wrote `source`, so any run that measured a for-each
742    /// loop was unreadable.
743    fn assert_indexable(limitations: &[serde_json::Value]) -> Vec<String> {
744        assert!(!limitations.is_empty(), "nothing to check");
745        for limitation in limitations {
746            for field in ["id", "kind", "file", "source", "reason"] {
747                assert!(
748                    limitation.get(field).and_then(|v| v.as_str()).is_some(),
749                    "a limitation needs a string {field}: {limitation}"
750                );
751            }
752            for field in ["line", "column"] {
753                assert!(
754                    limitation.get(field).and_then(|v| v.as_u64()).is_some(),
755                    "a limitation needs a number {field}: {limitation}"
756                );
757            }
758        }
759        let mut kinds = limitations
760            .iter()
761            .filter_map(|limitation| limitation["kind"].as_str().map(str::to_owned))
762            .collect::<Vec<_>>();
763        kinds.sort();
764        kinds.dedup();
765        kinds
766    }
767
768    /// A byte offset says a file is broken without saying where to look.
769    #[test]
770    fn a_file_that_does_not_parse_says_where() {
771        let broken = "package main\n\nfunc f() int {\n\treturn 1 )\n}\n";
772        let message = parse(broken).expect_err("does not parse").to_string();
773        assert!(message.contains("line 4"), "{message}");
774        assert!(message.contains("return 1"), "{message}");
775        assert!(
776            !message.contains("byte"),
777            "an offset is not somewhere a reader can look: {message}"
778        );
779    }
780
781    #[test]
782    fn every_limitation_carries_what_the_index_stores() {
783        const EVERY: &str = r#"package main
784
785func walk(items []int) int {
786	sum := 0
787	for _, item := range items {
788		sum += item
789	}
790	for {
791		break
792	}
793	return sum
794}
795"#;
796        let mut next = 0;
797        let mut decisions = 0;
798        let obligations =
799            build_go_obligations("walk.go", EVERY, &mut next, &mut decisions).expect("go");
800        assert_eq!(
801            assert_indexable(&obligations.manifest.limitations),
802            ["loop-without-condition"]
803        );
804    }
805
806    const SAMPLE: &str = r#"package main
807
808import "fmt"
809
810func classify(a int, b bool) string {
811	if a > 10 && b {
812		return "big"
813	}
814	for i := 0; i < a; i++ {
815		fmt.Println(i)
816	}
817	switch {
818	case a == 0:
819		return "zero"
820	default:
821		return "small"
822	}
823}
824"#;
825
826    fn obligations(source: &str) -> GoFileObligations {
827        let mut next = 0;
828        let mut decisions = 0;
829        build_go_obligations("main.go", source, &mut next, &mut decisions).expect("obligations")
830    }
831
832    /// Rewriting must never produce source Go cannot compile. Re-parsing the
833    /// output catches that without a toolchain, on every machine, every run.
834    fn rewritten(source: &str) -> String {
835        let mut next = 0;
836        let mut decisions = 0;
837        let go =
838            build_go_obligations("x.go", source, &mut next, &mut decisions).expect("obligations");
839        let out = rewrite(source, &go.edits);
840        parse(&out)
841            .unwrap_or_else(|error| panic!("rewritten source does not parse: {error}\n{out}"));
842        out
843    }
844
845    #[test]
846    fn a_probe_never_lands_where_go_does_not_allow_a_statement() {
847        // `for i := 0; c; i++` has two slots that hold a statement but are not
848        // statement positions; a probe there makes a four-clause loop that does
849        // not compile. Same for an `if` or `switch` initialiser.
850        let out = rewritten(
851            "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",
852        );
853        assert!(
854            !out.contains("for __supercov"),
855            "probe in a for-clause init:\n{out}"
856        );
857        assert!(
858            !out.contains("; __supercov.P"),
859            "probe in a for-clause post:\n{out}"
860        );
861        assert!(
862            out.contains("if b := a;"),
863            "the if initialiser survived intact:\n{out}"
864        );
865        assert!(
866            out.contains("switch c := a;"),
867            "the switch initialiser survived intact:\n{out}"
868        );
869    }
870
871    #[test]
872    fn wrapping_a_condition_preserves_short_circuit_order() {
873        // Go evaluates a call argument only when the call is reached, so a
874        // wrapped right-hand operand runs exactly when the unwrapped one would
875        // have. The branch wrapper must enclose the conditions, or the decision
876        // would be closed before its operands had been observed.
877        let out = rewritten(
878            "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",
879        );
880        let condition = out
881            .lines()
882            .find(|line| line.contains("if "))
883            .expect("the if survived");
884        let branch = condition.find(".BD(").expect("branch and decision wrapper");
885        let first = condition.find(".C(").expect("first condition wrapper");
886        assert!(
887            branch < first,
888            "the branch must enclose its conditions: {condition}"
889        );
890        assert_eq!(
891            condition.matches(".C(").count(),
892            2,
893            "one wrapper per condition: {condition}"
894        );
895        assert!(
896            condition.contains("&&"),
897            "the operator itself is untouched: {condition}"
898        );
899    }
900
901    #[test]
902    fn a_single_condition_branch_uses_the_wrapper_that_inlines() {
903        // Most branches in real code have one operand and no independence
904        // obligation. They take the form with no decision argument, which is
905        // the one small enough for the Go compiler to inline.
906        let out = rewritten(
907            "package main\nfunc f(a int) bool {\n\tif a > 10 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
908        );
909        assert!(out.contains(".B("), "{out}");
910        assert!(!out.contains(".BD("), "no decision here to close:\n{out}");
911    }
912
913    #[test]
914    fn a_switch_without_a_default_gains_one_so_matching_nothing_is_observable() {
915        // The outcome exists whether or not the author wrote a clause for it,
916        // and it cannot be seen without one.
917        let out = rewritten(
918            "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
919        );
920        assert!(out.contains("default:"), "{out}");
921
922        // A switch that already has one is left alone.
923        let existing = rewritten(
924            "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\tdefault:\n\t\treturn\n\t}\n}\n",
925        );
926        assert_eq!(existing.matches("default:").count(), 1, "{existing}");
927    }
928
929    #[test]
930    fn a_loop_with_no_condition_records_a_limitation_not_an_obligation() {
931        // A `range` loop and a bare `for {}` have no expression to observe.
932        // Declaring a branch nothing can measure would put an obligation in the
933        // denominator that no test could ever satisfy.
934        let mut next = 0;
935        let mut decisions = 0;
936        let go = build_go_obligations(
937            "x.go",
938            "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",
939            &mut next,
940            &mut decisions,
941        )
942        .unwrap();
943        assert!(
944            go.manifest.branches.iter().all(|b| b.kind != "loop"),
945            "{:?}",
946            go.manifest.branches
947        );
948        assert_eq!(
949            go.manifest.limitations.len(),
950            2,
951            "{:?}",
952            go.manifest.limitations
953        );
954        assert_eq!(go.manifest.limitations[0]["kind"], "loop-without-condition");
955    }
956
957    #[test]
958    fn only_a_file_that_gained_a_probe_imports_the_runtime() {
959        // Go rejects an unused import, so a file with nothing to observe must
960        // not get one.
961        let out = rewritten("package main\nfunc f(a int) int {\n\treturn a\n}\n");
962        assert!(
963            out.contains(RUNTIME_ALIAS),
964            "a function is itself an obligation:\n{out}"
965        );
966
967        let mut next = 0;
968        let mut decisions = 0;
969        let bare = build_go_obligations(
970            "t.go",
971            "package main\n\ntype T struct{}\n",
972            &mut next,
973            &mut decisions,
974        )
975        .unwrap();
976        assert!(bare.edits.is_empty(), "{:?}", bare.edits);
977        assert_eq!(
978            rewrite("package main\n\ntype T struct{}\n", &bare.edits),
979            "package main\n\ntype T struct{}\n"
980        );
981    }
982
983    #[test]
984    fn a_short_circuiting_operator_splits_a_decision_and_nothing_else_does() {
985        // `&&` and `||` are the only operators that let one condition decide
986        // the outcome without the other running, which is what MC/DC is about.
987        // A comparison is one condition however many operands it reads.
988        let go = obligations(SAMPLE);
989        let decision = go
990            .manifest
991            .decisions
992            .iter()
993            .find(|d| d.kind == "if")
994            .expect("the if carries a decision");
995        assert_eq!(decision.conditions, ["a > 10", "b"]);
996
997        // A single condition needs no independence obligation: the branch
998        // outcomes already say everything it could.
999        let simple = obligations(
1000            "package main\nfunc f(a int) bool {\n\tif a > 1 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1001        );
1002        assert!(
1003            simple.manifest.decisions.is_empty(),
1004            "{:?}",
1005            simple.manifest.decisions
1006        );
1007    }
1008
1009    #[test]
1010    fn negation_and_parentheses_do_not_invent_conditions() {
1011        // `!b` is `b` negated, not a second condition, and a parenthesised
1012        // group is transparent. Counting either as extra would inflate the
1013        // MC/DC denominator with obligations no test can satisfy separately.
1014        let go = obligations(
1015            "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",
1016        );
1017        let decision = &go.manifest.decisions[0];
1018        assert_eq!(decision.conditions, ["a > 1", "!b", "c"]);
1019    }
1020
1021    #[test]
1022    fn every_branching_construct_records_the_outcome_that_was_not_taken() {
1023        // An `if` with no else, a loop that never runs, and a switch that
1024        // matches nothing are all outcomes a reader has to see. Recording only
1025        // the arm that executed would make an untested guard look exercised.
1026        let go = obligations(SAMPLE);
1027        let by_kind = |kind: &str| {
1028            go.manifest
1029                .branches
1030                .iter()
1031                .find(|b| b.kind == kind)
1032                .map(|b| {
1033                    b.alternatives
1034                        .iter()
1035                        .map(|a| a.label.clone())
1036                        .collect::<Vec<_>>()
1037                })
1038        };
1039        assert_eq!(by_kind("if").unwrap(), ["true", "false"]);
1040        // A loop's condition is true on every iteration and false when it
1041        // stops, so the honest labels are the condition's own outcomes rather
1042        // than "entered" and "skipped".
1043        assert_eq!(by_kind("loop").unwrap(), ["true", "false"]);
1044        let switch = by_kind("switch").unwrap();
1045        assert!(switch.contains(&"default".to_owned()), "{switch:?}");
1046        assert!(
1047            !switch.contains(&"no case matched".to_owned()),
1048            "a default already covers it"
1049        );
1050
1051        // Without a default, the fall-through outcome is named explicitly.
1052        let open = obligations(
1053            "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1054        );
1055        let labels = open
1056            .manifest
1057            .branches
1058            .iter()
1059            .find(|b| b.kind == "switch")
1060            .unwrap()
1061            .alternatives
1062            .iter()
1063            .map(|a| a.label.clone())
1064            .collect::<Vec<_>>();
1065        assert!(labels.contains(&"no case matched".to_owned()), "{labels:?}");
1066    }
1067
1068    #[test]
1069    fn functions_and_statements_are_separate_obligations_with_locations() {
1070        let go = obligations(SAMPLE);
1071        let functions = go
1072            .manifest
1073            .points
1074            .iter()
1075            .filter(|p| p.kind == PointKind::Function)
1076            .collect::<Vec<_>>();
1077        assert_eq!(functions.len(), 1);
1078        assert_eq!(functions[0].label.as_deref(), Some("classify"));
1079        assert_eq!(functions[0].line, 5);
1080
1081        let statements = go
1082            .manifest
1083            .points
1084            .iter()
1085            .filter(|p| p.kind == PointKind::Statement)
1086            .count();
1087        assert!(
1088            statements >= 6,
1089            "expected the body's statements, got {statements}"
1090        );
1091        // Every obligation is anchored; an unanchorable line is absent rather
1092        // than counted as uncovered.
1093        assert!(
1094            go.manifest
1095                .points
1096                .iter()
1097                .all(|p| p.line > 0 && p.column > 0)
1098        );
1099        // Imports and type declarations carry no coverage question.
1100        assert!(
1101            !go.manifest
1102                .points
1103                .iter()
1104                .any(|p| p.source.starts_with("import"))
1105        );
1106    }
1107
1108    #[test]
1109    fn every_obligation_has_a_probe_and_malformed_source_is_refused() {
1110        let go = obligations(SAMPLE);
1111        let expected = go.manifest.points.len()
1112            + go.manifest
1113                .branches
1114                .iter()
1115                .map(|b| b.alternatives.len())
1116                .sum::<usize>();
1117        // Points and branch alternatives carry probes. Conditions and outcomes
1118        // do not: their vector already says which ran and what the decision
1119        // came to, so a probe would be the same fact stored twice.
1120        assert_eq!(go.probes.len(), expected);
1121        // A decision is still an obligation, answered by the width the runtime
1122        // sizes its vector from rather than by a probe.
1123        assert_eq!(go.decision_widths.len(), go.manifest.decisions.len());
1124
1125        // Guessing at a file that does not parse would put obligations on lines
1126        // that may not exist.
1127        let mut next = 0;
1128        let mut decisions = 0;
1129        assert!(matches!(
1130            build_go_obligations(
1131                "broken.go",
1132                "package main\nfunc f( {",
1133                &mut next,
1134                &mut decisions
1135            ),
1136            Err(GoInstrumenterError::Parse(_))
1137        ));
1138    }
1139
1140    #[test]
1141    fn decisions_are_numbered_across_the_module_not_within_a_file() {
1142        // The runtime holds one decision-state array for the whole module, so
1143        // an index meaning "the first decision in this file" would land on
1144        // every other file's first decision: two files would share condition
1145        // state, and the vectors both produced would describe neither.
1146        let mut next = 0;
1147        let mut decisions = 0;
1148        let first = build_go_obligations(
1149            "a.go",
1150            "package p\n\nfunc A(x, y bool) bool {\n\tif x && y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1151            &mut next,
1152            &mut decisions,
1153        )
1154        .unwrap();
1155        let second = build_go_obligations(
1156            "b.go",
1157            "package p\n\nfunc B(x, y bool) bool {\n\tif x || y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1158            &mut next,
1159            &mut decisions,
1160        )
1161        .unwrap();
1162
1163        let referenced = |obligations: &GoFileObligations| {
1164            obligations
1165                .edits
1166                .iter()
1167                .filter_map(|edit| {
1168                    let at = edit.text.find(".C(")?;
1169                    edit.text[at + 3..]
1170                        .split(',')
1171                        .next()?
1172                        .trim()
1173                        .parse::<u32>()
1174                        .ok()
1175                })
1176                .collect::<std::collections::BTreeSet<_>>()
1177        };
1178        assert_eq!(referenced(&first), [0].into());
1179        assert_eq!(referenced(&second), [1].into());
1180        assert_eq!(decisions, 2, "the module numbered two decisions in all");
1181        // And the widths each file reports stay in the order the ids assume,
1182        // so the concatenated table lines up with the concatenated manifest.
1183        assert_eq!(first.decision_widths, [2]);
1184        assert_eq!(second.decision_widths, [2]);
1185    }
1186}