1use 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#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum GoProbeTarget {
38 Statement {
39 id: String,
40 },
41 Function {
42 id: String,
43 },
44 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 pub at: usize,
62}
63
64#[derive(Debug, Clone, PartialEq)]
65pub struct GoFileObligations {
66 pub manifest: CoverageManifest,
67 pub probes: BTreeMap<u64, GoProbe>,
68 pub edits: Vec<GoEdit>,
70 pub decision_widths: Vec<u8>,
73}
74
75pub(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
108fn parse_exactly(source: &str) -> Result<tree_sitter::Tree, GoInstrumenterError> {
110 let mut parser = Parser::new();
111 parser
112 .set_language(&tree_sitter_go::LANGUAGE.into())
113 .map_err(|error| GoInstrumenterError::Parse(error.to_string()))?;
114 let tree = parser
115 .parse(source, None)
116 .ok_or_else(|| GoInstrumenterError::Parse("parser returned no tree".into()))?;
117 if tree.root_node().has_error() {
118 return Err(GoInstrumenterError::Parse(parse_failure(&tree, source)));
119 }
120 Ok(tree)
121}
122
123const NEW_VALUE_STAND_IN: &str = "nEw";
131
132fn stand_in_for_new_values(source: &str) -> Option<String> {
157 let bytes = source.as_bytes();
158 let mut masked: Option<String> = None;
159 let mut at = 0;
160 while at < bytes.len() {
161 if let Some(past) = skip_literal_or_comment(bytes, at) {
162 at = past;
163 continue;
164 }
165 let Some(open) = builtin_new_at(bytes, at) else {
166 at += 1;
167 continue;
168 };
169 let Some(close) = closing_parenthesis(bytes, open) else {
170 at += 3;
171 continue;
172 };
173 if !parses_as_a_type(&source[open + 1..close]) {
174 masked
175 .get_or_insert_with(|| source.to_owned())
176 .replace_range(at..at + 3, NEW_VALUE_STAND_IN);
177 }
178 at += 3;
179 }
180 masked
181}
182
183fn builtin_new_at(bytes: &[u8], at: usize) -> Option<usize> {
198 if !bytes[at..].starts_with(b"new") {
199 return None;
200 }
201 if at > 0 && (is_identifier_byte(bytes[at - 1]) || bytes[at - 1] == b'.') {
202 return None;
203 }
204 let mut open = at + 3;
206 while open < bytes.len() && bytes[open].is_ascii_whitespace() {
207 open += 1;
208 }
209 (bytes.get(open) == Some(&b'(')).then_some(open)
210}
211
212fn is_identifier_byte(byte: u8) -> bool {
213 byte.is_ascii_alphanumeric() || byte == b'_' || byte >= 0x80
217}
218
219fn closing_parenthesis(bytes: &[u8], open: usize) -> Option<usize> {
222 let mut depth = 0_usize;
223 let mut at = open;
224 while at < bytes.len() {
225 if let Some(past) = skip_literal_or_comment(bytes, at) {
226 at = past;
227 continue;
228 }
229 match bytes[at] {
230 b'(' => depth += 1,
231 b')' => {
232 depth = depth.saturating_sub(1);
233 if depth == 0 {
234 return Some(at);
235 }
236 }
237 _ => {}
238 }
239 at += 1;
240 }
241 None
242}
243
244fn parses_as_a_type(argument: &str) -> bool {
253 parse_exactly(&format!("package p\n\nvar _ {}\n", argument.trim())).is_ok()
256}
257
258fn skip_literal_or_comment(bytes: &[u8], at: usize) -> Option<usize> {
263 let rest = &bytes[at..];
264 if rest.starts_with(b"//") {
265 return Some(
266 bytes[at..]
267 .iter()
268 .position(|byte| *byte == b'\n')
269 .map_or(bytes.len(), |end| at + end),
270 );
271 }
272 if rest.starts_with(b"/*") {
273 return Some(
274 bytes[at + 2..]
275 .windows(2)
276 .position(|pair| pair == b"*/")
277 .map_or(bytes.len(), |end| at + 4 + end),
278 );
279 }
280 if bytes[at] == b'`' {
282 return Some(
283 bytes[at + 1..]
284 .iter()
285 .position(|byte| *byte == b'`')
286 .map_or(bytes.len(), |end| at + 2 + end),
287 );
288 }
289 let quote = match bytes[at] {
290 b'"' | b'\'' => bytes[at],
291 _ => return None,
292 };
293 let mut cursor = at + 1;
294 while cursor < bytes.len() {
295 match bytes[cursor] {
296 b'\\' => cursor += 2,
299 b'\n' => return Some(cursor),
302 byte if byte == quote => return Some(cursor + 1),
303 _ => cursor += 1,
304 }
305 }
306 Some(bytes.len())
307}
308
309pub fn parse(source: &str) -> Result<tree_sitter::Tree, GoInstrumenterError> {
310 let refusal = match parse_exactly(source) {
311 Ok(tree) => return Ok(tree),
312 Err(refusal) => refusal,
313 };
314 let stand_in = stand_in_for_new_values(source).ok_or_else(|| refusal.clone())?;
317 parse_exactly(&stand_in).map_err(|_| refusal)
320}
321
322pub(crate) fn parse_failure(tree: &tree_sitter::Tree, source: &str) -> String {
330 let mut deepest: Option<(usize, Node)> = None;
331 let mut stack = vec![(0_usize, tree.root_node())];
332 while let Some((depth, node)) = stack.pop() {
333 if (node.is_error() || node.is_missing()) && deepest.is_none_or(|(found, _)| depth > found)
334 {
335 deepest = Some((depth, node));
336 }
337 let mut cursor = node.walk();
341 for child in node.children(&mut cursor) {
342 stack.push((depth + 1, child));
343 }
344 }
345 let Some((_, node)) = deepest else {
346 return "source does not parse".to_owned();
347 };
348 let start = node.start_position();
349 let mut line = source
350 .lines()
351 .nth(start.row)
352 .unwrap_or_default()
353 .trim()
354 .to_owned();
355 if line.chars().count() > 120 {
356 line = line.chars().take(117).collect::<String>() + "...";
357 }
358 let what = if node.is_missing() {
359 "missing syntax"
360 } else {
361 "unexpected syntax"
362 };
363 let end = node.end_position().row + 1;
368 let through = if end > start.row + 1 {
369 format!(" (through line {end})")
370 } else {
371 String::new()
372 };
373 format!(
374 "{what} at line {}, column {}{through}: {line}",
375 start.row + 1,
376 start.column + 1,
377 )
378}
379
380fn is_statement(kind: &str) -> bool {
384 matches!(
385 kind,
386 "assignment_statement"
387 | "break_statement"
388 | "const_declaration"
389 | "continue_statement"
390 | "dec_statement"
391 | "defer_statement"
392 | "expression_statement"
393 | "expression_switch_statement"
394 | "fallthrough_statement"
395 | "for_statement"
396 | "go_statement"
397 | "goto_statement"
398 | "if_statement"
399 | "inc_statement"
400 | "labeled_statement"
401 | "return_statement"
402 | "select_statement"
403 | "send_statement"
404 | "short_var_declaration"
405 | "type_switch_statement"
406 | "var_declaration"
407 )
408}
409
410struct Collector<'a> {
411 file: &'a str,
412 source: &'a str,
413 alias: &'a str,
414 next_probe: &'a mut u64,
415 edits: Vec<GoEdit>,
416 points: Vec<PointMeta>,
417 branches: Vec<BranchMeta>,
418 decisions: Vec<DecisionMeta>,
419 probes: BTreeMap<u64, GoProbe>,
420 limitations: Vec<serde_json::Value>,
421 widths: Vec<u8>,
422 decision_base: u32,
427}
428
429fn branch_wrapper(alias: &str, when_true: u64, when_false: u64, decision: Option<usize>) -> String {
436 match decision {
437 Some(index) => format!("{alias}.BD({when_true}, {when_false}, {index}, "),
438 None => format!("{alias}.B({when_true}, {when_false}, "),
439 }
440}
441
442fn in_statement_position(node: Node) -> bool {
450 node.parent()
451 .is_some_and(|parent| parent.kind() == "statement_list")
452}
453
454fn loop_condition<'t>(node: Node<'t>) -> Option<Node<'t>> {
457 if let Some(condition) = node.child_by_field_name("condition") {
458 return Some(condition);
459 }
460 let mut cursor = node.walk();
461 let clause = node
462 .children(&mut cursor)
463 .find(|child| child.kind() == "for_clause")?;
464 let mut inner = clause.walk();
465 clause
466 .children(&mut inner)
467 .find(|child| child.is_named() && child.kind().ends_with("_expression"))
468}
469
470impl<'a> Collector<'a> {
471 fn id(&mut self, node: Node, kind: &str) -> String {
472 stable_obligation_id("go", self.file, kind, node.start_byte(), node.end_byte())
473 }
474
475 fn limitation_id(&mut self, node: Node, kind: &str) -> String {
480 self.id(node, kind)
481 }
482
483 fn probe(&mut self, target: GoProbeTarget, at: usize) -> u64 {
484 *self.next_probe += 1;
485 let id = *self.next_probe;
486 self.probes.insert(id, GoProbe { id, target, at });
487 id
488 }
489
490 fn edit(&mut self, at: usize, rank: i32, text: String) {
491 self.edits.push(GoEdit { at, rank, text });
492 }
493
494 fn call_before(&mut self, at: usize, call: String) {
497 self.edit(at, 100, format!("{call}; "));
498 }
499
500 fn position(&self, node: Node) -> (usize, usize) {
502 let start = node.start_position();
503 (start.row + 1, start.column + 1)
504 }
505
506 fn text(&self, node: Node) -> String {
507 let raw = &self.source[node.byte_range()];
508 let line = raw.lines().next().unwrap_or("");
509 line.trim().to_owned()
510 }
511
512 fn add_point(&mut self, node: Node, kind: PointKind, label: Option<String>) {
513 let (line, column) = self.position(node);
514 let id = self.id(
515 node,
516 match kind {
517 PointKind::Function => "function",
518 PointKind::Statement => "statement",
519 },
520 );
521 let target = match kind {
522 PointKind::Function => GoProbeTarget::Function { id: id.clone() },
523 PointKind::Statement => GoProbeTarget::Statement { id: id.clone() },
524 };
525 let at = match kind {
528 PointKind::Function => match node.child_by_field_name("body") {
529 Some(body) => body.start_byte() + 1,
530 None => return,
531 },
532 PointKind::Statement => node.start_byte(),
533 };
534 let probe = self.probe(target, at);
535 self.call_before(at, format!("{HITS_VARIABLE}[{probe}] = 2"));
539 self.points.push(PointMeta {
540 id,
541 kind,
542 file: self.file.to_owned(),
543 line,
544 column,
545 source: self.text(node),
546 label,
547 });
548 }
549
550 fn add_branch(&mut self, node: Node, kind: &str, labels: &[&str]) -> Vec<u64> {
551 let (line, column) = self.position(node);
552 let id = self.id(node, "branch");
553 let mut probes = Vec::new();
554 let alternatives = labels
555 .iter()
556 .map(|label| {
557 let alternative = format!("{id}.{label}");
558 probes.push(self.probe(
559 GoProbeTarget::Alternative {
560 branch: id.clone(),
561 alternative: alternative.clone(),
562 },
563 node.start_byte(),
564 ));
565 BranchAlternativeMeta {
566 id: alternative,
567 label: (*label).to_owned(),
568 }
569 })
570 .collect();
571 self.branches.push(BranchMeta {
572 id,
573 kind: kind.to_owned(),
574 file: self.file.to_owned(),
575 line,
576 column,
577 source: self.text(node),
578 alternatives,
579 });
580 probes
581 }
582
583 fn add_decision(&mut self, node: Node, kind: &str) -> Option<usize> {
589 let mut leaves = Vec::new();
590 condition_nodes(node, self.source, &mut leaves);
591 if leaves.len() < 2 {
592 return None;
593 }
594 let conditions = leaves
595 .iter()
596 .map(|leaf| self.source[leaf.byte_range()].trim().to_owned())
597 .collect::<Vec<_>>();
598 let (line, column) = self.position(node);
599 let id = self.id(node, "decision");
600 let index_of_decision = self.decision_base as usize + self.widths.len();
604 self.widths.push(leaves.len().min(64) as u8);
605 let alias = self.alias.to_owned();
606 for (index, leaf) in leaves.iter().enumerate() {
611 self.edit(
615 leaf.start_byte(),
616 20,
617 format!("{alias}.C({index_of_decision}, {index}, "),
618 );
619 self.edit(leaf.end_byte(), 20, ")".to_owned());
620 }
621 self.decisions.push(DecisionMeta {
622 id,
623 file: self.file.to_owned(),
624 line,
625 column,
626 source: self.text(node),
627 conditions,
628 kind: kind.to_owned(),
629 });
630 Some(index_of_decision)
631 }
632
633 fn walk(&mut self, node: Node) {
634 let alias = self.alias.to_owned();
635 match node.kind() {
636 "function_declaration" | "method_declaration" | "func_literal" => {
637 let label = node
638 .child_by_field_name("name")
639 .map(|name| self.source[name.byte_range()].to_owned());
640 self.add_point(node, PointKind::Function, label);
641 }
642 "if_statement" => {
643 if let Some(condition) = node.child_by_field_name("condition") {
644 let probes = self.add_branch(node, "if", &["true", "false"]);
649 let decision = self.add_decision(condition, "if");
650 self.edit(
651 condition.start_byte(),
652 5,
653 branch_wrapper(&alias, probes[0], probes[1], decision),
654 );
655 self.edit(condition.end_byte(), 5, ")".to_owned());
656 }
657 }
658 "for_statement" => {
659 match loop_condition(node) {
664 Some(condition) => {
665 let probes = self.add_branch(node, "loop", &["true", "false"]);
666 let decision = self.add_decision(condition, "loop");
667 self.edit(
668 condition.start_byte(),
669 5,
670 branch_wrapper(&alias, probes[0], probes[1], decision),
671 );
672 self.edit(condition.end_byte(), 5, ")".to_owned());
673 }
674 None => {
675 let (line, column) = self.position(node);
676 let limitation = self.limitation_id(node, "loop-without-condition");
677 self.limitations.push(serde_json::json!({
678 "id": limitation,
679 "kind": "loop-without-condition",
680 "file": self.file,
681 "source": self.text(node),
682 "line": line,
683 "column": column,
684 "reason": "a range or unconditional loop has no condition to observe, so no branch obligation is recorded for it",
685 }));
686 }
687 }
688 }
689 "expression_switch_statement" | "type_switch_statement" | "select_statement" => {
690 let kind = match node.kind() {
691 "expression_switch_statement" => "switch",
692 "type_switch_statement" => "type-switch",
693 _ => "select",
694 };
695 let mut cases = Vec::new();
696 let mut has_default = false;
697 let mut cursor = node.walk();
698 for child in node.children(&mut cursor) {
699 match child.kind() {
700 "expression_case" | "type_case" | "communication_case" => {
701 cases.push((self.text(child), Some(child)))
702 }
703 "default_case" => {
704 has_default = true;
705 cases.push(("default".to_owned(), Some(child)));
706 }
707 _ => {}
708 }
709 }
710 if !has_default && node.kind() != "select_statement" {
725 cases.push(("no case matched".to_owned(), None));
726 }
727 let labels = cases
728 .iter()
729 .map(|(label, _)| label.as_str())
730 .collect::<Vec<_>>();
731 let probes = self.add_branch(node, kind, &labels);
732 for (probe, (_, clause)) in probes.iter().zip(cases.iter()) {
733 match clause {
734 Some(clause) => {
735 let at = clause
736 .children(&mut clause.walk())
737 .find(|child| child.kind() == "statement_list")
738 .map(|body| body.start_byte())
739 .unwrap_or_else(|| clause.end_byte());
740 self.call_before(at, format!("{alias}.A({probe})"));
741 }
742 None => {
743 let at = node.end_byte().saturating_sub(1);
745 self.edit(at, 100, format!("\ndefault:\n{alias}.A({probe})\n"));
746 }
747 }
748 }
749 }
750 kind if is_statement(kind) && in_statement_position(node) => {
751 self.add_point(node, PointKind::Statement, None);
752 }
753 _ => {}
754 }
755 if in_statement_position(node)
758 && matches!(
759 node.kind(),
760 "if_statement"
761 | "for_statement"
762 | "expression_switch_statement"
763 | "type_switch_statement"
764 | "select_statement"
765 )
766 {
767 self.add_point(node, PointKind::Statement, None);
768 }
769 let mut cursor = node.walk();
770 for child in node.children(&mut cursor) {
771 if child.is_named() {
772 self.walk(child);
773 }
774 }
775 }
776}
777
778fn condition_nodes<'t>(node: Node<'t>, source: &str, out: &mut Vec<Node<'t>>) {
784 match node.kind() {
785 "binary_expression" => {
786 let operator = node
787 .child_by_field_name("operator")
788 .map(|op| &source[op.byte_range()])
789 .unwrap_or("");
790 if operator == "&&" || operator == "||" {
791 if let Some(left) = node.child_by_field_name("left") {
792 condition_nodes(left, source, out);
793 }
794 if let Some(right) = node.child_by_field_name("right") {
795 condition_nodes(right, source, out);
796 }
797 return;
798 }
799 out.push(node);
800 }
801 "parenthesized_expression" => {
802 let mut cursor = node.walk();
803 match node.children(&mut cursor).find(|child| child.is_named()) {
804 Some(inner) => condition_nodes(inner, source, out),
805 None => out.push(node),
806 }
807 }
808 _ => out.push(node),
809 }
810}
811
812#[derive(Debug, Clone, PartialEq, Eq)]
819pub struct GoEdit {
820 pub at: usize,
821 pub rank: i32,
822 pub text: String,
823}
824
825pub fn rewrite(source: &str, edits: &[GoEdit]) -> String {
827 let mut ordered = edits.to_vec();
828 ordered.sort_by(|a, b| b.at.cmp(&a.at).then(b.rank.cmp(&a.rank)));
829 let mut out = source.to_owned();
830 for edit in ordered {
831 if edit.at > out.len() {
832 continue;
833 }
834 out.insert_str(edit.at, &edit.text);
835 }
836 out
837}
838
839pub fn import_edit(source: &str, alias: &str, path: &str) -> Option<GoEdit> {
843 let tree = parse(source).ok()?;
844 let mut cursor = tree.root_node().walk();
845 let package = tree
846 .root_node()
847 .children(&mut cursor)
848 .find(|child| child.kind() == "package_clause")?;
849 Some(GoEdit {
850 at: package.end_byte(),
851 rank: 0,
852 text: format!("\nimport {alias} \"{path}\""),
853 })
854}
855
856pub fn build_go_obligations(
857 file: &str,
858 source: &str,
859 next_probe: &mut u64,
860 next_decision: &mut u32,
861) -> Result<GoFileObligations, GoInstrumenterError> {
862 build_go_obligations_with_alias(file, source, next_probe, next_decision, RUNTIME_ALIAS)
863}
864
865pub const RUNTIME_ALIAS: &str = "__supercov";
868
869pub const HITS_VARIABLE: &str = "__supercovHits";
872
873pub const RUNTIME_IMPORT: &str = "github.com/supercorp-ai/supercov/runtime/go/supercov";
875
876pub fn build_go_obligations_with_alias(
877 file: &str,
878 source: &str,
879 next_probe: &mut u64,
880 next_decision: &mut u32,
881 alias: &str,
882) -> Result<GoFileObligations, GoInstrumenterError> {
883 let tree = parse(source)?;
884 let decision_base = *next_decision;
885 let mut collector = Collector {
886 file,
887 source,
888 alias,
889 next_probe,
890 decision_base,
891 edits: Vec::new(),
892 points: Vec::new(),
893 branches: Vec::new(),
894 decisions: Vec::new(),
895 probes: BTreeMap::new(),
896 limitations: Vec::new(),
897 widths: Vec::new(),
898 };
899 let mut cursor = tree.root_node().walk();
900 for child in tree.root_node().children(&mut cursor) {
901 if child.is_named() {
902 collector.walk(child);
903 }
904 }
905 *next_decision += collector.widths.len() as u32;
906 let mut edits = collector.edits;
907 let calls_runtime = edits
911 .iter()
912 .any(|edit| edit.text.contains(&format!("{alias}.")));
913 if calls_runtime && let Some(import) = import_edit(source, alias, RUNTIME_IMPORT) {
914 edits.push(import);
915 }
916 Ok(GoFileObligations {
917 manifest: CoverageManifest {
918 decisions: collector.decisions,
919 points: collector.points,
920 branches: collector.branches,
921 limitations: collector.limitations,
922 unmeasured: Vec::new(),
923 scope: None,
924 },
925 probes: collector.probes,
926 edits,
927 decision_widths: collector.widths,
928 })
929}
930
931#[cfg(test)]
932mod tests {
933 use super::*;
934
935 fn assert_indexable(limitations: &[serde_json::Value]) -> Vec<String> {
944 assert!(!limitations.is_empty(), "nothing to check");
945 for limitation in limitations {
946 for field in ["id", "kind", "file", "source", "reason"] {
947 assert!(
948 limitation.get(field).and_then(|v| v.as_str()).is_some(),
949 "a limitation needs a string {field}: {limitation}"
950 );
951 }
952 for field in ["line", "column"] {
953 assert!(
954 limitation.get(field).and_then(|v| v.as_u64()).is_some(),
955 "a limitation needs a number {field}: {limitation}"
956 );
957 }
958 }
959 let mut kinds = limitations
960 .iter()
961 .filter_map(|limitation| limitation["kind"].as_str().map(str::to_owned))
962 .collect::<Vec<_>>();
963 kinds.sort();
964 kinds.dedup();
965 kinds
966 }
967
968 #[test]
970 fn a_file_that_does_not_parse_says_where() {
971 let broken = "package main\n\nfunc f() int {\n\treturn 1 )\n}\n";
972 let message = parse(broken).expect_err("does not parse").to_string();
973 assert!(message.contains("line 4"), "{message}");
974 assert!(message.contains("return 1"), "{message}");
975 assert!(
976 !message.contains("byte"),
977 "an offset is not somewhere a reader can look: {message}"
978 );
979 }
980
981 #[test]
982 fn every_limitation_carries_what_the_index_stores() {
983 const EVERY: &str = r#"package main
984
985func walk(items []int) int {
986 sum := 0
987 for _, item := range items {
988 sum += item
989 }
990 for {
991 break
992 }
993 return sum
994}
995"#;
996 let mut next = 0;
997 let mut decisions = 0;
998 let obligations =
999 build_go_obligations("walk.go", EVERY, &mut next, &mut decisions).expect("go");
1000 assert_eq!(
1001 assert_indexable(&obligations.manifest.limitations),
1002 ["loop-without-condition"]
1003 );
1004 }
1005
1006 const SAMPLE: &str = r#"package main
1007
1008import "fmt"
1009
1010func classify(a int, b bool) string {
1011 if a > 10 && b {
1012 return "big"
1013 }
1014 for i := 0; i < a; i++ {
1015 fmt.Println(i)
1016 }
1017 switch {
1018 case a == 0:
1019 return "zero"
1020 default:
1021 return "small"
1022 }
1023}
1024"#;
1025
1026 fn obligations(source: &str) -> GoFileObligations {
1027 let mut next = 0;
1028 let mut decisions = 0;
1029 build_go_obligations("main.go", source, &mut next, &mut decisions).expect("obligations")
1030 }
1031
1032 fn rewritten(source: &str) -> String {
1035 let mut next = 0;
1036 let mut decisions = 0;
1037 let go =
1038 build_go_obligations("x.go", source, &mut next, &mut decisions).expect("obligations");
1039 let out = rewrite(source, &go.edits);
1040 parse(&out)
1041 .unwrap_or_else(|error| panic!("rewritten source does not parse: {error}\n{out}"));
1042 out
1043 }
1044
1045 #[test]
1046 fn a_probe_never_lands_where_go_does_not_allow_a_statement() {
1047 let out = rewritten(
1051 "package main\nfunc f(a int) int {\n\tfor i := 0; i < a; i++ {\n\t\ta++\n\t}\n\tif b := a; b > 1 {\n\t\treturn b\n\t}\n\tswitch c := a; c {\n\tcase 1:\n\t\treturn 1\n\t}\n\treturn 0\n}\n",
1052 );
1053 assert!(
1054 !out.contains("for __supercov"),
1055 "probe in a for-clause init:\n{out}"
1056 );
1057 assert!(
1058 !out.contains("; __supercov.P"),
1059 "probe in a for-clause post:\n{out}"
1060 );
1061 assert!(
1062 out.contains("if b := a;"),
1063 "the if initialiser survived intact:\n{out}"
1064 );
1065 assert!(
1066 out.contains("switch c := a;"),
1067 "the switch initialiser survived intact:\n{out}"
1068 );
1069 }
1070
1071 #[test]
1072 fn wrapping_a_condition_preserves_short_circuit_order() {
1073 let out = rewritten(
1078 "package main\nfunc f(a int, b bool) bool {\n\tif a > 10 && b {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1079 );
1080 let condition = out
1081 .lines()
1082 .find(|line| line.contains("if "))
1083 .expect("the if survived");
1084 let branch = condition.find(".BD(").expect("branch and decision wrapper");
1085 let first = condition.find(".C(").expect("first condition wrapper");
1086 assert!(
1087 branch < first,
1088 "the branch must enclose its conditions: {condition}"
1089 );
1090 assert_eq!(
1091 condition.matches(".C(").count(),
1092 2,
1093 "one wrapper per condition: {condition}"
1094 );
1095 assert!(
1096 condition.contains("&&"),
1097 "the operator itself is untouched: {condition}"
1098 );
1099 }
1100
1101 #[test]
1102 fn a_single_condition_branch_uses_the_wrapper_that_inlines() {
1103 let out = rewritten(
1107 "package main\nfunc f(a int) bool {\n\tif a > 10 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1108 );
1109 assert!(out.contains(".B("), "{out}");
1110 assert!(!out.contains(".BD("), "no decision here to close:\n{out}");
1111 }
1112
1113 #[test]
1114 fn a_switch_without_a_default_gains_one_so_matching_nothing_is_observable() {
1115 let out = rewritten(
1118 "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1119 );
1120 assert!(out.contains("default:"), "{out}");
1121
1122 let existing = rewritten(
1124 "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\tdefault:\n\t\treturn\n\t}\n}\n",
1125 );
1126 assert_eq!(existing.matches("default:").count(), 1, "{existing}");
1127 }
1128
1129 #[test]
1130 fn a_loop_with_no_condition_records_a_limitation_not_an_obligation() {
1131 let mut next = 0;
1135 let mut decisions = 0;
1136 let go = build_go_obligations(
1137 "x.go",
1138 "package main\nfunc f(xs []int) {\n\tfor _, x := range xs {\n\t\t_ = x\n\t}\n\tfor {\n\t\tbreak\n\t}\n}\n",
1139 &mut next,
1140 &mut decisions,
1141 )
1142 .unwrap();
1143 assert!(
1144 go.manifest.branches.iter().all(|b| b.kind != "loop"),
1145 "{:?}",
1146 go.manifest.branches
1147 );
1148 assert_eq!(
1149 go.manifest.limitations.len(),
1150 2,
1151 "{:?}",
1152 go.manifest.limitations
1153 );
1154 assert_eq!(go.manifest.limitations[0]["kind"], "loop-without-condition");
1155 }
1156
1157 #[test]
1158 fn only_a_file_that_gained_a_probe_imports_the_runtime() {
1159 let out = rewritten("package main\nfunc f(a int) int {\n\treturn a\n}\n");
1162 assert!(
1163 out.contains(RUNTIME_ALIAS),
1164 "a function is itself an obligation:\n{out}"
1165 );
1166
1167 let mut next = 0;
1168 let mut decisions = 0;
1169 let bare = build_go_obligations(
1170 "t.go",
1171 "package main\n\ntype T struct{}\n",
1172 &mut next,
1173 &mut decisions,
1174 )
1175 .unwrap();
1176 assert!(bare.edits.is_empty(), "{:?}", bare.edits);
1177 assert_eq!(
1178 rewrite("package main\n\ntype T struct{}\n", &bare.edits),
1179 "package main\n\ntype T struct{}\n"
1180 );
1181 }
1182
1183 #[test]
1184 fn a_short_circuiting_operator_splits_a_decision_and_nothing_else_does() {
1185 let go = obligations(SAMPLE);
1189 let decision = go
1190 .manifest
1191 .decisions
1192 .iter()
1193 .find(|d| d.kind == "if")
1194 .expect("the if carries a decision");
1195 assert_eq!(decision.conditions, ["a > 10", "b"]);
1196
1197 let simple = obligations(
1200 "package main\nfunc f(a int) bool {\n\tif a > 1 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1201 );
1202 assert!(
1203 simple.manifest.decisions.is_empty(),
1204 "{:?}",
1205 simple.manifest.decisions
1206 );
1207 }
1208
1209 #[test]
1210 fn negation_and_parentheses_do_not_invent_conditions() {
1211 let go = obligations(
1215 "package main\nfunc f(a int, b bool, c bool) bool {\n\tif (a > 1 || !b) && c {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1216 );
1217 let decision = &go.manifest.decisions[0];
1218 assert_eq!(decision.conditions, ["a > 1", "!b", "c"]);
1219 }
1220
1221 #[test]
1222 fn every_branching_construct_records_the_outcome_that_was_not_taken() {
1223 let go = obligations(SAMPLE);
1227 let by_kind = |kind: &str| {
1228 go.manifest
1229 .branches
1230 .iter()
1231 .find(|b| b.kind == kind)
1232 .map(|b| {
1233 b.alternatives
1234 .iter()
1235 .map(|a| a.label.clone())
1236 .collect::<Vec<_>>()
1237 })
1238 };
1239 assert_eq!(by_kind("if").unwrap(), ["true", "false"]);
1240 assert_eq!(by_kind("loop").unwrap(), ["true", "false"]);
1244 let switch = by_kind("switch").unwrap();
1245 assert!(switch.contains(&"default".to_owned()), "{switch:?}");
1246 assert!(
1247 !switch.contains(&"no case matched".to_owned()),
1248 "a default already covers it"
1249 );
1250
1251 let open = obligations(
1253 "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1254 );
1255 let labels = open
1256 .manifest
1257 .branches
1258 .iter()
1259 .find(|b| b.kind == "switch")
1260 .unwrap()
1261 .alternatives
1262 .iter()
1263 .map(|a| a.label.clone())
1264 .collect::<Vec<_>>();
1265 assert!(labels.contains(&"no case matched".to_owned()), "{labels:?}");
1266 }
1267
1268 #[test]
1269 fn functions_and_statements_are_separate_obligations_with_locations() {
1270 let go = obligations(SAMPLE);
1271 let functions = go
1272 .manifest
1273 .points
1274 .iter()
1275 .filter(|p| p.kind == PointKind::Function)
1276 .collect::<Vec<_>>();
1277 assert_eq!(functions.len(), 1);
1278 assert_eq!(functions[0].label.as_deref(), Some("classify"));
1279 assert_eq!(functions[0].line, 5);
1280
1281 let statements = go
1282 .manifest
1283 .points
1284 .iter()
1285 .filter(|p| p.kind == PointKind::Statement)
1286 .count();
1287 assert!(
1288 statements >= 6,
1289 "expected the body's statements, got {statements}"
1290 );
1291 assert!(
1294 go.manifest
1295 .points
1296 .iter()
1297 .all(|p| p.line > 0 && p.column > 0)
1298 );
1299 assert!(
1301 !go.manifest
1302 .points
1303 .iter()
1304 .any(|p| p.source.starts_with("import"))
1305 );
1306 }
1307
1308 #[test]
1309 fn every_obligation_has_a_probe_and_malformed_source_is_refused() {
1310 let go = obligations(SAMPLE);
1311 let expected = go.manifest.points.len()
1312 + go.manifest
1313 .branches
1314 .iter()
1315 .map(|b| b.alternatives.len())
1316 .sum::<usize>();
1317 assert_eq!(go.probes.len(), expected);
1321 assert_eq!(go.decision_widths.len(), go.manifest.decisions.len());
1324
1325 let mut next = 0;
1328 let mut decisions = 0;
1329 assert!(matches!(
1330 build_go_obligations(
1331 "broken.go",
1332 "package main\nfunc f( {",
1333 &mut next,
1334 &mut decisions
1335 ),
1336 Err(GoInstrumenterError::Parse(_))
1337 ));
1338 }
1339
1340 #[test]
1341 fn go_1_26_new_of_a_value_is_measured_rather_than_dropped() {
1342 let go = obligations(
1351 "package p\n\nfunc Greeting() *string {\n\treturn new(\"hello\")\n}\n\nfunc Sum(a, b int) *int {\n\treturn new(a + b)\n}\n\nfunc Copy(v int) *int {\n\tif v > 0 {\n\t\treturn new(v)\n\t}\n\treturn new(0)\n}\n",
1352 );
1353 assert_eq!(
1354 go.manifest
1355 .points
1356 .iter()
1357 .filter(|p| p.kind == PointKind::Function)
1358 .count(),
1359 3,
1360 "every function in the file is an obligation: {:?}",
1361 go.manifest.points
1362 );
1363 assert_eq!(
1364 go.manifest.branches.len(),
1365 1,
1366 "the `if` beside it is still a branch with both arms to answer for"
1367 );
1368 assert!(
1373 go.manifest
1374 .points
1375 .iter()
1376 .any(|point| point.source.contains("new(\"hello\")")),
1377 "{:?}",
1378 go.manifest.points
1379 );
1380 assert!(
1381 !go.manifest
1382 .points
1383 .iter()
1384 .any(|point| point.source.contains("nEw")),
1385 "{:?}",
1386 go.manifest.points
1387 );
1388 }
1389
1390 #[test]
1391 fn new_of_a_type_is_left_exactly_as_it_was() {
1392 let go = obligations(
1397 "package p\n\nfunc Types() {\n\t_ = new(int)\n\t_ = new(map[string]int)\n\t_ = new([]byte)\n\t_ = new(chan int)\n\t_ = new(struct{ A int })\n\t_ = new(func(int) error)\n\t_ = new([3]int)\n\t_ = new(interface{})\n}\n",
1398 );
1399 assert_eq!(
1400 go.manifest
1401 .points
1402 .iter()
1403 .filter(|p| p.kind == PointKind::Function)
1404 .count(),
1405 1
1406 );
1407 let mixed = obligations(
1410 "package p\n\nfunc Mixed(v int) {\n\t_ = new(map[string][]chan int)\n\t_ = new(v)\n\t_ = new(\"x\")\n}\n",
1411 );
1412 assert_eq!(
1413 mixed
1414 .manifest
1415 .points
1416 .iter()
1417 .filter(|p| p.kind == PointKind::Function)
1418 .count(),
1419 1
1420 );
1421 }
1422
1423 #[test]
1424 fn a_method_called_new_is_not_the_builtin() {
1425 let go = obligations(
1430 "package p\n\ntype pool struct{}\n\nfunc (p pool) new(v int) *int {\n\treturn new(v + 1)\n}\n\nfunc use(p pool) *int {\n\treturn p.new(2)\n}\n",
1431 );
1432 assert!(
1433 go.manifest
1434 .points
1435 .iter()
1436 .any(|point| point.source.contains("p.new(2)")),
1437 "{:?}",
1438 go.manifest.points
1439 );
1440 }
1441
1442 #[test]
1443 fn a_file_with_runes_outside_ascii_is_scanned_without_splitting_one() {
1444 let go = obligations(
1451 "package p\n\n// Prüfen: gibt \u{e4}\u{f6}\u{fc} zur\u{fc}ck.\nfunc Gr\u{fc}\u{df}en() *string {\n\tsch\u{f6}n := \"gr\u{fc}\u{df}e \u{2014} \u{fc}ber alles\"\n\treturn new(sch\u{f6}n + \"!\")\n}\n",
1452 );
1453 assert_eq!(
1454 go.manifest
1455 .points
1456 .iter()
1457 .filter(|p| p.kind == PointKind::Function)
1458 .count(),
1459 1
1460 );
1461 }
1462
1463 #[test]
1464 fn the_scan_never_mistakes_text_for_code() {
1465 let go = obligations(concat!(
1471 "package p\n\n",
1472 "func f(v int) *int {\n",
1473 "\t_ = \"new(\\\"x\\\") and an unclosed ( paren\"\n",
1475 "\t_ = `raw new(\"y\") with ) and \\ backslash`\n",
1476 "\t_ = '('\n",
1477 "\t// new(\"comment\") and a stray )\n",
1478 "\t/* new(\"block\") ) */\n",
1479 "\treturn new(v + 1)\n}\n",
1481 ));
1482 assert_eq!(
1483 go.manifest
1484 .points
1485 .iter()
1486 .filter(|p| p.kind == PointKind::Function)
1487 .count(),
1488 1
1489 );
1490
1491 let untouched =
1494 obligations("package p\n\nfunc g() string {\n\treturn `new(\"only in text\")`\n}\n");
1495 assert!(
1496 untouched
1497 .manifest
1498 .points
1499 .iter()
1500 .any(|point| point.source.contains("new(\"only in text\")")),
1501 "{:?}",
1502 untouched.manifest.points
1503 );
1504 }
1505
1506 #[test]
1507 fn no_shape_of_input_can_make_the_scan_panic() {
1508 let awkward = concat!(
1515 "package p\n\n",
1516 "// gr\u{fc}\u{df}e \u{2014} new(\"in a comment\")\n",
1517 "func f(sch\u{f6}n int) *int {\n",
1518 "\t_ = \"\u{fc}ber new(\\\"in a string\\\"\"\n",
1519 "\t_ = `\u{e4}\u{f6}\u{fc} new(\n",
1520 "\t_ = '\u{fc}'\n",
1521 "\t/* new(\"unterminated\n",
1522 "\treturn new(sch\u{f6}n + 1)\n}\n",
1523 );
1524 for end in 0..=awkward.len() {
1525 if !awkward.is_char_boundary(end) {
1528 continue;
1529 }
1530 let _ = parse(&awkward[..end]);
1531 if awkward.is_char_boundary(end) {
1534 let _ = parse(&awkward[end..]);
1535 }
1536 }
1537
1538 for source in [
1541 "package p\n\nfunc f() {\n\t_ = new(\n",
1542 "package p\n\nfunc f() {\n\t_ = new",
1543 "package p\n\nfunc f() {\n\t_ = new(",
1544 "package p\n\nfunc f() {\n\t_ = new()\n}\n",
1545 "new",
1546 "new(",
1547 "",
1548 ] {
1549 let _ = parse(source);
1550 }
1551 }
1552
1553 #[test]
1554 fn the_scan_reads_new_the_way_go_spells_it() {
1555 let go = obligations(concat!(
1561 "package p\n\n",
1562 "type pool struct{}\n\n",
1563 "func (p pool) new(v int) *int {\n\treturn &v\n}\n\n",
1564 "func renew(v string) *string {\n\treturn &v\n}\n\n",
1565 "func f(p pool, v int) *int {\n",
1566 "\t_ = new (\"spaced\")\n",
1568 "\t_ = new\t(\"tabbed\")\n",
1569 "\t_ = p.new(2)\n",
1571 "\t_ = renew(\"x\")\n",
1573 "\tnewValue := v\n",
1575 "\treturn new(newValue)\n}\n",
1576 ));
1577 assert_eq!(
1578 go.manifest
1579 .points
1580 .iter()
1581 .filter(|p| p.kind == PointKind::Function)
1582 .count(),
1583 3,
1584 "{:?}",
1585 go.manifest.points
1586 );
1587 assert!(
1589 go.manifest
1590 .points
1591 .iter()
1592 .any(|point| point.source.contains("new (\"spaced\")")),
1593 "{:?}",
1594 go.manifest.points
1595 );
1596 assert!(
1597 !go.manifest
1598 .points
1599 .iter()
1600 .any(|point| point.source.contains("nEw")),
1601 "{:?}",
1602 go.manifest.points
1603 );
1604 }
1605
1606 #[test]
1607 fn a_file_that_is_simply_broken_still_does_not_parse() {
1608 let mut next = 0;
1612 let mut decisions = 0;
1613 assert!(matches!(
1614 build_go_obligations(
1615 "broken.go",
1616 "package p\n\nfunc f() {\n\t_ = new(1)\n\tif {\n}\n",
1617 &mut next,
1618 &mut decisions
1619 ),
1620 Err(GoInstrumenterError::Parse(_))
1621 ));
1622 }
1623
1624 #[test]
1625 fn decisions_are_numbered_across_the_module_not_within_a_file() {
1626 let mut next = 0;
1631 let mut decisions = 0;
1632 let first = build_go_obligations(
1633 "a.go",
1634 "package p\n\nfunc A(x, y bool) bool {\n\tif x && y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1635 &mut next,
1636 &mut decisions,
1637 )
1638 .unwrap();
1639 let second = build_go_obligations(
1640 "b.go",
1641 "package p\n\nfunc B(x, y bool) bool {\n\tif x || y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1642 &mut next,
1643 &mut decisions,
1644 )
1645 .unwrap();
1646
1647 let referenced = |obligations: &GoFileObligations| {
1648 obligations
1649 .edits
1650 .iter()
1651 .filter_map(|edit| {
1652 let at = edit.text.find(".C(")?;
1653 edit.text[at + 3..]
1654 .split(',')
1655 .next()?
1656 .trim()
1657 .parse::<u32>()
1658 .ok()
1659 })
1660 .collect::<std::collections::BTreeSet<_>>()
1661 };
1662 assert_eq!(referenced(&first), [0].into());
1663 assert_eq!(referenced(&second), [1].into());
1664 assert_eq!(decisions, 2, "the module numbered two decisions in all");
1665 assert_eq!(first.decision_widths, [2]);
1668 assert_eq!(second.decision_widths, [2]);
1669 }
1670}