1use 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
22pub const RUNTIME_CLASS: &str = "com.supercorp.supercov.Supercov";
24pub 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
77fn 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
103fn 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 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 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 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 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 None => return,
234 },
235 PointKind::Statement => node.start_byte(),
236 };
237 let probe = self.probe(target, at);
238 if after_contract {
239 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 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 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 Some(arm) if arm.kind() == "block" => self.store(arm.start_byte() + 1, *probe),
349 Some(arm) => self.store(arm.start_byte(), *probe),
350 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 self.add_point(node, PointKind::Statement, None);
375 }
376 }
377}
378
379fn 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
405fn 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
442fn 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
470fn 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
477fn narrows_a_type(node: Node, source: &str, language: JvmLanguage) -> bool {
491 let narrows = match language {
492 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
529fn 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
562fn 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 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 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 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 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
795fn 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
811pub 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 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 #[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 assert!(message.contains("line 3"), "{message}");
877 assert!(message.contains("through line "), "{message}");
880 assert!(message.contains("class Outer"), "{message}");
881
882 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 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 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 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 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 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 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 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 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 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 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 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}