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 ending_fallthrough(clause: Node) -> Option<Node> {
451 let body = clause
452 .children(&mut clause.walk())
453 .find(|child| child.kind() == "statement_list")?;
454 let last = body.named_child(body.named_child_count().checked_sub(1)?)?;
455 (last.kind() == "fallthrough_statement").then_some(last)
456}
457
458fn in_statement_position(node: Node) -> bool {
459 node.parent()
460 .is_some_and(|parent| parent.kind() == "statement_list")
461}
462
463fn loop_condition<'t>(node: Node<'t>) -> Option<Node<'t>> {
466 if let Some(condition) = node.child_by_field_name("condition") {
467 return Some(condition);
468 }
469 let mut cursor = node.walk();
470 let clause = node
471 .children(&mut cursor)
472 .find(|child| child.kind() == "for_clause")?;
473 let mut inner = clause.walk();
474 clause
475 .children(&mut inner)
476 .find(|child| child.is_named() && child.kind().ends_with("_expression"))
477}
478
479impl<'a> Collector<'a> {
480 fn id(&mut self, node: Node, kind: &str) -> String {
481 stable_obligation_id("go", self.file, kind, node.start_byte(), node.end_byte())
482 }
483
484 fn limitation_id(&mut self, node: Node, kind: &str) -> String {
489 self.id(node, kind)
490 }
491
492 fn probe(&mut self, target: GoProbeTarget, at: usize) -> u64 {
493 *self.next_probe += 1;
494 let id = *self.next_probe;
495 self.probes.insert(id, GoProbe { id, target, at });
496 id
497 }
498
499 fn edit(&mut self, at: usize, rank: i32, text: String) {
500 self.edits.push(GoEdit { at, rank, text });
501 }
502
503 fn call_before(&mut self, at: usize, call: String) {
506 self.edit(at, 100, format!("{call}; "));
507 }
508
509 fn position(&self, node: Node) -> (usize, usize) {
511 let start = node.start_position();
512 (start.row + 1, start.column + 1)
513 }
514
515 fn text(&self, node: Node) -> String {
516 let raw = &self.source[node.byte_range()];
517 let line = raw.lines().next().unwrap_or("");
518 line.trim().to_owned()
519 }
520
521 fn add_point(&mut self, node: Node, kind: PointKind, label: Option<String>) {
522 let (line, column) = self.position(node);
523 let id = self.id(
524 node,
525 match kind {
526 PointKind::Function => "function",
527 PointKind::Statement => "statement",
528 },
529 );
530 let target = match kind {
531 PointKind::Function => GoProbeTarget::Function { id: id.clone() },
532 PointKind::Statement => GoProbeTarget::Statement { id: id.clone() },
533 };
534 let at = match kind {
537 PointKind::Function => match node.child_by_field_name("body") {
538 Some(body) => body.start_byte() + 1,
539 None => return,
540 },
541 PointKind::Statement => node.start_byte(),
542 };
543 let probe = self.probe(target, at);
544 self.call_before(at, format!("{HITS_VARIABLE}[{probe}] = 2"));
548 self.points.push(PointMeta {
549 id,
550 kind,
551 file: self.file.to_owned(),
552 line,
553 column,
554 source: self.text(node),
555 label,
556 });
557 }
558
559 fn add_branch(&mut self, node: Node, kind: &str, labels: &[&str]) -> Vec<u64> {
560 let (line, column) = self.position(node);
561 let id = self.id(node, "branch");
562 let mut probes = Vec::new();
563 let alternatives = labels
564 .iter()
565 .map(|label| {
566 let alternative = format!("{id}.{label}");
567 probes.push(self.probe(
568 GoProbeTarget::Alternative {
569 branch: id.clone(),
570 alternative: alternative.clone(),
571 },
572 node.start_byte(),
573 ));
574 BranchAlternativeMeta {
575 id: alternative,
576 label: (*label).to_owned(),
577 }
578 })
579 .collect();
580 self.branches.push(BranchMeta {
581 id,
582 kind: kind.to_owned(),
583 file: self.file.to_owned(),
584 line,
585 column,
586 source: self.text(node),
587 alternatives,
588 });
589 probes
590 }
591
592 fn add_decision(&mut self, node: Node, kind: &str) -> Option<usize> {
598 let mut leaves = Vec::new();
599 condition_nodes(node, self.source, &mut leaves);
600 if leaves.len() < 2 {
601 return None;
602 }
603 let conditions = leaves
604 .iter()
605 .map(|leaf| self.source[leaf.byte_range()].trim().to_owned())
606 .collect::<Vec<_>>();
607 let (line, column) = self.position(node);
608 let id = self.id(node, "decision");
609 let index_of_decision = self.decision_base as usize + self.widths.len();
613 self.widths.push(leaves.len().min(64) as u8);
614 let alias = self.alias.to_owned();
615 for (index, leaf) in leaves.iter().enumerate() {
620 self.edit(
624 leaf.start_byte(),
625 20,
626 format!("{alias}.C({index_of_decision}, {index}, "),
627 );
628 self.edit(leaf.end_byte(), 20, ")".to_owned());
629 }
630 self.decisions.push(DecisionMeta {
631 id,
632 file: self.file.to_owned(),
633 line,
634 column,
635 source: self.text(node),
636 conditions,
637 kind: kind.to_owned(),
638 });
639 Some(index_of_decision)
640 }
641
642 fn walk(&mut self, node: Node) {
643 let alias = self.alias.to_owned();
644 match node.kind() {
645 "function_declaration" | "method_declaration" | "func_literal" => {
646 let label = node
647 .child_by_field_name("name")
648 .map(|name| self.source[name.byte_range()].to_owned());
649 self.add_point(node, PointKind::Function, label);
650 }
651 "if_statement" => {
652 if let Some(condition) = node.child_by_field_name("condition") {
653 let probes = self.add_branch(node, "if", &["true", "false"]);
658 let decision = self.add_decision(condition, "if");
659 self.edit(
660 condition.start_byte(),
661 5,
662 branch_wrapper(&alias, probes[0], probes[1], decision),
663 );
664 self.edit(condition.end_byte(), 5, ")".to_owned());
665 }
666 }
667 "for_statement" => {
668 match loop_condition(node) {
673 Some(condition) => {
674 let probes = self.add_branch(node, "loop", &["true", "false"]);
675 let decision = self.add_decision(condition, "loop");
676 self.edit(
677 condition.start_byte(),
678 5,
679 branch_wrapper(&alias, probes[0], probes[1], decision),
680 );
681 self.edit(condition.end_byte(), 5, ")".to_owned());
682 }
683 None => {
684 let (line, column) = self.position(node);
685 let limitation = self.limitation_id(node, "loop-without-condition");
686 self.limitations.push(serde_json::json!({
687 "id": limitation,
688 "kind": "loop-without-condition",
689 "file": self.file,
690 "source": self.text(node),
691 "line": line,
692 "column": column,
693 "reason": "a range or unconditional loop has no condition to observe, so no branch obligation is recorded for it",
694 }));
695 }
696 }
697 }
698 "expression_switch_statement" | "type_switch_statement" | "select_statement" => {
699 let kind = match node.kind() {
700 "expression_switch_statement" => "switch",
701 "type_switch_statement" => "type-switch",
702 _ => "select",
703 };
704 let mut cases = Vec::new();
705 let mut has_default = false;
706 let mut cursor = node.walk();
707 for child in node.children(&mut cursor) {
708 match child.kind() {
709 "expression_case" | "type_case" | "communication_case" => {
710 cases.push((self.text(child), Some(child)))
711 }
712 "default_case" => {
713 has_default = true;
714 cases.push(("default".to_owned(), Some(child)));
715 }
716 _ => {}
717 }
718 }
719 if !has_default && node.kind() != "select_statement" {
734 cases.push(("no case matched".to_owned(), None));
735 }
736 let labels = cases
737 .iter()
738 .map(|(label, _)| label.as_str())
739 .collect::<Vec<_>>();
740 let probes = self.add_branch(node, kind, &labels);
741 let fallthroughs = cases
748 .iter()
749 .map(|(_, clause)| clause.and_then(|clause| ending_fallthrough(clause)))
750 .collect::<Vec<_>>();
751 let flag = format!("_scvFell{}", node.start_byte());
752 if fallthroughs.iter().any(Option::is_some) {
753 let statement = node
755 .parent()
756 .filter(|parent| parent.kind() == "labeled_statement")
757 .unwrap_or(node);
758 self.edit(statement.start_byte(), 90, format!("var {flag} bool; "));
759 for fallthrough in fallthroughs.iter().flatten() {
760 self.call_before(fallthrough.start_byte(), format!("{flag} = true"));
761 }
762 }
763 for (index, (probe, (_, clause))) in probes.iter().zip(cases.iter()).enumerate() {
764 match clause {
765 Some(clause) => {
766 let at = clause
767 .children(&mut clause.walk())
768 .find(|child| child.kind() == "statement_list")
769 .map(|body| body.start_byte())
770 .unwrap_or_else(|| clause.end_byte());
771 if index > 0 && fallthroughs[index - 1].is_some() {
772 self.edit(
773 at,
774 100,
775 format!("if {flag} {{ {flag} = false }} else {{ {alias}.A({probe}) }}; "),
776 );
777 } else {
778 self.call_before(at, format!("{alias}.A({probe})"));
779 }
780 }
781 None => {
782 let at = node.end_byte().saturating_sub(1);
784 self.edit(at, 100, format!("\ndefault:\n{alias}.A({probe})\n"));
785 }
786 }
787 }
788 }
789 kind if is_statement(kind) && in_statement_position(node) => {
790 self.add_point(node, PointKind::Statement, None);
791 }
792 _ => {}
793 }
794 if in_statement_position(node)
797 && matches!(
798 node.kind(),
799 "if_statement"
800 | "for_statement"
801 | "expression_switch_statement"
802 | "type_switch_statement"
803 | "select_statement"
804 )
805 {
806 self.add_point(node, PointKind::Statement, None);
807 }
808 let mut cursor = node.walk();
809 for child in node.children(&mut cursor) {
810 if child.is_named() {
811 self.walk(child);
812 }
813 }
814 }
815}
816
817fn condition_nodes<'t>(node: Node<'t>, source: &str, out: &mut Vec<Node<'t>>) {
823 match node.kind() {
824 "binary_expression" => {
825 let operator = node
826 .child_by_field_name("operator")
827 .map(|op| &source[op.byte_range()])
828 .unwrap_or("");
829 if operator == "&&" || operator == "||" {
830 if let Some(left) = node.child_by_field_name("left") {
831 condition_nodes(left, source, out);
832 }
833 if let Some(right) = node.child_by_field_name("right") {
834 condition_nodes(right, source, out);
835 }
836 return;
837 }
838 out.push(node);
839 }
840 "parenthesized_expression" => {
841 let mut cursor = node.walk();
842 match node.children(&mut cursor).find(|child| child.is_named()) {
843 Some(inner) => condition_nodes(inner, source, out),
844 None => out.push(node),
845 }
846 }
847 _ => out.push(node),
848 }
849}
850
851#[derive(Debug, Clone, PartialEq, Eq)]
858pub struct GoEdit {
859 pub at: usize,
860 pub rank: i32,
861 pub text: String,
862}
863
864pub fn rewrite(source: &str, edits: &[GoEdit]) -> String {
866 let mut ordered = edits.to_vec();
867 ordered.sort_by(|a, b| b.at.cmp(&a.at).then(b.rank.cmp(&a.rank)));
868 let mut out = source.to_owned();
869 for edit in ordered {
870 if edit.at > out.len() {
871 continue;
872 }
873 out.insert_str(edit.at, &edit.text);
874 }
875 out
876}
877
878pub fn import_edit(source: &str, alias: &str, path: &str) -> Option<GoEdit> {
882 let tree = parse(source).ok()?;
883 let mut cursor = tree.root_node().walk();
884 let package = tree
885 .root_node()
886 .children(&mut cursor)
887 .find(|child| child.kind() == "package_clause")?;
888 Some(GoEdit {
889 at: package.end_byte(),
890 rank: 0,
891 text: format!("\nimport {alias} \"{path}\""),
892 })
893}
894
895pub fn build_go_obligations(
896 file: &str,
897 source: &str,
898 next_probe: &mut u64,
899 next_decision: &mut u32,
900) -> Result<GoFileObligations, GoInstrumenterError> {
901 build_go_obligations_with_alias(file, source, next_probe, next_decision, RUNTIME_ALIAS)
902}
903
904pub const RUNTIME_ALIAS: &str = "__supercov";
907
908pub const HITS_VARIABLE: &str = "__supercovHits";
911
912pub const RUNTIME_IMPORT: &str = "github.com/supercorp-ai/supercov/runtime/go/supercov";
914
915pub fn build_go_obligations_with_alias(
916 file: &str,
917 source: &str,
918 next_probe: &mut u64,
919 next_decision: &mut u32,
920 alias: &str,
921) -> Result<GoFileObligations, GoInstrumenterError> {
922 let tree = parse(source)?;
923 let decision_base = *next_decision;
924 let mut collector = Collector {
925 file,
926 source,
927 alias,
928 next_probe,
929 decision_base,
930 edits: Vec::new(),
931 points: Vec::new(),
932 branches: Vec::new(),
933 decisions: Vec::new(),
934 probes: BTreeMap::new(),
935 limitations: Vec::new(),
936 widths: Vec::new(),
937 };
938 let mut cursor = tree.root_node().walk();
939 for child in tree.root_node().children(&mut cursor) {
940 if child.is_named() {
941 collector.walk(child);
942 }
943 }
944 *next_decision += collector.widths.len() as u32;
945 let mut edits = collector.edits;
946 let calls_runtime = edits
950 .iter()
951 .any(|edit| edit.text.contains(&format!("{alias}.")));
952 if calls_runtime && let Some(import) = import_edit(source, alias, RUNTIME_IMPORT) {
953 edits.push(import);
954 }
955 Ok(GoFileObligations {
956 manifest: CoverageManifest {
957 decisions: collector.decisions,
958 points: collector.points,
959 branches: collector.branches,
960 limitations: collector.limitations,
961 unmeasured: Vec::new(),
962 scope: None,
963 assertion_sites: Vec::new(),
964 },
965 probes: collector.probes,
966 edits,
967 decision_widths: collector.widths,
968 })
969}
970
971#[cfg(test)]
972mod tests {
973 use super::*;
974
975 fn assert_indexable(limitations: &[serde_json::Value]) -> Vec<String> {
984 assert!(!limitations.is_empty(), "nothing to check");
985 for limitation in limitations {
986 for field in ["id", "kind", "file", "source", "reason"] {
987 assert!(
988 limitation.get(field).and_then(|v| v.as_str()).is_some(),
989 "a limitation needs a string {field}: {limitation}"
990 );
991 }
992 for field in ["line", "column"] {
993 assert!(
994 limitation.get(field).and_then(|v| v.as_u64()).is_some(),
995 "a limitation needs a number {field}: {limitation}"
996 );
997 }
998 }
999 let mut kinds = limitations
1000 .iter()
1001 .filter_map(|limitation| limitation["kind"].as_str().map(str::to_owned))
1002 .collect::<Vec<_>>();
1003 kinds.sort();
1004 kinds.dedup();
1005 kinds
1006 }
1007
1008 #[test]
1010 fn a_file_that_does_not_parse_says_where() {
1011 let broken = "package main\n\nfunc f() int {\n\treturn 1 )\n}\n";
1012 let message = parse(broken).expect_err("does not parse").to_string();
1013 assert!(message.contains("line 4"), "{message}");
1014 assert!(message.contains("return 1"), "{message}");
1015 assert!(
1016 !message.contains("byte"),
1017 "an offset is not somewhere a reader can look: {message}"
1018 );
1019 }
1020
1021 #[test]
1022 fn every_limitation_carries_what_the_index_stores() {
1023 const EVERY: &str = r#"package main
1024
1025func walk(items []int) int {
1026 sum := 0
1027 for _, item := range items {
1028 sum += item
1029 }
1030 for {
1031 break
1032 }
1033 return sum
1034}
1035"#;
1036 let mut next = 0;
1037 let mut decisions = 0;
1038 let obligations =
1039 build_go_obligations("walk.go", EVERY, &mut next, &mut decisions).expect("go");
1040 assert_eq!(
1041 assert_indexable(&obligations.manifest.limitations),
1042 ["loop-without-condition"]
1043 );
1044 }
1045
1046 const SAMPLE: &str = r#"package main
1047
1048import "fmt"
1049
1050func classify(a int, b bool) string {
1051 if a > 10 && b {
1052 return "big"
1053 }
1054 for i := 0; i < a; i++ {
1055 fmt.Println(i)
1056 }
1057 switch {
1058 case a == 0:
1059 return "zero"
1060 default:
1061 return "small"
1062 }
1063}
1064"#;
1065
1066 fn obligations(source: &str) -> GoFileObligations {
1067 let mut next = 0;
1068 let mut decisions = 0;
1069 build_go_obligations("main.go", source, &mut next, &mut decisions).expect("obligations")
1070 }
1071
1072 fn rewritten(source: &str) -> String {
1075 let mut next = 0;
1076 let mut decisions = 0;
1077 let go =
1078 build_go_obligations("x.go", source, &mut next, &mut decisions).expect("obligations");
1079 let out = rewrite(source, &go.edits);
1080 parse(&out)
1081 .unwrap_or_else(|error| panic!("rewritten source does not parse: {error}\n{out}"));
1082 out
1083 }
1084
1085 #[test]
1086 fn a_probe_never_lands_where_go_does_not_allow_a_statement() {
1087 let out = rewritten(
1091 "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",
1092 );
1093 assert!(
1094 !out.contains("for __supercov"),
1095 "probe in a for-clause init:\n{out}"
1096 );
1097 assert!(
1098 !out.contains("; __supercov.P"),
1099 "probe in a for-clause post:\n{out}"
1100 );
1101 assert!(
1102 out.contains("if b := a;"),
1103 "the if initialiser survived intact:\n{out}"
1104 );
1105 assert!(
1106 out.contains("switch c := a;"),
1107 "the switch initialiser survived intact:\n{out}"
1108 );
1109 }
1110
1111 #[test]
1112 fn wrapping_a_condition_preserves_short_circuit_order() {
1113 let out = rewritten(
1118 "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",
1119 );
1120 let condition = out
1121 .lines()
1122 .find(|line| line.contains("if "))
1123 .expect("the if survived");
1124 let branch = condition.find(".BD(").expect("branch and decision wrapper");
1125 let first = condition.find(".C(").expect("first condition wrapper");
1126 assert!(
1127 branch < first,
1128 "the branch must enclose its conditions: {condition}"
1129 );
1130 assert_eq!(
1131 condition.matches(".C(").count(),
1132 2,
1133 "one wrapper per condition: {condition}"
1134 );
1135 assert!(
1136 condition.contains("&&"),
1137 "the operator itself is untouched: {condition}"
1138 );
1139 }
1140
1141 #[test]
1142 fn a_single_condition_branch_uses_the_wrapper_that_inlines() {
1143 let out = rewritten(
1147 "package main\nfunc f(a int) bool {\n\tif a > 10 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1148 );
1149 assert!(out.contains(".B("), "{out}");
1150 assert!(!out.contains(".BD("), "no decision here to close:\n{out}");
1151 }
1152
1153 #[test]
1154 fn a_switch_without_a_default_gains_one_so_matching_nothing_is_observable() {
1155 let out = rewritten(
1158 "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1159 );
1160 assert!(out.contains("default:"), "{out}");
1161
1162 let existing = rewritten(
1164 "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\tdefault:\n\t\treturn\n\t}\n}\n",
1165 );
1166 assert_eq!(existing.matches("default:").count(), 1, "{existing}");
1167 }
1168
1169 #[test]
1170 fn a_loop_with_no_condition_records_a_limitation_not_an_obligation() {
1171 let mut next = 0;
1175 let mut decisions = 0;
1176 let go = build_go_obligations(
1177 "x.go",
1178 "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",
1179 &mut next,
1180 &mut decisions,
1181 )
1182 .unwrap();
1183 assert!(
1184 go.manifest.branches.iter().all(|b| b.kind != "loop"),
1185 "{:?}",
1186 go.manifest.branches
1187 );
1188 assert_eq!(
1189 go.manifest.limitations.len(),
1190 2,
1191 "{:?}",
1192 go.manifest.limitations
1193 );
1194 assert_eq!(go.manifest.limitations[0]["kind"], "loop-without-condition");
1195 }
1196
1197 #[test]
1198 fn only_a_file_that_gained_a_probe_imports_the_runtime() {
1199 let out = rewritten("package main\nfunc f(a int) int {\n\treturn a\n}\n");
1202 assert!(
1203 out.contains(RUNTIME_ALIAS),
1204 "a function is itself an obligation:\n{out}"
1205 );
1206
1207 let mut next = 0;
1208 let mut decisions = 0;
1209 let bare = build_go_obligations(
1210 "t.go",
1211 "package main\n\ntype T struct{}\n",
1212 &mut next,
1213 &mut decisions,
1214 )
1215 .unwrap();
1216 assert!(bare.edits.is_empty(), "{:?}", bare.edits);
1217 assert_eq!(
1218 rewrite("package main\n\ntype T struct{}\n", &bare.edits),
1219 "package main\n\ntype T struct{}\n"
1220 );
1221 }
1222
1223 #[test]
1224 fn a_short_circuiting_operator_splits_a_decision_and_nothing_else_does() {
1225 let go = obligations(SAMPLE);
1229 let decision = go
1230 .manifest
1231 .decisions
1232 .iter()
1233 .find(|d| d.kind == "if")
1234 .expect("the if carries a decision");
1235 assert_eq!(decision.conditions, ["a > 10", "b"]);
1236
1237 let simple = obligations(
1240 "package main\nfunc f(a int) bool {\n\tif a > 1 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1241 );
1242 assert!(
1243 simple.manifest.decisions.is_empty(),
1244 "{:?}",
1245 simple.manifest.decisions
1246 );
1247 }
1248
1249 #[test]
1250 fn negation_and_parentheses_do_not_invent_conditions() {
1251 let go = obligations(
1255 "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",
1256 );
1257 let decision = &go.manifest.decisions[0];
1258 assert_eq!(decision.conditions, ["a > 1", "!b", "c"]);
1259 }
1260
1261 #[test]
1262 fn every_branching_construct_records_the_outcome_that_was_not_taken() {
1263 let go = obligations(SAMPLE);
1267 let by_kind = |kind: &str| {
1268 go.manifest
1269 .branches
1270 .iter()
1271 .find(|b| b.kind == kind)
1272 .map(|b| {
1273 b.alternatives
1274 .iter()
1275 .map(|a| a.label.clone())
1276 .collect::<Vec<_>>()
1277 })
1278 };
1279 assert_eq!(by_kind("if").unwrap(), ["true", "false"]);
1280 assert_eq!(by_kind("loop").unwrap(), ["true", "false"]);
1284 let switch = by_kind("switch").unwrap();
1285 assert!(switch.contains(&"default".to_owned()), "{switch:?}");
1286 assert!(
1287 !switch.contains(&"no case matched".to_owned()),
1288 "a default already covers it"
1289 );
1290
1291 let open = obligations(
1293 "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1294 );
1295 let labels = open
1296 .manifest
1297 .branches
1298 .iter()
1299 .find(|b| b.kind == "switch")
1300 .unwrap()
1301 .alternatives
1302 .iter()
1303 .map(|a| a.label.clone())
1304 .collect::<Vec<_>>();
1305 assert!(labels.contains(&"no case matched".to_owned()), "{labels:?}");
1306 }
1307
1308 #[test]
1309 fn functions_and_statements_are_separate_obligations_with_locations() {
1310 let go = obligations(SAMPLE);
1311 let functions = go
1312 .manifest
1313 .points
1314 .iter()
1315 .filter(|p| p.kind == PointKind::Function)
1316 .collect::<Vec<_>>();
1317 assert_eq!(functions.len(), 1);
1318 assert_eq!(functions[0].label.as_deref(), Some("classify"));
1319 assert_eq!(functions[0].line, 5);
1320
1321 let statements = go
1322 .manifest
1323 .points
1324 .iter()
1325 .filter(|p| p.kind == PointKind::Statement)
1326 .count();
1327 assert!(
1328 statements >= 6,
1329 "expected the body's statements, got {statements}"
1330 );
1331 assert!(
1334 go.manifest
1335 .points
1336 .iter()
1337 .all(|p| p.line > 0 && p.column > 0)
1338 );
1339 assert!(
1341 !go.manifest
1342 .points
1343 .iter()
1344 .any(|p| p.source.starts_with("import"))
1345 );
1346 }
1347
1348 #[test]
1349 fn every_obligation_has_a_probe_and_malformed_source_is_refused() {
1350 let go = obligations(SAMPLE);
1351 let expected = go.manifest.points.len()
1352 + go.manifest
1353 .branches
1354 .iter()
1355 .map(|b| b.alternatives.len())
1356 .sum::<usize>();
1357 assert_eq!(go.probes.len(), expected);
1361 assert_eq!(go.decision_widths.len(), go.manifest.decisions.len());
1364
1365 let mut next = 0;
1368 let mut decisions = 0;
1369 assert!(matches!(
1370 build_go_obligations(
1371 "broken.go",
1372 "package main\nfunc f( {",
1373 &mut next,
1374 &mut decisions
1375 ),
1376 Err(GoInstrumenterError::Parse(_))
1377 ));
1378 }
1379
1380 #[test]
1381 fn go_1_26_new_of_a_value_is_measured_rather_than_dropped() {
1382 let go = obligations(
1391 "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",
1392 );
1393 assert_eq!(
1394 go.manifest
1395 .points
1396 .iter()
1397 .filter(|p| p.kind == PointKind::Function)
1398 .count(),
1399 3,
1400 "every function in the file is an obligation: {:?}",
1401 go.manifest.points
1402 );
1403 assert_eq!(
1404 go.manifest.branches.len(),
1405 1,
1406 "the `if` beside it is still a branch with both arms to answer for"
1407 );
1408 assert!(
1413 go.manifest
1414 .points
1415 .iter()
1416 .any(|point| point.source.contains("new(\"hello\")")),
1417 "{:?}",
1418 go.manifest.points
1419 );
1420 assert!(
1421 !go.manifest
1422 .points
1423 .iter()
1424 .any(|point| point.source.contains("nEw")),
1425 "{:?}",
1426 go.manifest.points
1427 );
1428 }
1429
1430 #[test]
1431 fn new_of_a_type_is_left_exactly_as_it_was() {
1432 let go = obligations(
1437 "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",
1438 );
1439 assert_eq!(
1440 go.manifest
1441 .points
1442 .iter()
1443 .filter(|p| p.kind == PointKind::Function)
1444 .count(),
1445 1
1446 );
1447 let mixed = obligations(
1450 "package p\n\nfunc Mixed(v int) {\n\t_ = new(map[string][]chan int)\n\t_ = new(v)\n\t_ = new(\"x\")\n}\n",
1451 );
1452 assert_eq!(
1453 mixed
1454 .manifest
1455 .points
1456 .iter()
1457 .filter(|p| p.kind == PointKind::Function)
1458 .count(),
1459 1
1460 );
1461 }
1462
1463 #[test]
1464 fn a_method_called_new_is_not_the_builtin() {
1465 let go = obligations(
1470 "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",
1471 );
1472 assert!(
1473 go.manifest
1474 .points
1475 .iter()
1476 .any(|point| point.source.contains("p.new(2)")),
1477 "{:?}",
1478 go.manifest.points
1479 );
1480 }
1481
1482 #[test]
1483 fn a_file_with_runes_outside_ascii_is_scanned_without_splitting_one() {
1484 let go = obligations(
1491 "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",
1492 );
1493 assert_eq!(
1494 go.manifest
1495 .points
1496 .iter()
1497 .filter(|p| p.kind == PointKind::Function)
1498 .count(),
1499 1
1500 );
1501 }
1502
1503 #[test]
1504 fn the_scan_never_mistakes_text_for_code() {
1505 let go = obligations(concat!(
1511 "package p\n\n",
1512 "func f(v int) *int {\n",
1513 "\t_ = \"new(\\\"x\\\") and an unclosed ( paren\"\n",
1515 "\t_ = `raw new(\"y\") with ) and \\ backslash`\n",
1516 "\t_ = '('\n",
1517 "\t// new(\"comment\") and a stray )\n",
1518 "\t/* new(\"block\") ) */\n",
1519 "\treturn new(v + 1)\n}\n",
1521 ));
1522 assert_eq!(
1523 go.manifest
1524 .points
1525 .iter()
1526 .filter(|p| p.kind == PointKind::Function)
1527 .count(),
1528 1
1529 );
1530
1531 let untouched =
1534 obligations("package p\n\nfunc g() string {\n\treturn `new(\"only in text\")`\n}\n");
1535 assert!(
1536 untouched
1537 .manifest
1538 .points
1539 .iter()
1540 .any(|point| point.source.contains("new(\"only in text\")")),
1541 "{:?}",
1542 untouched.manifest.points
1543 );
1544 }
1545
1546 #[test]
1547 fn no_shape_of_input_can_make_the_scan_panic() {
1548 let awkward = concat!(
1555 "package p\n\n",
1556 "// gr\u{fc}\u{df}e \u{2014} new(\"in a comment\")\n",
1557 "func f(sch\u{f6}n int) *int {\n",
1558 "\t_ = \"\u{fc}ber new(\\\"in a string\\\"\"\n",
1559 "\t_ = `\u{e4}\u{f6}\u{fc} new(\n",
1560 "\t_ = '\u{fc}'\n",
1561 "\t/* new(\"unterminated\n",
1562 "\treturn new(sch\u{f6}n + 1)\n}\n",
1563 );
1564 for end in 0..=awkward.len() {
1565 if !awkward.is_char_boundary(end) {
1568 continue;
1569 }
1570 let _ = parse(&awkward[..end]);
1571 if awkward.is_char_boundary(end) {
1574 let _ = parse(&awkward[end..]);
1575 }
1576 }
1577
1578 for source in [
1581 "package p\n\nfunc f() {\n\t_ = new(\n",
1582 "package p\n\nfunc f() {\n\t_ = new",
1583 "package p\n\nfunc f() {\n\t_ = new(",
1584 "package p\n\nfunc f() {\n\t_ = new()\n}\n",
1585 "new",
1586 "new(",
1587 "",
1588 ] {
1589 let _ = parse(source);
1590 }
1591 }
1592
1593 #[test]
1594 fn the_scan_reads_new_the_way_go_spells_it() {
1595 let go = obligations(concat!(
1601 "package p\n\n",
1602 "type pool struct{}\n\n",
1603 "func (p pool) new(v int) *int {\n\treturn &v\n}\n\n",
1604 "func renew(v string) *string {\n\treturn &v\n}\n\n",
1605 "func f(p pool, v int) *int {\n",
1606 "\t_ = new (\"spaced\")\n",
1608 "\t_ = new\t(\"tabbed\")\n",
1609 "\t_ = p.new(2)\n",
1611 "\t_ = renew(\"x\")\n",
1613 "\tnewValue := v\n",
1615 "\treturn new(newValue)\n}\n",
1616 ));
1617 assert_eq!(
1618 go.manifest
1619 .points
1620 .iter()
1621 .filter(|p| p.kind == PointKind::Function)
1622 .count(),
1623 3,
1624 "{:?}",
1625 go.manifest.points
1626 );
1627 assert!(
1629 go.manifest
1630 .points
1631 .iter()
1632 .any(|point| point.source.contains("new (\"spaced\")")),
1633 "{:?}",
1634 go.manifest.points
1635 );
1636 assert!(
1637 !go.manifest
1638 .points
1639 .iter()
1640 .any(|point| point.source.contains("nEw")),
1641 "{:?}",
1642 go.manifest.points
1643 );
1644 }
1645
1646 #[test]
1647 fn a_file_that_is_simply_broken_still_does_not_parse() {
1648 let mut next = 0;
1652 let mut decisions = 0;
1653 assert!(matches!(
1654 build_go_obligations(
1655 "broken.go",
1656 "package p\n\nfunc f() {\n\t_ = new(1)\n\tif {\n}\n",
1657 &mut next,
1658 &mut decisions
1659 ),
1660 Err(GoInstrumenterError::Parse(_))
1661 ));
1662 }
1663
1664 #[test]
1665 fn decisions_are_numbered_across_the_module_not_within_a_file() {
1666 let mut next = 0;
1671 let mut decisions = 0;
1672 let first = build_go_obligations(
1673 "a.go",
1674 "package p\n\nfunc A(x, y bool) bool {\n\tif x && y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1675 &mut next,
1676 &mut decisions,
1677 )
1678 .unwrap();
1679 let second = build_go_obligations(
1680 "b.go",
1681 "package p\n\nfunc B(x, y bool) bool {\n\tif x || y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1682 &mut next,
1683 &mut decisions,
1684 )
1685 .unwrap();
1686
1687 let referenced = |obligations: &GoFileObligations| {
1688 obligations
1689 .edits
1690 .iter()
1691 .filter_map(|edit| {
1692 let at = edit.text.find(".C(")?;
1693 edit.text[at + 3..]
1694 .split(',')
1695 .next()?
1696 .trim()
1697 .parse::<u32>()
1698 .ok()
1699 })
1700 .collect::<std::collections::BTreeSet<_>>()
1701 };
1702 assert_eq!(referenced(&first), [0].into());
1703 assert_eq!(referenced(&second), [1].into());
1704 assert_eq!(decisions, 2, "the module numbered two decisions in all");
1705 assert_eq!(first.decision_widths, [2]);
1708 assert_eq!(second.decision_widths, [2]);
1709 }
1710}