Skip to main content

supercov_engine/
jvm_instrumenter.rs

1//! Supercov-owned Java and Kotlin parsing and obligation discovery.
2//!
3//! JaCoCo instruments bytecode; Supercov instruments source, for the same
4//! reason it does everywhere else. Bytecode branches are the compiler's
5//! branches, and a condition the compiler folded away is one no test can be
6//! asked about. The denominator has to be the source the author wrote.
7//!
8//! Java and Kotlin share this module because they share a runtime and most of
9//! a model. Where they differ they differ sharply, and each difference is
10//! named at the point it matters rather than hidden behind a trait.
11
12use std::collections::BTreeMap;
13
14use tree_sitter::{Node, Parser};
15
16use crate::coverage_analysis::PointKind;
17use crate::coverage_report::{
18    BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
19};
20use crate::go_instrumenter::{GoEdit, GoProbe, GoProbeTarget};
21
22/// The runtime class instrumented source calls.
23pub const RUNTIME_CLASS: &str = "com.supercorp.supercov.Supercov";
24/// The array probes store into, qualified so no import is needed.
25pub const HITS: &str = "com.supercorp.supercov.Supercov.HITS";
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
28pub enum JvmLanguage {
29    Java,
30    Kotlin,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum JvmInstrumenterError {
35    Parse(String),
36}
37
38impl std::fmt::Display for JvmInstrumenterError {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match self {
41            JvmInstrumenterError::Parse(detail) => write!(f, "JVM parse error: {detail}"),
42        }
43    }
44}
45
46pub fn parse(
47    source: &str,
48    language: JvmLanguage,
49) -> Result<tree_sitter::Tree, JvmInstrumenterError> {
50    let mut parser = Parser::new();
51    let grammar = match language {
52        JvmLanguage::Java => tree_sitter_java::LANGUAGE.into(),
53        JvmLanguage::Kotlin => tree_sitter_kotlin_ng::LANGUAGE.into(),
54    };
55    parser
56        .set_language(&grammar)
57        .map_err(|error| JvmInstrumenterError::Parse(error.to_string()))?;
58    let tree = parser
59        .parse(source, None)
60        .ok_or_else(|| JvmInstrumenterError::Parse("parser returned no tree".into()))?;
61    if tree.root_node().has_error() {
62        return Err(JvmInstrumenterError::Parse(
63            crate::go_instrumenter::parse_failure(&tree, source),
64        ));
65    }
66    Ok(tree)
67}
68
69#[derive(Debug, Clone, PartialEq)]
70pub struct JvmFileObligations {
71    pub manifest: CoverageManifest,
72    pub probes: BTreeMap<u64, GoProbe>,
73    pub edits: Vec<GoEdit>,
74    pub decision_widths: Vec<u8>,
75}
76
77/// Statements Java nests directly inside a block. A declaration that cannot
78/// execute carries no coverage question and is absent rather than uncovered.
79fn is_java_statement(kind: &str) -> bool {
80    matches!(
81        kind,
82        "assert_statement"
83            | "break_statement"
84            | "continue_statement"
85            | "do_statement"
86            | "enhanced_for_statement"
87            | "expression_statement"
88            | "for_statement"
89            | "if_statement"
90            | "labeled_statement"
91            | "local_variable_declaration"
92            | "return_statement"
93            | "switch_expression"
94            | "synchronized_statement"
95            | "throw_statement"
96            | "try_statement"
97            | "try_with_resources_statement"
98            | "while_statement"
99            | "yield_statement"
100    )
101}
102
103/// The kinds tree-sitter's Kotlin grammar actually produces in statement
104/// position, which are not the ones the language's own vocabulary suggests.
105///
106/// Everything in Kotlin is an expression, so a `return` is a
107/// `return_expression` and a `y--` is a `unary_expression`. `break` and
108/// `continue` are stranger still: the grammar gives them no kind of their own
109/// and reports them as identifiers, so they are recognised by their text.
110/// Matching identifiers in general would put a probe before every bare name.
111fn is_kotlin_statement(node: Node, source: &str) -> bool {
112    match node.kind() {
113        "assignment"
114        | "call_expression"
115        | "do_while_statement"
116        | "for_statement"
117        | "if_expression"
118        | "property_declaration"
119        | "return_expression"
120        | "throw_expression"
121        | "try_expression"
122        | "unary_expression"
123        | "when_expression"
124        | "while_statement" => true,
125        "identifier" => matches!(source[node.byte_range()].trim(), "break" | "continue"),
126        _ => false,
127    }
128}
129
130struct Collector<'a> {
131    file: &'a str,
132    source: &'a str,
133    language: JvmLanguage,
134    next_probe: &'a mut u64,
135    edits: Vec<GoEdit>,
136    points: Vec<PointMeta>,
137    branches: Vec<BranchMeta>,
138    decisions: Vec<DecisionMeta>,
139    probes: BTreeMap<u64, GoProbe>,
140    limitations: Vec<serde_json::Value>,
141    widths: Vec<u8>,
142    /// How many decisions the project already numbered before this file. The
143    /// runtime holds one decision-state array for the whole run, so an index
144    /// that meant "the first decision in this file" would land on every other
145    /// file's first decision too.
146    decision_base: u32,
147}
148
149impl Collector<'_> {
150    fn id(&mut self, node: Node, kind: &str) -> String {
151        let language = match self.language {
152            JvmLanguage::Java => "java",
153            JvmLanguage::Kotlin => "kotlin",
154        };
155        crate::go_instrumenter::stable_obligation_id(
156            language,
157            self.file,
158            kind,
159            node.start_byte(),
160            node.end_byte(),
161        )
162    }
163
164    /// A limitation's identity. The contract requires one, and requires it to
165    /// be unique: a manifest whose limitations cannot be told apart cannot say
166    /// which surface each one is about, so the reader refuses the run rather
167    /// than present a list nobody can act on.
168    fn limitation_id(&mut self, node: Node, kind: &str) -> String {
169        self.id(node, kind)
170    }
171
172    fn probe(&mut self, target: GoProbeTarget, at: usize) -> u64 {
173        *self.next_probe += 1;
174        let id = *self.next_probe;
175        self.probes.insert(id, GoProbe { id, target, at });
176        id
177    }
178
179    fn edit(&mut self, at: usize, rank: i32, text: String) {
180        self.edits.push(GoEdit { at, rank, text });
181    }
182
183    fn position(&self, node: Node) -> (usize, usize) {
184        let start = node.start_position();
185        (start.row + 1, start.column + 1)
186    }
187
188    fn text(&self, node: Node) -> String {
189        self.source[node.byte_range()]
190            .lines()
191            .next()
192            .unwrap_or("")
193            .trim()
194            .to_owned()
195    }
196
197    /// A probe in statement position. Java needs the semicolon; Kotlin's
198    /// newline-terminated statements do not mind one either way, and writing
199    /// it keeps a probe and the statement it precedes on one line so neither
200    /// moves the other's reported position.
201    fn store(&mut self, at: usize, probe: u64) {
202        self.edit(at, 100, format!("{HITS}[{probe}] = 2; "));
203    }
204
205    fn add_point(&mut self, node: Node, kind: PointKind, label: Option<String>) {
206        let (line, column) = self.position(node);
207        let id = self.id(
208            node,
209            match kind {
210                PointKind::Function => "function",
211                PointKind::Statement => "statement",
212            },
213        );
214        let target = match kind {
215            PointKind::Function => GoProbeTarget::Function { id: id.clone() },
216            PointKind::Statement => GoProbeTarget::Statement { id: id.clone() },
217        };
218        // A contract has to stay the first statement, so the probe that
219        // records the function being entered goes after it instead of before.
220        let mut after_contract = false;
221        let at = match kind {
222            PointKind::Function => match body_block(node, self.language) {
223                Some(body) => match opening_contract(body, self.source, self.language) {
224                    Some(contract) => {
225                        after_contract = true;
226                        contract.end_byte()
227                    }
228                    None => body.start_byte() + 1,
229                },
230                // An expression-bodied Kotlin function has no block to open.
231                // Its expression is still measured; the function itself simply
232                // has nowhere to record being entered.
233                None => return,
234            },
235            PointKind::Statement => node.start_byte(),
236        };
237        let probe = self.probe(target, at);
238        if after_contract {
239            // Kotlin separates statements by newline, and this one follows the
240            // contract on its own line, so it needs the semicolon written.
241            self.edit(at, 100, format!("; {HITS}[{probe}] = 2;"));
242        } else {
243            self.store(at, probe);
244        }
245        self.points.push(PointMeta {
246            id,
247            kind,
248            file: self.file.to_owned(),
249            line,
250            column,
251            source: self.text(node),
252            label,
253        });
254    }
255
256    fn add_branch(&mut self, node: Node, kind: &str, labels: &[&str]) -> Vec<u64> {
257        let (line, column) = self.position(node);
258        let id = self.id(node, "branch");
259        let mut probes = Vec::new();
260        let alternatives = labels
261            .iter()
262            .map(|label| {
263                let alternative = format!("{id}.{label}");
264                probes.push(self.probe(
265                    GoProbeTarget::Alternative {
266                        branch: id.clone(),
267                        alternative: alternative.clone(),
268                    },
269                    node.start_byte(),
270                ));
271                BranchAlternativeMeta {
272                    id: alternative,
273                    label: (*label).to_owned(),
274                }
275            })
276            .collect();
277        self.branches.push(BranchMeta {
278            id,
279            kind: kind.to_owned(),
280            file: self.file.to_owned(),
281            line,
282            column,
283            source: self.text(node),
284            alternatives,
285        });
286        probes
287    }
288
289    fn add_decision(&mut self, node: Node, kind: &str) -> Option<usize> {
290        let mut leaves = Vec::new();
291        condition_nodes(node, self.source, &mut leaves);
292        if leaves.len() < 2 {
293            return None;
294        }
295        let conditions = leaves
296            .iter()
297            .map(|leaf| self.source[leaf.byte_range()].trim().to_owned())
298            .collect::<Vec<_>>();
299        let (line, column) = self.position(node);
300        let id = self.id(node, "decision");
301        let index = self.decision_base as usize + self.widths.len();
302        self.widths.push(leaves.len().min(64) as u8);
303        for (position, leaf) in leaves.iter().enumerate() {
304            // Java and Kotlin both evaluate an argument only when the call is
305            // reached, so a wrapped right-hand operand runs exactly when the
306            // unwrapped one would have.
307            self.edit(
308                leaf.start_byte(),
309                20,
310                format!("{RUNTIME_CLASS}.c({index}, {position}, "),
311            );
312            self.edit(leaf.end_byte(), 20, ")".to_owned());
313        }
314        self.decisions.push(DecisionMeta {
315            id,
316            file: self.file.to_owned(),
317            line,
318            column,
319            source: self.text(node),
320            conditions,
321            kind: kind.to_owned(),
322        });
323        Some(index)
324    }
325
326    /// Give a branch arm somewhere to record itself.
327    ///
328    /// `if (x) return;` has no block, so a probe before the statement would
329    /// leave it outside the `if` entirely: it would run unconditionally while
330    /// the return stayed guarded, and the report would claim the arm was
331    /// taken. Braces are the only honest fix.
332    ///
333    /// The arm also needs its own point. Walking never reaches it, because a
334    /// statement is recognised by its parent being a block and this one's
335    /// parent is the branch.
336    /// Record which way a branch went from inside its arms, for a condition
337    /// that cannot be wrapped.
338    ///
339    /// An `if` with no `else` has nowhere to record being false, so one is
340    /// added holding nothing but the probe. An empty else changes no
341    /// behaviour: it is the branch the program already took.
342    fn record_arms(&mut self, node: Node, language: JvmLanguage, probes: &[u64]) {
343        let (consequence, alternative) = arms(node, language);
344        for (arm, probe) in [consequence, alternative].into_iter().zip(probes) {
345            match arm {
346                // `ensure_block` has already braced an unbraced arm, and a
347                // store ranked above that brace lands inside it.
348                Some(arm) if arm.kind() == "block" => self.store(arm.start_byte() + 1, *probe),
349                Some(arm) => self.store(arm.start_byte(), *probe),
350                // Ranked above the brace `ensure_block` may have added at
351                // this same offset. Edits at one offset are applied highest
352                // rank first and each pushes the last to the right, so a
353                // lower rank here would put the `else` inside the braces
354                // rather than after them -- which is what an unbraced arm on
355                // one line produced, and it is not Kotlin.
356                None => self.edit(
357                    node.end_byte(),
358                    70,
359                    format!(" else {{ {HITS}[{probe}] = 2; }}"),
360                ),
361            }
362        }
363    }
364
365    fn ensure_block(&mut self, node: Node) {
366        if node.kind() == "block" {
367            return;
368        }
369        self.edit(node.start_byte(), 60, "{ ".to_owned());
370        self.edit(node.end_byte(), 60, " }".to_owned());
371        if is_statement(node, self.source, self.language) {
372            // Ranked above the brace so that applying right to left leaves the
373            // brace outermost and the probe within it.
374            self.add_point(node, PointKind::Statement, None);
375        }
376    }
377}
378
379/// The block a function's statements live in, if it has one.
380///
381/// Java names the field; Kotlin's grammar does not. It puts an unnamed
382/// `function_body` between the declaration and the block, so asking for the
383/// `body` field there answers nothing and every Kotlin function goes
384/// unmeasured — silently, because a function with no block is a real thing in
385/// Kotlin and the caller treats the absence as one.
386fn body_block<'t>(node: Node<'t>, language: JvmLanguage) -> Option<Node<'t>> {
387    let body = node.child_by_field_name("body").or_else(|| {
388        let mut cursor = node.walk();
389        node.children(&mut cursor)
390            .find(|child| matches!(child.kind(), "function_body" | "block"))
391    })?;
392    match language {
393        JvmLanguage::Java => (body.kind() == "block").then_some(body),
394        JvmLanguage::Kotlin => {
395            if body.kind() == "block" {
396                return Some(body);
397            }
398            let mut cursor = body.walk();
399            body.children(&mut cursor)
400                .find(|child| child.kind() == "block")
401        }
402    }
403}
404
405/// Flatten a boolean expression into its independent conditions.
406///
407/// `&&` and `||` are the only short-circuiting operators either language has,
408/// so they are the only ones that split a decision. `!` negates a condition
409/// rather than introducing one, and parentheses are transparent. Java's
410/// non-short-circuiting `&` and `|` deliberately do not split: both operands
411/// always run, so neither can independently affect the outcome in the sense
412/// MC/DC means.
413fn condition_nodes<'t>(node: Node<'t>, source: &str, out: &mut Vec<Node<'t>>) {
414    match node.kind() {
415        "binary_expression" => {
416            let operator = node
417                .child_by_field_name("operator")
418                .map(|op| &source[op.byte_range()])
419                .unwrap_or("");
420            if operator == "&&" || operator == "||" {
421                if let Some(left) = node.child_by_field_name("left") {
422                    condition_nodes(left, source, out);
423                }
424                if let Some(right) = node.child_by_field_name("right") {
425                    condition_nodes(right, source, out);
426                }
427                return;
428            }
429            out.push(node);
430        }
431        "parenthesized_expression" => {
432            let mut cursor = node.walk();
433            match node.children(&mut cursor).find(|child| child.is_named()) {
434                Some(inner) => condition_nodes(inner, source, out),
435                None => out.push(node),
436            }
437        }
438        _ => out.push(node),
439    }
440}
441
442/// The `contract { ... }` a Kotlin function body may open with.
443///
444/// Kotlin requires a contract to be the *first* statement of its function --
445/// "Contract should be the first statement" is an error, not a warning -- so a
446/// probe written at the top of the body stops the function compiling, and
447/// moshi's `knownNotNull` is exactly that shape. The function probe goes after
448/// the contract instead, which records the same event: a contract block is
449/// erased before bytecode and cannot throw, so reaching it and reaching the
450/// statement after it cannot come apart. For the same reason the contract
451/// takes no statement obligation of its own -- it is a declaration the
452/// compiler reads, not code that runs.
453fn opening_contract<'tree>(
454    body: Node<'tree>,
455    source: &str,
456    language: JvmLanguage,
457) -> Option<Node<'tree>> {
458    if language != JvmLanguage::Kotlin {
459        return None;
460    }
461    let mut cursor = body.walk();
462    let first = body.children(&mut cursor).find(|child| child.is_named())?;
463    if first.kind() != "call_expression" {
464        return None;
465    }
466    let callee = first.child(0)?;
467    (source[callee.byte_range()].trim() == "contract").then_some(first)
468}
469
470/// Whether this node *is* the contract its block opens with.
471fn is_opening_contract(node: Node, source: &str, language: JvmLanguage) -> bool {
472    node.parent()
473        .and_then(|parent| opening_contract(parent, source, language))
474        .is_some_and(|contract| contract.id() == node.id())
475}
476
477/// Whether the compiler has to see this condition to compile the code around
478/// it.
479///
480/// Some conditions are not only values: the compiler reads them and narrows a
481/// type in the branch that follows. Java's pattern `instanceof` binds a name
482/// whose scope is decided by flow analysis; Kotlin's `is` and its null
483/// comparisons produce smart casts. Wrapping such a condition in a call leaves
484/// an ordinary boolean expression, the narrowing never happens, and the code
485/// after it stops compiling -- `cannot find symbol: variable s`, `unresolved
486/// reference on receiver of type Any?`.
487///
488/// This is not a corner. Pattern `instanceof` is how Java has been written
489/// since 16, and `x != null` guards a great deal of Kotlin.
490fn narrows_a_type(node: Node, source: &str, language: JvmLanguage) -> bool {
491    let narrows = match language {
492        // A binding gives the pattern a name to scope; without one the
493        // condition is an ordinary value. Java spells a binding two ways: a
494        // trailing name (`o instanceof String s`) and a record pattern, which
495        // deconstructs into names of its own (`o instanceof R(int a)`) and
496        // carries no name field at all.
497        JvmLanguage::Java => {
498            node.kind() == "instanceof_expression"
499                && (node.child_by_field_name("name").is_some() || {
500                    let mut cursor = node.walk();
501                    node.children(&mut cursor)
502                        .any(|child| child.kind().ends_with("_pattern"))
503                })
504        }
505        JvmLanguage::Kotlin => {
506            node.kind() == "is_expression"
507                || (node.kind() == "binary_expression"
508                    && matches!(
509                        node.child_by_field_name("operator")
510                            .map(|operator| source[operator.byte_range()].trim())
511                            .unwrap_or_default(),
512                        "==" | "!="
513                    )
514                    && ["left", "right"].iter().any(|side| {
515                        node.child_by_field_name(side)
516                            .is_some_and(|side| source[side.byte_range()].trim() == "null")
517                    }))
518        }
519    };
520    if narrows {
521        return true;
522    }
523    let mut cursor = node.walk();
524    node.children(&mut cursor)
525        .filter(Node::is_named)
526        .any(|child| narrows_a_type(child, source, language))
527}
528
529/// An `if`'s two arms.
530///
531/// Java names them; Kotlin's grammar does not, so there they are the named
532/// children either side of the `else` keyword. Asking Kotlin for a field it
533/// has no name for answers nothing, which silently left its unbraced arms
534/// unmeasured and, worse, made a branch recorded from its arms write two
535/// `else` blocks onto one `if`.
536fn arms<'t>(node: Node<'t>, language: JvmLanguage) -> (Option<Node<'t>>, Option<Node<'t>>) {
537    match language {
538        JvmLanguage::Java => (
539            node.child_by_field_name("consequence"),
540            node.child_by_field_name("alternative"),
541        ),
542        JvmLanguage::Kotlin => {
543            let condition = node
544                .child_by_field_name("condition")
545                .map(|c| c.byte_range());
546            let mut cursor = node.walk();
547            let children = node.children(&mut cursor).collect::<Vec<_>>();
548            let otherwise = children.iter().position(|child| child.kind() == "else");
549            let arm = |child: &&Node<'t>| child.is_named() && Some(child.byte_range()) != condition;
550            (
551                children
552                    .iter()
553                    .take(otherwise.unwrap_or(children.len()))
554                    .find(arm)
555                    .copied(),
556                otherwise.and_then(|at| children.iter().skip(at + 1).find(arm).copied()),
557            )
558        }
559    }
560}
561
562/// The expression inside `if (...)`. Java parenthesises its condition; Kotlin
563/// does not.
564fn condition_of<'t>(node: Node<'t>, language: JvmLanguage) -> Option<Node<'t>> {
565    let condition = node.child_by_field_name("condition")?;
566    if language == JvmLanguage::Java && condition.kind() == "parenthesized_expression" {
567        let mut cursor = condition.walk();
568        return condition
569            .children(&mut cursor)
570            .find(|child| child.is_named());
571    }
572    Some(condition)
573}
574
575pub fn build_jvm_obligations(
576    file: &str,
577    source: &str,
578    language: JvmLanguage,
579    next_probe: &mut u64,
580    next_decision: &mut u32,
581) -> Result<JvmFileObligations, JvmInstrumenterError> {
582    let tree = parse(source, language)?;
583    let decision_base = *next_decision;
584    let mut collector = Collector {
585        file,
586        source,
587        language,
588        next_probe,
589        decision_base,
590        edits: Vec::new(),
591        points: Vec::new(),
592        branches: Vec::new(),
593        decisions: Vec::new(),
594        probes: BTreeMap::new(),
595        limitations: Vec::new(),
596        widths: Vec::new(),
597    };
598    walk(&mut collector, tree.root_node());
599    *next_decision += collector.widths.len() as u32;
600    Ok(JvmFileObligations {
601        manifest: CoverageManifest {
602            decisions: collector.decisions,
603            points: collector.points,
604            branches: collector.branches,
605            limitations: collector.limitations,
606            unmeasured: Vec::new(),
607            scope: None,
608        },
609        probes: collector.probes,
610        edits: collector.edits,
611        decision_widths: collector.widths,
612    })
613}
614
615fn walk(collector: &mut Collector, node: Node) {
616    let language = collector.language;
617    match node.kind() {
618        "method_declaration" | "constructor_declaration" | "function_declaration" => {
619            let label = node
620                .child_by_field_name("name")
621                .map(|name| collector.source[name.byte_range()].to_owned());
622            collector.add_point(node, PointKind::Function, label);
623        }
624        "if_statement" | "if_expression" => {
625            if let Some(condition) = condition_of(node, language) {
626                let probes = collector.add_branch(node, "if", &["true", "false"]);
627                // An arm written without braces has nowhere to record itself.
628                let (consequence, alternative) = arms(node, language);
629                for arm in [consequence, alternative].into_iter().flatten() {
630                    collector.ensure_block(arm);
631                }
632                if narrows_a_type(condition, collector.source, language) {
633                    // The condition stays exactly as written, and the branch is
634                    // recorded from inside the arms instead. Which way it went
635                    // is still measured; only the vectors are lost, because
636                    // those need the operands wrapped.
637                    collector.record_arms(node, language, &probes);
638                    let limitation = collector.limitation_id(node, "condition-narrows-a-type");
639                    collector.limitations.push(serde_json::json!({
640                        "id": limitation,
641                        "kind": "condition-narrows-a-type",
642                        "file": collector.file,
643                        "source": collector.text(node),
644                        "line": collector.position(node).0,
645                        "column": collector.position(node).1,
646                        "reason": "the compiler reads this condition to narrow a type in the branch below it, so observing its operands would stop the code compiling; the branch is recorded from its arms and carries no condition vectors",
647                    }));
648                    return;
649                }
650                let decision = collector.add_decision(condition, "if");
651                let wrapper = match decision {
652                    Some(index) => {
653                        format!("{RUNTIME_CLASS}.bd({}, {}, {index}, ", probes[0], probes[1])
654                    }
655                    None => format!("{RUNTIME_CLASS}.b({}, {}, ", probes[0], probes[1]),
656                };
657                collector.edit(condition.start_byte(), 5, wrapper);
658                collector.edit(condition.end_byte(), 5, ")".to_owned());
659            }
660        }
661        "while_statement" | "for_statement" | "do_statement" | "do_while_statement" => {
662            match node.child_by_field_name("condition") {
663                Some(condition) => {
664                    let inner = if language == JvmLanguage::Java
665                        && condition.kind() == "parenthesized_expression"
666                    {
667                        let mut cursor = condition.walk();
668                        condition
669                            .children(&mut cursor)
670                            .find(|child| child.is_named())
671                            .unwrap_or(condition)
672                    } else {
673                        condition
674                    };
675                    // `while (true)` is not an ordinary condition. The Java
676                    // compiler treats a constant one specially: it knows the
677                    // loop never completes, so a method whose body is one
678                    // needs no return after it. Wrapping the constant in a
679                    // call makes it an ordinary boolean expression, the
680                    // compiler decides the loop can exit, and the method stops
681                    // compiling for want of a return it never needed.
682                    //
683                    // There is nothing to measure there either. A condition
684                    // that can only go one way is an obligation no test could
685                    // ever half-satisfy, so leaving it alone is the more
686                    // accurate answer as well as the only compiling one.
687                    if matches!(
688                        collector.source[inner.byte_range()].trim(),
689                        "true" | "false"
690                    ) {
691                        let (line, column) = collector.position(node);
692                        let limitation =
693                            collector.limitation_id(node, "loop-with-constant-condition");
694                        collector.limitations.push(serde_json::json!({
695                            "id": limitation,
696                            "kind": "loop-with-constant-condition",
697                            "file": collector.file,
698                            "source": collector.text(node),
699                            "line": line,
700                            "column": column,
701                            "reason": "a loop whose condition is a constant can only go one way, and wrapping it would change what the compiler knows about the code around it",
702                        }));
703                    } else if narrows_a_type(inner, collector.source, language) {
704                        // A loop condition narrows types too. `while (node !=
705                        // null)` is how a great deal of Kotlin walks a
706                        // structure, and the body below it reads `node` as
707                        // non-null. An `if` survives this because its arms can
708                        // carry the probes instead; a loop has only the one
709                        // arm, and no place to record the exit that a `break`
710                        // would not also reach. So the condition is left
711                        // exactly as written and the loop carries no branch
712                        // obligation, rather than one no test could close.
713                        let (line, column) = collector.position(node);
714                        let limitation = collector.limitation_id(node, "condition-narrows-a-type");
715                        collector.limitations.push(serde_json::json!({
716                            "id": limitation,
717                            "kind": "condition-narrows-a-type",
718                            "file": collector.file,
719                            "source": collector.text(node),
720                            "line": line,
721                            "column": column,
722                            "reason": "the compiler reads this loop condition to narrow a type in the body below it, so observing its operands would stop the code compiling; the loop carries no branch obligation and its body is measured by its statements",
723                        }));
724                    } else {
725                        let probes = collector.add_branch(node, "loop", &["true", "false"]);
726                        let decision = collector.add_decision(inner, "loop");
727                        let wrapper = match decision {
728                            Some(index) => format!(
729                                "{RUNTIME_CLASS}.bd({}, {}, {index}, ",
730                                probes[0], probes[1]
731                            ),
732                            None => format!("{RUNTIME_CLASS}.b({}, {}, ", probes[0], probes[1]),
733                        };
734                        collector.edit(inner.start_byte(), 5, wrapper);
735                        collector.edit(inner.end_byte(), 5, ")".to_owned());
736                    }
737                }
738                None => {
739                    let (line, column) = collector.position(node);
740                    let limitation = collector.limitation_id(node, "loop-without-condition");
741                    collector.limitations.push(serde_json::json!({
742                        "id": limitation,
743                        "kind": "loop-without-condition",
744                        "file": collector.file,
745                        "source": collector.text(node),
746                        "line": line,
747                        "column": column,
748                        "reason": "a for-each or unconditional loop has no condition to observe, so no branch obligation is recorded for it",
749                    }));
750                }
751            }
752            if let Some(body) = node.child_by_field_name("body") {
753                collector.ensure_block(body);
754            }
755        }
756        "enhanced_for_statement" => {
757            let (line, column) = collector.position(node);
758            let limitation = collector.limitation_id(node, "loop-without-condition");
759            collector.limitations.push(serde_json::json!({
760                "id": limitation,
761                "kind": "loop-without-condition",
762                "file": collector.file,
763                "source": collector.text(node),
764                "line": line,
765                "column": column,
766                "reason": "a for-each loop has no condition to observe, so no branch obligation is recorded for it",
767            }));
768            if let Some(body) = node.child_by_field_name("body") {
769                collector.ensure_block(body);
770            }
771        }
772        _ if in_statement_position(node, language)
773            && is_statement(node, collector.source, language)
774            && !is_opening_contract(node, collector.source, language) =>
775        {
776            collector.add_point(node, PointKind::Statement, None);
777        }
778        _ => {}
779    }
780    let mut cursor = node.walk();
781    for child in node.children(&mut cursor) {
782        if child.is_named() {
783            walk(collector, child);
784        }
785    }
786}
787
788fn is_statement(node: Node, source: &str, language: JvmLanguage) -> bool {
789    match language {
790        JvmLanguage::Java => is_java_statement(node.kind()),
791        JvmLanguage::Kotlin => is_kotlin_statement(node, source),
792    }
793}
794
795/// Whether a probe may be placed before this node.
796///
797/// Both languages have slots that hold a statement but are not statement
798/// positions — a `for` loop's initialiser and update, a resource in
799/// `try-with-resources`. A probe there does not compile. Everything that is
800/// genuinely a statement is a child of a block or a switch group, so that is
801/// the rule rather than a list of exceptions to remember.
802fn in_statement_position(node: Node, _language: JvmLanguage) -> bool {
803    node.parent().is_some_and(|parent| {
804        matches!(
805            parent.kind(),
806            "block" | "statements" | "switch_block_statement_group" | "constructor_body"
807        )
808    })
809}
810
811/// Apply edits right to left so earlier offsets stay valid.
812pub fn rewrite(source: &str, edits: &[GoEdit]) -> String {
813    crate::go_instrumenter::rewrite(source, edits)
814}
815
816#[cfg(test)]
817mod tests {
818    use super::*;
819
820    /// Every field the coverage index stores for a limitation.
821    ///
822    /// A limitation missing one of these is written into a run that then
823    /// cannot be opened at all -- `invalid coverage index: coverage
824    /// limitation`, with no coverage report and nothing naming the file that
825    /// caused it. These were writing `detail` where the index reads `reason`,
826    /// and none of them wrote `source`, so any run that measured a for-each
827    /// loop was unreadable.
828    fn assert_indexable(limitations: &[serde_json::Value]) -> Vec<String> {
829        assert!(!limitations.is_empty(), "nothing to check");
830        for limitation in limitations {
831            for field in ["id", "kind", "file", "source", "reason"] {
832                assert!(
833                    limitation.get(field).and_then(|v| v.as_str()).is_some(),
834                    "a limitation needs a string {field}: {limitation}"
835                );
836            }
837            for field in ["line", "column"] {
838                assert!(
839                    limitation.get(field).and_then(|v| v.as_u64()).is_some(),
840                    "a limitation needs a number {field}: {limitation}"
841                );
842            }
843        }
844        let mut kinds = limitations
845            .iter()
846            .filter_map(|limitation| limitation["kind"].as_str().map(str::to_owned))
847            .collect::<Vec<_>>();
848        kinds.sort();
849        kinds.dedup();
850        kinds
851    }
852
853    /// A file that does not parse has to say where, or nobody can act on it.
854    /// square/moshi's JsonReader.kt is this shape: nine hundred lines, and a
855    /// nested class whose primary constructor is written on the line after its
856    /// name, which tree-sitter-kotlin-ng 1.1.0 does not accept. The message
857    /// said "source does not parse" and left the reader to find it.
858    #[test]
859    fn a_file_that_does_not_parse_says_where() {
860        const NESTED: &str = r#"package app
861
862class Outer {
863  public class Options
864  private constructor(
865    internal val strings: Array<out String>,
866  ) {
867  }
868}
869"#;
870        let message = parse(NESTED, JvmLanguage::Kotlin)
871            .expect_err("does not parse")
872            .to_string();
873        // Where the parser gave up, and how far it gave up for -- a class it
874        // could not read is not a one-line problem, and saying "line 3" alone
875        // reads as though it were.
876        assert!(message.contains("line 3"), "{message}");
877        // The extent is tree-sitter's own account of how far it recovered, so
878        // the test holds that a range is given rather than pinning its end.
879        assert!(message.contains("through line "), "{message}");
880        assert!(message.contains("class Outer"), "{message}");
881
882        // An error inside an otherwise valid file is named where it is,
883        // which is the case a reader meets most often.
884        let local = "class A {\n    fun f(): Int {\n        return 1 )\n    }\n}\n";
885        let message = parse(local, JvmLanguage::Kotlin)
886            .expect_err("does not parse")
887            .to_string();
888        assert!(message.contains("line 3"), "{message}");
889    }
890
891    #[test]
892    fn every_limitation_carries_what_the_index_stores() {
893        const JAVA: &str = r#"class Every {
894    int walk(java.util.List<Object> items) {
895        int sum = 0;
896        for (Object item : items) {
897            if (item instanceof Integer value) {
898                sum += value;
899            }
900        }
901        for (;;) {
902            break;
903        }
904        while (true) {
905            break;
906        }
907        return sum;
908    }
909}
910"#;
911        let (obligations, _) = java(JAVA);
912        assert_eq!(
913            assert_indexable(&obligations.manifest.limitations),
914            [
915                "condition-narrows-a-type",
916                "loop-with-constant-condition",
917                "loop-without-condition"
918            ]
919        );
920
921        const KOTLIN: &str = r#"fun walk(items: List<Any>, head: Any?): Int {
922    var sum = 0
923    for (item in items) {
924        if (item is Int) {
925            sum += item
926        }
927    }
928    var node = head
929    while (node != null) {
930        node = null
931    }
932    while (true) {
933        break
934    }
935    return sum
936}
937"#;
938        let mut next = 0;
939        let mut decisions = 0;
940        let obligations = build_jvm_obligations(
941            "Every.kt",
942            KOTLIN,
943            JvmLanguage::Kotlin,
944            &mut next,
945            &mut decisions,
946        )
947        .expect("kotlin");
948        assert_eq!(
949            assert_indexable(&obligations.manifest.limitations),
950            [
951                "condition-narrows-a-type",
952                "loop-with-constant-condition",
953                "loop-without-condition"
954            ]
955        );
956    }
957
958    fn java(source: &str) -> (JvmFileObligations, String) {
959        let mut next = 0;
960        let mut decisions = 0;
961        let obligations = build_jvm_obligations(
962            "X.java",
963            source,
964            JvmLanguage::Java,
965            &mut next,
966            &mut decisions,
967        )
968        .expect("java");
969        let out = rewrite(source, &obligations.edits);
970        parse(&out, JvmLanguage::Java)
971            .unwrap_or_else(|error| panic!("rewritten Java does not parse: {error}\n{out}"));
972        (obligations, out)
973    }
974
975    const SAMPLE: &str = r#"class Classify {
976    String classify(int a, boolean b) {
977        if (a > 10 && b) {
978            return "big";
979        }
980        for (int i = 0; i < a; i++) {
981            System.out.print(i);
982        }
983        return "small";
984    }
985}
986"#;
987
988    #[test]
989    fn a_short_circuiting_operator_splits_a_decision_and_a_bitwise_one_does_not() {
990        // `&` and `|` evaluate both operands always, so neither can
991        // independently affect the outcome in the sense MC/DC means. Counting
992        // them would put obligations in the denominator no test could satisfy.
993        let (short_circuit, _) = java(SAMPLE);
994        assert_eq!(
995            short_circuit.manifest.decisions[0].conditions,
996            ["a > 10", "b"]
997        );
998
999        let (bitwise, _) = java(
1000            "class X { boolean f(boolean a, boolean b) { if (a & b) { return true; } return false; } }",
1001        );
1002        assert!(
1003            bitwise.manifest.decisions.is_empty(),
1004            "{:?}",
1005            bitwise.manifest.decisions
1006        );
1007    }
1008
1009    #[test]
1010    fn an_arm_written_without_braces_gets_them() {
1011        // `if (x) return;` has no block, so a probe before the statement would
1012        // sit outside the `if`: it would run unconditionally while the return
1013        // stayed guarded, and the report would claim the arm was taken.
1014        let (_, out) = java("class X { int f(int a) { if (a > 1) return 1; else return 2; } }");
1015        assert!(out.contains("{ "), "{out}");
1016        let guarded = out.find("return 1").expect("consequence");
1017        let opened = out[..guarded].rfind('{').expect("a brace before it");
1018        let probe = out[..guarded].rfind("HITS[").expect("a probe before it");
1019        assert!(
1020            opened < probe,
1021            "the probe must be inside the braces:\n{out}"
1022        );
1023    }
1024
1025    #[test]
1026    fn a_probe_never_lands_where_java_forbids_a_statement() {
1027        // A `for` loop's initialiser and update hold statements but are not
1028        // statement positions, and try-with-resources holds declarations.
1029        let (_, out) = java(
1030            "import java.io.*;\nclass X { void f(int a) throws Exception { for (int i = 0; i < a; i++) { g(); } try (Reader r = open()) { g(); } } void g() {} Reader open() { return null; } }",
1031        );
1032        assert!(
1033            !out.contains("for (com.supercorp"),
1034            "probe in a for initialiser:\n{out}"
1035        );
1036        assert!(
1037            !out.contains("try (com.supercorp"),
1038            "probe in a resource:\n{out}"
1039        );
1040    }
1041
1042    #[test]
1043    fn a_for_each_loop_records_a_limitation_not_an_obligation() {
1044        // There is no condition to observe. Declaring a branch nothing can
1045        // measure would put an obligation in the denominator no test could
1046        // ever satisfy.
1047        let (obligations, _) =
1048            java("class X { void f(int[] xs) { for (int x : xs) { g(x); } } void g(int x) {} }");
1049        assert!(
1050            obligations
1051                .manifest
1052                .branches
1053                .iter()
1054                .all(|b| b.kind != "loop")
1055        );
1056        assert_eq!(obligations.manifest.limitations.len(), 1);
1057        assert_eq!(
1058            obligations.manifest.limitations[0]["kind"],
1059            "loop-without-condition"
1060        );
1061    }
1062
1063    #[test]
1064    fn kotlin_shares_the_model_and_differs_where_it_must() {
1065        // Kotlin's `if` is an expression and its condition is not
1066        // parenthesised, so the wrapper has to find a different child.
1067        let source = "fun f(a: Int, b: Boolean): String {\n    if (a > 10 && b) {\n        return \"big\"\n    }\n    return \"small\"\n}\n";
1068        let mut next = 0;
1069        let mut decisions = 0;
1070        let obligations = build_jvm_obligations(
1071            "X.kt",
1072            source,
1073            JvmLanguage::Kotlin,
1074            &mut next,
1075            &mut decisions,
1076        )
1077        .expect("kotlin");
1078        assert_eq!(
1079            obligations.manifest.decisions[0].conditions,
1080            ["a > 10", "b"]
1081        );
1082        let out = rewrite(source, &obligations.edits);
1083        parse(&out, JvmLanguage::Kotlin)
1084            .unwrap_or_else(|error| panic!("rewritten Kotlin does not parse: {error}\n{out}"));
1085        assert!(out.contains(".bd("), "{out}");
1086        assert!(
1087            out.contains(".c(0, 0, ") && out.contains(".c(0, 1, "),
1088            "{out}"
1089        );
1090    }
1091
1092    #[test]
1093    fn decisions_are_numbered_across_the_project_not_within_a_file() {
1094        // The runtime holds one decision-state array for the whole run, so an
1095        // index meaning "the first decision in this file" would land on every
1096        // other file's first decision: two classes would share condition
1097        // state, and the vectors both produced would describe neither.
1098        let mut next = 0;
1099        let mut decisions = 0;
1100        let java = build_jvm_obligations(
1101            "A.java",
1102            "class A { static boolean f(boolean x, boolean y) { if (x && y) { return true; } return false; } }",
1103            JvmLanguage::Java,
1104            &mut next,
1105            &mut decisions,
1106        )
1107        .unwrap();
1108        let kotlin = build_jvm_obligations(
1109            "B.kt",
1110            "fun g(x: Boolean, y: Boolean): Boolean {\n    if (x || y) {\n        return true\n    }\n    return false\n}\n",
1111            JvmLanguage::Kotlin,
1112            &mut next,
1113            &mut decisions,
1114        )
1115        .unwrap();
1116
1117        let referenced = |obligations: &JvmFileObligations| {
1118            obligations
1119                .edits
1120                .iter()
1121                .filter_map(|edit| {
1122                    let at = edit.text.find(".c(")?;
1123                    edit.text[at + 3..]
1124                        .split(',')
1125                        .next()?
1126                        .trim()
1127                        .parse::<u32>()
1128                        .ok()
1129                })
1130                .collect::<std::collections::BTreeSet<_>>()
1131        };
1132        assert_eq!(referenced(&java), [0].into());
1133        assert_eq!(referenced(&kotlin), [1].into());
1134        assert_eq!(decisions, 2, "the project numbered two decisions in all");
1135        assert_eq!(java.decision_widths, [2]);
1136        assert_eq!(kotlin.decision_widths, [2]);
1137    }
1138
1139    #[test]
1140    fn kotlin_statements_are_the_kinds_the_grammar_produces() {
1141        // Everything in Kotlin is an expression, so the grammar's names are
1142        // not the language's vocabulary: a `return` is a return_expression,
1143        // a `y--` is a unary_expression, and `break` and `continue` get no
1144        // kind of their own at all and arrive as identifiers. A list written
1145        // from the language reference misses all of them, and a function of
1146        // nothing but returns measures as having no statements.
1147        let source = "fun f(xs: List<Int>, a: Int): Int {\n    var y = a\n    y = y + 1\n    y--\n    for (i in xs) {\n        if (i == 1) { continue }\n        if (i == 2) { break }\n    }\n    try { println(y) } catch (e: Exception) { throw e }\n    return y\n}\n";
1148        let mut next = 0;
1149        let mut decisions = 0;
1150        let obligations = build_jvm_obligations(
1151            "f.kt",
1152            source,
1153            JvmLanguage::Kotlin,
1154            &mut next,
1155            &mut decisions,
1156        )
1157        .expect("obligations");
1158
1159        // The rewritten source still parses, which is what says the probes
1160        // went somewhere Kotlin accepts.
1161        let rewritten = rewrite(source, &obligations.edits);
1162        parse(&rewritten, JvmLanguage::Kotlin)
1163            .unwrap_or_else(|error| panic!("{error}\n{rewritten}"));
1164
1165        let lines = obligations
1166            .manifest
1167            .points
1168            .iter()
1169            .map(|point| point.line)
1170            .collect::<std::collections::BTreeSet<_>>();
1171        // A loop is a branch rather than a point, in both languages: what
1172        // matters about it is which way it went, and its body's statements
1173        // are measured on their own.
1174        for (line, what) in [
1175            (1, "the function itself"),
1176            (2, "var y = a"),
1177            (3, "y = y + 1"),
1178            (4, "y--"),
1179            (6, "if/continue"),
1180            (7, "if/break"),
1181            (9, "try/throw"),
1182            (10, "return"),
1183        ] {
1184            assert!(
1185                lines.contains(&line),
1186                "{what} on line {line} is unmeasured: {lines:?}"
1187            );
1188        }
1189    }
1190
1191    #[test]
1192    fn a_loop_on_a_constant_keeps_what_the_compiler_knows() {
1193        // `while (true)` is how Java writes a loop that never completes, and
1194        // the compiler treats the constant specially: a method whose body is
1195        // one needs no return after it. Wrapping the constant in a call makes
1196        // it an ordinary boolean expression, the compiler decides the loop can
1197        // exit, and the method stops compiling for want of a return it never
1198        // needed. There is nothing to measure there in any case.
1199        let source = "class X {\n  String f() {\n    while (true) {\n      if (g()) { return \"a\"; }\n    }\n  }\n  boolean g() { return true; }\n}";
1200        let mut next = 0;
1201        let mut decisions = 0;
1202        let obligations = build_jvm_obligations(
1203            "X.java",
1204            source,
1205            JvmLanguage::Java,
1206            &mut next,
1207            &mut decisions,
1208        )
1209        .expect("obligations");
1210        let rewritten = rewrite(source, &obligations.edits);
1211        assert!(
1212            rewritten.contains("while (true)"),
1213            "the constant must survive untouched:\n{rewritten}"
1214        );
1215        // The `if` beside it is still measured, so this is a narrow exception
1216        // rather than a loop nobody looks at.
1217        assert!(
1218            obligations
1219                .manifest
1220                .branches
1221                .iter()
1222                .any(|branch| branch.kind == "if"),
1223            "{:?}",
1224            obligations.manifest.branches
1225        );
1226        assert!(
1227            !obligations
1228                .manifest
1229                .branches
1230                .iter()
1231                .any(|branch| branch.kind == "loop"),
1232            "a condition that can only go one way is not an obligation: {:?}",
1233            obligations.manifest.branches
1234        );
1235        assert!(
1236            obligations
1237                .manifest
1238                .limitations
1239                .iter()
1240                .any(|limitation| limitation["kind"] == "loop-with-constant-condition"),
1241            "{:?}",
1242            obligations.manifest.limitations
1243        );
1244    }
1245}