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);
790 let after_a_clause = cases.iter().any(|(_, clause)| clause.is_some());
791 self.edit(
792 at,
793 100,
794 format!(
795 "{}default: {alias}.A({probe}) ",
796 if after_a_clause { "; " } else { "" }
797 ),
798 );
799 }
800 }
801 }
802 }
803 kind if is_statement(kind) && in_statement_position(node) => {
804 self.add_point(node, PointKind::Statement, None);
805 }
806 _ => {}
807 }
808 if in_statement_position(node)
811 && matches!(
812 node.kind(),
813 "if_statement"
814 | "for_statement"
815 | "expression_switch_statement"
816 | "type_switch_statement"
817 | "select_statement"
818 )
819 {
820 self.add_point(node, PointKind::Statement, None);
821 }
822 let mut cursor = node.walk();
823 for child in node.children(&mut cursor) {
824 if child.is_named() {
825 self.walk(child);
826 }
827 }
828 }
829}
830
831fn condition_nodes<'t>(node: Node<'t>, source: &str, out: &mut Vec<Node<'t>>) {
837 match node.kind() {
838 "binary_expression" => {
839 let operator = node
840 .child_by_field_name("operator")
841 .map(|op| &source[op.byte_range()])
842 .unwrap_or("");
843 if operator == "&&" || operator == "||" {
844 if let Some(left) = node.child_by_field_name("left") {
845 condition_nodes(left, source, out);
846 }
847 if let Some(right) = node.child_by_field_name("right") {
848 condition_nodes(right, source, out);
849 }
850 return;
851 }
852 out.push(node);
853 }
854 "parenthesized_expression" => {
855 let mut cursor = node.walk();
856 match node.children(&mut cursor).find(|child| child.is_named()) {
857 Some(inner) => condition_nodes(inner, source, out),
858 None => out.push(node),
859 }
860 }
861 _ => out.push(node),
862 }
863}
864
865#[derive(Debug, Clone, PartialEq, Eq)]
872pub struct GoEdit {
873 pub at: usize,
874 pub rank: i32,
875 pub text: String,
876}
877
878pub fn rewrite(source: &str, edits: &[GoEdit]) -> String {
886 let mut ordered = edits.to_vec();
887 ordered.sort_by(|a, b| b.at.cmp(&a.at).then(b.rank.cmp(&a.rank)));
888 let lines = std::iter::once(0)
889 .chain(source.match_indices('\n').map(|(at, _)| at + 1))
890 .collect::<Vec<_>>();
891 let mut out = source.to_owned();
892 let mut placed = None;
893 for edit in ordered {
894 if edit.at > source.len() {
895 continue;
896 }
897 if placed != Some(edit.at) {
898 placed = Some(edit.at);
899 let line = lines.partition_point(|start| *start <= edit.at);
900 let column = edit.at - lines[line - 1] + 1;
901 out.insert_str(edit.at, &format!("/*line :{line}:{column}*/"));
902 }
903 out.insert_str(edit.at, &edit.text);
904 }
905 out
906}
907
908pub fn import_edit(source: &str, alias: &str, path: &str) -> Option<GoEdit> {
914 let tree = parse(source).ok()?;
915 let mut cursor = tree.root_node().walk();
916 let package = tree
917 .root_node()
918 .children(&mut cursor)
919 .find(|child| child.kind() == "package_clause")?;
920 Some(GoEdit {
921 at: package.end_byte(),
922 rank: 0,
923 text: format!("; import {alias} \"{path}\""),
924 })
925}
926
927pub fn build_go_obligations(
928 file: &str,
929 source: &str,
930 next_probe: &mut u64,
931 next_decision: &mut u32,
932) -> Result<GoFileObligations, GoInstrumenterError> {
933 build_go_obligations_with_alias(file, source, next_probe, next_decision, RUNTIME_ALIAS)
934}
935
936pub const RUNTIME_ALIAS: &str = "__supercov";
939
940pub const HITS_VARIABLE: &str = "__supercovHits";
943
944pub const RUNTIME_IMPORT: &str = "github.com/supercorp-ai/supercov/runtime/go/supercov";
946
947pub fn build_go_obligations_with_alias(
948 file: &str,
949 source: &str,
950 next_probe: &mut u64,
951 next_decision: &mut u32,
952 alias: &str,
953) -> Result<GoFileObligations, GoInstrumenterError> {
954 let tree = parse(source)?;
955 let decision_base = *next_decision;
956 let mut collector = Collector {
957 file,
958 source,
959 alias,
960 next_probe,
961 decision_base,
962 edits: Vec::new(),
963 points: Vec::new(),
964 branches: Vec::new(),
965 decisions: Vec::new(),
966 probes: BTreeMap::new(),
967 limitations: Vec::new(),
968 widths: Vec::new(),
969 };
970 let mut cursor = tree.root_node().walk();
971 for child in tree.root_node().children(&mut cursor) {
972 if child.is_named() {
973 collector.walk(child);
974 }
975 }
976 *next_decision += collector.widths.len() as u32;
977 let mut edits = collector.edits;
978 let calls_runtime = edits
982 .iter()
983 .any(|edit| edit.text.contains(&format!("{alias}.")));
984 if calls_runtime && let Some(import) = import_edit(source, alias, RUNTIME_IMPORT) {
985 edits.push(import);
986 }
987 Ok(GoFileObligations {
988 manifest: CoverageManifest {
989 decisions: collector.decisions,
990 points: collector.points,
991 branches: collector.branches,
992 limitations: collector.limitations,
993 unmeasured: Vec::new(),
994 scope: None,
995 assertion_sites: Vec::new(),
996 },
997 probes: collector.probes,
998 edits,
999 decision_widths: collector.widths,
1000 })
1001}
1002
1003#[cfg(test)]
1004mod tests {
1005 use super::*;
1006
1007 fn assert_indexable(limitations: &[serde_json::Value]) -> Vec<String> {
1016 assert!(!limitations.is_empty(), "nothing to check");
1017 for limitation in limitations {
1018 for field in ["id", "kind", "file", "source", "reason"] {
1019 assert!(
1020 limitation.get(field).and_then(|v| v.as_str()).is_some(),
1021 "a limitation needs a string {field}: {limitation}"
1022 );
1023 }
1024 for field in ["line", "column"] {
1025 assert!(
1026 limitation.get(field).and_then(|v| v.as_u64()).is_some(),
1027 "a limitation needs a number {field}: {limitation}"
1028 );
1029 }
1030 }
1031 let mut kinds = limitations
1032 .iter()
1033 .filter_map(|limitation| limitation["kind"].as_str().map(str::to_owned))
1034 .collect::<Vec<_>>();
1035 kinds.sort();
1036 kinds.dedup();
1037 kinds
1038 }
1039
1040 #[test]
1042 fn a_file_that_does_not_parse_says_where() {
1043 let broken = "package main\n\nfunc f() int {\n\treturn 1 )\n}\n";
1044 let message = parse(broken).expect_err("does not parse").to_string();
1045 assert!(message.contains("line 4"), "{message}");
1046 assert!(message.contains("return 1"), "{message}");
1047 assert!(
1048 !message.contains("byte"),
1049 "an offset is not somewhere a reader can look: {message}"
1050 );
1051 }
1052
1053 #[test]
1054 fn every_limitation_carries_what_the_index_stores() {
1055 const EVERY: &str = r#"package main
1056
1057func walk(items []int) int {
1058 sum := 0
1059 for _, item := range items {
1060 sum += item
1061 }
1062 for {
1063 break
1064 }
1065 return sum
1066}
1067"#;
1068 let mut next = 0;
1069 let mut decisions = 0;
1070 let obligations =
1071 build_go_obligations("walk.go", EVERY, &mut next, &mut decisions).expect("go");
1072 assert_eq!(
1073 assert_indexable(&obligations.manifest.limitations),
1074 ["loop-without-condition"]
1075 );
1076 }
1077
1078 const SAMPLE: &str = r#"package main
1079
1080import "fmt"
1081
1082func classify(a int, b bool) string {
1083 if a > 10 && b {
1084 return "big"
1085 }
1086 for i := 0; i < a; i++ {
1087 fmt.Println(i)
1088 }
1089 switch {
1090 case a == 0:
1091 return "zero"
1092 default:
1093 return "small"
1094 }
1095}
1096"#;
1097
1098 fn obligations(source: &str) -> GoFileObligations {
1099 let mut next = 0;
1100 let mut decisions = 0;
1101 build_go_obligations("main.go", source, &mut next, &mut decisions).expect("obligations")
1102 }
1103
1104 fn rewritten(source: &str) -> String {
1107 let mut next = 0;
1108 let mut decisions = 0;
1109 let go =
1110 build_go_obligations("x.go", source, &mut next, &mut decisions).expect("obligations");
1111 let out = rewrite(source, &go.edits);
1112 parse(&out)
1113 .unwrap_or_else(|error| panic!("rewritten source does not parse: {error}\n{out}"));
1114 out
1115 }
1116
1117 #[test]
1118 fn what_follows_an_insertion_is_where_it_was_written() {
1119 const SOURCE: &str =
1122 "package main\n\nfunc seal(key string) string {\n\treturn key + missing\n}\n";
1123 let mut next = 0;
1124 let mut decisions = 0;
1125 let obligations = build_go_obligations("seal.go", SOURCE, &mut next, &mut decisions)
1126 .expect("obligations");
1127 let rewritten = rewrite(SOURCE, &obligations.edits);
1128 assert!(
1130 rewritten.contains("/*line :4:2*/return key + missing"),
1131 "{rewritten}"
1132 );
1133 assert_eq!(
1134 rewritten.lines().count(),
1135 SOURCE.lines().count(),
1136 "{rewritten}"
1137 );
1138 }
1139
1140 #[test]
1141 fn a_switch_given_a_default_keeps_its_lines() {
1142 const SOURCE: &str = "package main\n\nfunc kind(key string) int {\n\tswitch {\n\tcase len(key) == 0:\n\t\treturn 0\n\tcase len(key) < 4:\n\t\treturn 1\n\t}\n\tswitch len(key) { case 4: return 2 }\n\tswitch key {\n\t}\n\treturn 3\n}\n";
1146 let mut next = 0;
1147 let mut decisions = 0;
1148 let obligations = build_go_obligations("kind.go", SOURCE, &mut next, &mut decisions)
1149 .expect("obligations");
1150 let rewritten = rewrite(SOURCE, &obligations.edits);
1151 assert_eq!(
1152 rewritten.lines().count(),
1153 SOURCE.lines().count(),
1154 "{rewritten}"
1155 );
1156 assert_eq!(rewritten.matches("default: ").count(), 3, "{rewritten}");
1157 assert_eq!(rewritten.matches("; default: ").count(), 2, "{rewritten}");
1160 parse(&rewritten.replace(RUNTIME_IMPORT, "example.com/kind/supercov")).expect("still Go");
1161 }
1162
1163 #[test]
1164 fn a_probe_never_lands_where_go_does_not_allow_a_statement() {
1165 let out = rewritten(
1169 "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",
1170 );
1171 assert!(
1172 !out.contains("for __supercov"),
1173 "probe in a for-clause init:\n{out}"
1174 );
1175 assert!(
1176 !out.contains("; __supercov.P"),
1177 "probe in a for-clause post:\n{out}"
1178 );
1179 assert!(
1180 out.contains("if b := a;"),
1181 "the if initialiser survived intact:\n{out}"
1182 );
1183 assert!(
1184 out.contains("switch c := a;"),
1185 "the switch initialiser survived intact:\n{out}"
1186 );
1187 }
1188
1189 #[test]
1190 fn wrapping_a_condition_preserves_short_circuit_order() {
1191 let out = rewritten(
1196 "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",
1197 );
1198 let condition = out
1199 .lines()
1200 .find(|line| line.contains("if "))
1201 .expect("the if survived");
1202 let branch = condition.find(".BD(").expect("branch and decision wrapper");
1203 let first = condition.find(".C(").expect("first condition wrapper");
1204 assert!(
1205 branch < first,
1206 "the branch must enclose its conditions: {condition}"
1207 );
1208 assert_eq!(
1209 condition.matches(".C(").count(),
1210 2,
1211 "one wrapper per condition: {condition}"
1212 );
1213 assert!(
1214 condition.contains("&&"),
1215 "the operator itself is untouched: {condition}"
1216 );
1217 }
1218
1219 #[test]
1220 fn a_single_condition_branch_uses_the_wrapper_that_inlines() {
1221 let out = rewritten(
1225 "package main\nfunc f(a int) bool {\n\tif a > 10 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1226 );
1227 assert!(out.contains(".B("), "{out}");
1228 assert!(!out.contains(".BD("), "no decision here to close:\n{out}");
1229 }
1230
1231 #[test]
1232 fn a_switch_without_a_default_gains_one_so_matching_nothing_is_observable() {
1233 let out = rewritten(
1236 "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1237 );
1238 assert!(out.contains("default:"), "{out}");
1239
1240 let existing = rewritten(
1242 "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\tdefault:\n\t\treturn\n\t}\n}\n",
1243 );
1244 assert_eq!(existing.matches("default:").count(), 1, "{existing}");
1245 }
1246
1247 #[test]
1248 fn a_loop_with_no_condition_records_a_limitation_not_an_obligation() {
1249 let mut next = 0;
1253 let mut decisions = 0;
1254 let go = build_go_obligations(
1255 "x.go",
1256 "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",
1257 &mut next,
1258 &mut decisions,
1259 )
1260 .unwrap();
1261 assert!(
1262 go.manifest.branches.iter().all(|b| b.kind != "loop"),
1263 "{:?}",
1264 go.manifest.branches
1265 );
1266 assert_eq!(
1267 go.manifest.limitations.len(),
1268 2,
1269 "{:?}",
1270 go.manifest.limitations
1271 );
1272 assert_eq!(go.manifest.limitations[0]["kind"], "loop-without-condition");
1273 }
1274
1275 #[test]
1276 fn only_a_file_that_gained_a_probe_imports_the_runtime() {
1277 let out = rewritten("package main\nfunc f(a int) int {\n\treturn a\n}\n");
1280 assert!(
1281 out.contains(RUNTIME_ALIAS),
1282 "a function is itself an obligation:\n{out}"
1283 );
1284
1285 let mut next = 0;
1286 let mut decisions = 0;
1287 let bare = build_go_obligations(
1288 "t.go",
1289 "package main\n\ntype T struct{}\n",
1290 &mut next,
1291 &mut decisions,
1292 )
1293 .unwrap();
1294 assert!(bare.edits.is_empty(), "{:?}", bare.edits);
1295 assert_eq!(
1296 rewrite("package main\n\ntype T struct{}\n", &bare.edits),
1297 "package main\n\ntype T struct{}\n"
1298 );
1299 }
1300
1301 #[test]
1302 fn a_short_circuiting_operator_splits_a_decision_and_nothing_else_does() {
1303 let go = obligations(SAMPLE);
1307 let decision = go
1308 .manifest
1309 .decisions
1310 .iter()
1311 .find(|d| d.kind == "if")
1312 .expect("the if carries a decision");
1313 assert_eq!(decision.conditions, ["a > 10", "b"]);
1314
1315 let simple = obligations(
1318 "package main\nfunc f(a int) bool {\n\tif a > 1 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1319 );
1320 assert!(
1321 simple.manifest.decisions.is_empty(),
1322 "{:?}",
1323 simple.manifest.decisions
1324 );
1325 }
1326
1327 #[test]
1328 fn negation_and_parentheses_do_not_invent_conditions() {
1329 let go = obligations(
1333 "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",
1334 );
1335 let decision = &go.manifest.decisions[0];
1336 assert_eq!(decision.conditions, ["a > 1", "!b", "c"]);
1337 }
1338
1339 #[test]
1340 fn every_branching_construct_records_the_outcome_that_was_not_taken() {
1341 let go = obligations(SAMPLE);
1345 let by_kind = |kind: &str| {
1346 go.manifest
1347 .branches
1348 .iter()
1349 .find(|b| b.kind == kind)
1350 .map(|b| {
1351 b.alternatives
1352 .iter()
1353 .map(|a| a.label.clone())
1354 .collect::<Vec<_>>()
1355 })
1356 };
1357 assert_eq!(by_kind("if").unwrap(), ["true", "false"]);
1358 assert_eq!(by_kind("loop").unwrap(), ["true", "false"]);
1362 let switch = by_kind("switch").unwrap();
1363 assert!(switch.contains(&"default".to_owned()), "{switch:?}");
1364 assert!(
1365 !switch.contains(&"no case matched".to_owned()),
1366 "a default already covers it"
1367 );
1368
1369 let open = obligations(
1371 "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1372 );
1373 let labels = open
1374 .manifest
1375 .branches
1376 .iter()
1377 .find(|b| b.kind == "switch")
1378 .unwrap()
1379 .alternatives
1380 .iter()
1381 .map(|a| a.label.clone())
1382 .collect::<Vec<_>>();
1383 assert!(labels.contains(&"no case matched".to_owned()), "{labels:?}");
1384 }
1385
1386 #[test]
1387 fn functions_and_statements_are_separate_obligations_with_locations() {
1388 let go = obligations(SAMPLE);
1389 let functions = go
1390 .manifest
1391 .points
1392 .iter()
1393 .filter(|p| p.kind == PointKind::Function)
1394 .collect::<Vec<_>>();
1395 assert_eq!(functions.len(), 1);
1396 assert_eq!(functions[0].label.as_deref(), Some("classify"));
1397 assert_eq!(functions[0].line, 5);
1398
1399 let statements = go
1400 .manifest
1401 .points
1402 .iter()
1403 .filter(|p| p.kind == PointKind::Statement)
1404 .count();
1405 assert!(
1406 statements >= 6,
1407 "expected the body's statements, got {statements}"
1408 );
1409 assert!(
1412 go.manifest
1413 .points
1414 .iter()
1415 .all(|p| p.line > 0 && p.column > 0)
1416 );
1417 assert!(
1419 !go.manifest
1420 .points
1421 .iter()
1422 .any(|p| p.source.starts_with("import"))
1423 );
1424 }
1425
1426 #[test]
1427 fn every_obligation_has_a_probe_and_malformed_source_is_refused() {
1428 let go = obligations(SAMPLE);
1429 let expected = go.manifest.points.len()
1430 + go.manifest
1431 .branches
1432 .iter()
1433 .map(|b| b.alternatives.len())
1434 .sum::<usize>();
1435 assert_eq!(go.probes.len(), expected);
1439 assert_eq!(go.decision_widths.len(), go.manifest.decisions.len());
1442
1443 let mut next = 0;
1446 let mut decisions = 0;
1447 assert!(matches!(
1448 build_go_obligations(
1449 "broken.go",
1450 "package main\nfunc f( {",
1451 &mut next,
1452 &mut decisions
1453 ),
1454 Err(GoInstrumenterError::Parse(_))
1455 ));
1456 }
1457
1458 #[test]
1459 fn go_1_26_new_of_a_value_is_measured_rather_than_dropped() {
1460 let go = obligations(
1469 "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",
1470 );
1471 assert_eq!(
1472 go.manifest
1473 .points
1474 .iter()
1475 .filter(|p| p.kind == PointKind::Function)
1476 .count(),
1477 3,
1478 "every function in the file is an obligation: {:?}",
1479 go.manifest.points
1480 );
1481 assert_eq!(
1482 go.manifest.branches.len(),
1483 1,
1484 "the `if` beside it is still a branch with both arms to answer for"
1485 );
1486 assert!(
1491 go.manifest
1492 .points
1493 .iter()
1494 .any(|point| point.source.contains("new(\"hello\")")),
1495 "{:?}",
1496 go.manifest.points
1497 );
1498 assert!(
1499 !go.manifest
1500 .points
1501 .iter()
1502 .any(|point| point.source.contains("nEw")),
1503 "{:?}",
1504 go.manifest.points
1505 );
1506 }
1507
1508 #[test]
1509 fn new_of_a_type_is_left_exactly_as_it_was() {
1510 let go = obligations(
1515 "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",
1516 );
1517 assert_eq!(
1518 go.manifest
1519 .points
1520 .iter()
1521 .filter(|p| p.kind == PointKind::Function)
1522 .count(),
1523 1
1524 );
1525 let mixed = obligations(
1528 "package p\n\nfunc Mixed(v int) {\n\t_ = new(map[string][]chan int)\n\t_ = new(v)\n\t_ = new(\"x\")\n}\n",
1529 );
1530 assert_eq!(
1531 mixed
1532 .manifest
1533 .points
1534 .iter()
1535 .filter(|p| p.kind == PointKind::Function)
1536 .count(),
1537 1
1538 );
1539 }
1540
1541 #[test]
1542 fn a_method_called_new_is_not_the_builtin() {
1543 let go = obligations(
1548 "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",
1549 );
1550 assert!(
1551 go.manifest
1552 .points
1553 .iter()
1554 .any(|point| point.source.contains("p.new(2)")),
1555 "{:?}",
1556 go.manifest.points
1557 );
1558 }
1559
1560 #[test]
1561 fn a_file_with_runes_outside_ascii_is_scanned_without_splitting_one() {
1562 let go = obligations(
1569 "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",
1570 );
1571 assert_eq!(
1572 go.manifest
1573 .points
1574 .iter()
1575 .filter(|p| p.kind == PointKind::Function)
1576 .count(),
1577 1
1578 );
1579 }
1580
1581 #[test]
1582 fn the_scan_never_mistakes_text_for_code() {
1583 let go = obligations(concat!(
1589 "package p\n\n",
1590 "func f(v int) *int {\n",
1591 "\t_ = \"new(\\\"x\\\") and an unclosed ( paren\"\n",
1593 "\t_ = `raw new(\"y\") with ) and \\ backslash`\n",
1594 "\t_ = '('\n",
1595 "\t// new(\"comment\") and a stray )\n",
1596 "\t/* new(\"block\") ) */\n",
1597 "\treturn new(v + 1)\n}\n",
1599 ));
1600 assert_eq!(
1601 go.manifest
1602 .points
1603 .iter()
1604 .filter(|p| p.kind == PointKind::Function)
1605 .count(),
1606 1
1607 );
1608
1609 let untouched =
1612 obligations("package p\n\nfunc g() string {\n\treturn `new(\"only in text\")`\n}\n");
1613 assert!(
1614 untouched
1615 .manifest
1616 .points
1617 .iter()
1618 .any(|point| point.source.contains("new(\"only in text\")")),
1619 "{:?}",
1620 untouched.manifest.points
1621 );
1622 }
1623
1624 #[test]
1625 fn no_shape_of_input_can_make_the_scan_panic() {
1626 let awkward = concat!(
1633 "package p\n\n",
1634 "// gr\u{fc}\u{df}e \u{2014} new(\"in a comment\")\n",
1635 "func f(sch\u{f6}n int) *int {\n",
1636 "\t_ = \"\u{fc}ber new(\\\"in a string\\\"\"\n",
1637 "\t_ = `\u{e4}\u{f6}\u{fc} new(\n",
1638 "\t_ = '\u{fc}'\n",
1639 "\t/* new(\"unterminated\n",
1640 "\treturn new(sch\u{f6}n + 1)\n}\n",
1641 );
1642 for end in 0..=awkward.len() {
1643 if !awkward.is_char_boundary(end) {
1646 continue;
1647 }
1648 let _ = parse(&awkward[..end]);
1649 if awkward.is_char_boundary(end) {
1652 let _ = parse(&awkward[end..]);
1653 }
1654 }
1655
1656 for source in [
1659 "package p\n\nfunc f() {\n\t_ = new(\n",
1660 "package p\n\nfunc f() {\n\t_ = new",
1661 "package p\n\nfunc f() {\n\t_ = new(",
1662 "package p\n\nfunc f() {\n\t_ = new()\n}\n",
1663 "new",
1664 "new(",
1665 "",
1666 ] {
1667 let _ = parse(source);
1668 }
1669 }
1670
1671 #[test]
1672 fn the_scan_reads_new_the_way_go_spells_it() {
1673 let go = obligations(concat!(
1679 "package p\n\n",
1680 "type pool struct{}\n\n",
1681 "func (p pool) new(v int) *int {\n\treturn &v\n}\n\n",
1682 "func renew(v string) *string {\n\treturn &v\n}\n\n",
1683 "func f(p pool, v int) *int {\n",
1684 "\t_ = new (\"spaced\")\n",
1686 "\t_ = new\t(\"tabbed\")\n",
1687 "\t_ = p.new(2)\n",
1689 "\t_ = renew(\"x\")\n",
1691 "\tnewValue := v\n",
1693 "\treturn new(newValue)\n}\n",
1694 ));
1695 assert_eq!(
1696 go.manifest
1697 .points
1698 .iter()
1699 .filter(|p| p.kind == PointKind::Function)
1700 .count(),
1701 3,
1702 "{:?}",
1703 go.manifest.points
1704 );
1705 assert!(
1707 go.manifest
1708 .points
1709 .iter()
1710 .any(|point| point.source.contains("new (\"spaced\")")),
1711 "{:?}",
1712 go.manifest.points
1713 );
1714 assert!(
1715 !go.manifest
1716 .points
1717 .iter()
1718 .any(|point| point.source.contains("nEw")),
1719 "{:?}",
1720 go.manifest.points
1721 );
1722 }
1723
1724 #[test]
1725 fn a_file_that_is_simply_broken_still_does_not_parse() {
1726 let mut next = 0;
1730 let mut decisions = 0;
1731 assert!(matches!(
1732 build_go_obligations(
1733 "broken.go",
1734 "package p\n\nfunc f() {\n\t_ = new(1)\n\tif {\n}\n",
1735 &mut next,
1736 &mut decisions
1737 ),
1738 Err(GoInstrumenterError::Parse(_))
1739 ));
1740 }
1741
1742 #[test]
1743 fn decisions_are_numbered_across_the_module_not_within_a_file() {
1744 let mut next = 0;
1749 let mut decisions = 0;
1750 let first = build_go_obligations(
1751 "a.go",
1752 "package p\n\nfunc A(x, y bool) bool {\n\tif x && y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1753 &mut next,
1754 &mut decisions,
1755 )
1756 .unwrap();
1757 let second = build_go_obligations(
1758 "b.go",
1759 "package p\n\nfunc B(x, y bool) bool {\n\tif x || y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1760 &mut next,
1761 &mut decisions,
1762 )
1763 .unwrap();
1764
1765 let referenced = |obligations: &GoFileObligations| {
1766 obligations
1767 .edits
1768 .iter()
1769 .filter_map(|edit| {
1770 let at = edit.text.find(".C(")?;
1771 edit.text[at + 3..]
1772 .split(',')
1773 .next()?
1774 .trim()
1775 .parse::<u32>()
1776 .ok()
1777 })
1778 .collect::<std::collections::BTreeSet<_>>()
1779 };
1780 assert_eq!(referenced(&first), [0].into());
1781 assert_eq!(referenced(&second), [1].into());
1782 assert_eq!(decisions, 2, "the module numbered two decisions in all");
1783 assert_eq!(first.decision_widths, [2]);
1786 assert_eq!(second.decision_widths, [2]);
1787 }
1788}