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 generics: bool,
430 conditions: std::collections::HashSet<(usize, usize)>,
433 unmeasured_values: Vec<(String, usize, usize, String)>,
435}
436
437fn branch_wrapper(alias: &str, when_true: u64, when_false: u64, decision: Option<usize>) -> String {
449 match decision {
450 Some(index) => format!("{alias}.BD({when_true}, {when_false}, {index}, bool("),
451 None => format!("{alias}.B({when_true}, {when_false}, bool("),
452 }
453}
454
455fn ending_fallthrough(clause: Node) -> Option<Node> {
464 let body = clause
465 .children(&mut clause.walk())
466 .find(|child| child.kind() == "statement_list")?;
467 let last = body.named_child(body.named_child_count().checked_sub(1)?)?;
468 (last.kind() == "fallthrough_statement").then_some(last)
469}
470
471fn in_statement_position(node: Node) -> bool {
472 node.parent()
473 .is_some_and(|parent| parent.kind() == "statement_list")
474}
475
476fn is_range_loop(node: Node) -> bool {
480 node.children(&mut node.walk())
481 .any(|child| child.kind() == "range_clause")
482}
483
484fn in_function_with_goto(node: Node) -> bool {
486 let mut function = node;
487 while !matches!(
488 function.kind(),
489 "function_declaration" | "method_declaration" | "func_literal" | "source_file"
490 ) {
491 match function.parent() {
492 Some(parent) => function = parent,
493 None => break,
494 }
495 }
496 let mut stack = vec![function];
497 while let Some(next) = stack.pop() {
498 if next.kind() == "goto_statement" {
499 return true;
500 }
501 stack.extend(next.children(&mut next.walk()));
502 }
503 false
504}
505
506fn loop_condition<'t>(node: Node<'t>) -> Option<Node<'t>> {
507 if let Some(condition) = node.child_by_field_name("condition") {
508 return Some(condition);
509 }
510 let mut cursor = node.walk();
511 let clause = node
512 .children(&mut cursor)
513 .find(|child| child.kind() == "for_clause")?;
514 let mut inner = clause.walk();
515 clause
516 .children(&mut inner)
517 .find(|child| child.is_named() && child.kind().ends_with("_expression"))
518}
519
520impl<'a> Collector<'a> {
521 fn id(&mut self, node: Node, kind: &str) -> String {
522 stable_obligation_id("go", self.file, kind, node.start_byte(), node.end_byte())
523 }
524
525 fn limitation_id(&mut self, node: Node, kind: &str) -> String {
530 self.id(node, kind)
531 }
532
533 fn probe(&mut self, target: GoProbeTarget, at: usize) -> u64 {
534 *self.next_probe += 1;
535 let id = *self.next_probe;
536 self.probes.insert(id, GoProbe { id, target, at });
537 id
538 }
539
540 fn edit(&mut self, at: usize, rank: i32, text: String) {
541 self.edits.push(GoEdit { at, rank, text });
542 }
543
544 fn call_before(&mut self, at: usize, call: String) {
547 self.edit(at, 100, format!("{call}; "));
548 }
549
550 fn position(&self, node: Node) -> (usize, usize) {
552 let start = node.start_position();
553 (start.row + 1, start.column + 1)
554 }
555
556 fn text(&self, node: Node) -> String {
557 let raw = &self.source[node.byte_range()];
558 let line = raw.lines().next().unwrap_or("");
559 line.trim().to_owned()
560 }
561
562 fn add_point(&mut self, node: Node, kind: PointKind, label: Option<String>) {
563 let (line, column) = self.position(node);
564 let id = self.id(
565 node,
566 match kind {
567 PointKind::Function => "function",
568 PointKind::Statement => "statement",
569 },
570 );
571 let target = match kind {
572 PointKind::Function => GoProbeTarget::Function { id: id.clone() },
573 PointKind::Statement => GoProbeTarget::Statement { id: id.clone() },
574 };
575 let at = match kind {
578 PointKind::Function => match node.child_by_field_name("body") {
579 Some(body) => body.start_byte() + 1,
580 None => return,
581 },
582 PointKind::Statement => node.start_byte(),
583 };
584 let probe = self.probe(target, at);
585 self.call_before(at, format!("{HITS_VARIABLE}[{probe}] = 2"));
589 self.points.push(PointMeta {
590 id,
591 kind,
592 file: self.file.to_owned(),
593 line,
594 column,
595 source: self.text(node),
596 label,
597 });
598 }
599
600 fn add_branch(&mut self, node: Node, kind: &str, labels: &[&str]) -> Vec<u64> {
601 let pairs = labels
602 .iter()
603 .map(|label| (*label, *label))
604 .collect::<Vec<_>>();
605 self.add_branch_named(node, kind, &pairs)
606 }
607
608 fn add_branch_named(&mut self, node: Node, kind: &str, labels: &[(&str, &str)]) -> Vec<u64> {
610 let (line, column) = self.position(node);
611 let id = self.id(node, "branch");
612 let mut probes = Vec::new();
613 let alternatives = labels
614 .iter()
615 .map(|(suffix, label)| {
616 let alternative = format!("{id}.{suffix}");
617 probes.push(self.probe(
618 GoProbeTarget::Alternative {
619 branch: id.clone(),
620 alternative: alternative.clone(),
621 },
622 node.start_byte(),
623 ));
624 BranchAlternativeMeta {
625 id: alternative,
626 label: (*label).to_owned(),
627 }
628 })
629 .collect();
630 self.branches.push(BranchMeta {
631 id,
632 kind: kind.to_owned(),
633 file: self.file.to_owned(),
634 line,
635 column,
636 source: self.text(node),
637 alternatives,
638 });
639 probes
640 }
641
642 fn add_decision(&mut self, node: Node, kind: &str) -> Option<usize> {
648 self.add_decision_wrapping(node, kind, false)
649 }
650
651 fn add_decision_wrapping(&mut self, node: Node, kind: &str, value: bool) -> Option<usize> {
656 let mut leaves = Vec::new();
657 condition_nodes(node, self.source, &mut leaves);
658 if leaves.len() < 2 {
659 return None;
660 }
661 let conditions = leaves
662 .iter()
663 .map(|leaf| self.source[leaf.byte_range()].trim().to_owned())
664 .collect::<Vec<_>>();
665 let (line, column) = self.position(node);
666 let id = self.id(node, "decision");
667 let index_of_decision = self.decision_base as usize + self.widths.len();
671 self.widths.push(leaves.len().min(64) as u8);
672 let alias = self.alias.to_owned();
673 for (index, leaf) in leaves.iter().enumerate() {
678 let (open, close) = if !value {
682 (
683 format!("{alias}.C({index_of_decision}, {index}, bool("),
684 "))",
685 )
686 } else if untyped_boolean(*leaf, self.source) {
687 (
688 format!("({alias}.C({index_of_decision}, {index}, "),
689 ") == true)",
690 )
691 } else {
692 (format!("{alias}.CV({index_of_decision}, {index}, "), ")")
693 };
694 self.edit(leaf.start_byte(), 20, open);
695 self.edit(leaf.end_byte(), 20, close.to_owned());
696 }
697 self.decisions.push(DecisionMeta {
698 id,
699 file: self.file.to_owned(),
700 line,
701 column,
702 source: self.text(node),
703 conditions,
704 kind: kind.to_owned(),
705 });
706 Some(index_of_decision)
707 }
708
709 fn walk(&mut self, node: Node) {
710 let alias = self.alias.to_owned();
711 match node.kind() {
712 "function_declaration" | "method_declaration" | "func_literal" => {
713 let label = node
714 .child_by_field_name("name")
715 .map(|name| self.source[name.byte_range()].to_owned());
716 self.add_point(node, PointKind::Function, label);
717 }
718 "if_statement" => {
719 if let Some(condition) = node.child_by_field_name("condition") {
720 let bare = unparenthesized(condition);
721 self.conditions.insert((bare.start_byte(), bare.end_byte()));
722 let probes = self.add_branch(node, "if", &["true", "false"]);
727 let decision = self.add_decision(condition, "if");
728 self.edit(
729 condition.start_byte(),
730 5,
731 branch_wrapper(&alias, probes[0], probes[1], decision),
732 );
733 self.edit(condition.end_byte(), 5, "))".to_owned());
734 }
735 }
736 "for_statement" => {
737 match loop_condition(node) {
742 Some(condition) => {
743 let bare = unparenthesized(condition);
744 self.conditions.insert((bare.start_byte(), bare.end_byte()));
745 let probes = self.add_branch(node, "loop", &["true", "false"]);
746 let decision = self.add_decision(condition, "loop");
747 self.edit(
748 condition.start_byte(),
749 5,
750 branch_wrapper(&alias, probes[0], probes[1], decision),
751 );
752 self.edit(condition.end_byte(), 5, "))".to_owned());
753 }
754 None if is_range_loop(node) && !in_function_with_goto(node) => {
755 let probes = self.add_branch_named(
762 node,
763 "for",
764 &[("zero", "zero iterations"), ("entered", "entered")],
765 );
766 let flag = format!("_scvRan{}", node.start_byte());
767 let statement = node
768 .parent()
769 .filter(|parent| parent.kind() == "labeled_statement")
770 .unwrap_or(node);
771 self.edit(statement.start_byte(), 90, format!("var {flag} bool; "));
772 if let Some(body) = node.child_by_field_name("body") {
773 self.edit(
774 body.start_byte() + 1,
775 100,
776 format!(
777 " if !{flag} {{ {flag} = true; {alias}.A({}) }};",
778 probes[1]
779 ),
780 );
781 }
782 self.edit(
783 node.end_byte(),
784 100,
785 format!("; if !{flag} {{ {alias}.A({}) }}", probes[0]),
786 );
787 }
788 None => {
789 let (line, column) = self.position(node);
790 let limitation = self.limitation_id(node, "loop-without-condition");
791 self.limitations.push(serde_json::json!({
792 "id": limitation,
793 "kind": "loop-without-condition",
794 "file": self.file,
795 "source": self.text(node),
796 "line": line,
797 "column": column,
798 "reason": "a range or unconditional loop has no condition to observe, so no branch obligation is recorded for it",
799 }));
800 }
801 }
802 }
803 "expression_switch_statement" | "type_switch_statement" | "select_statement" => {
804 let kind = match node.kind() {
805 "expression_switch_statement" => "switch",
806 "type_switch_statement" => "type-switch",
807 _ => "select",
808 };
809 let mut cases = Vec::new();
810 let mut has_default = false;
811 let mut cursor = node.walk();
812 for child in node.children(&mut cursor) {
813 match child.kind() {
814 "expression_case" | "type_case" | "communication_case" => {
815 cases.push((self.text(child), Some(child)))
816 }
817 "default_case" => {
818 has_default = true;
819 cases.push(("default".to_owned(), Some(child)));
820 }
821 _ => {}
822 }
823 }
824 if !has_default && node.kind() != "select_statement" {
839 cases.push(("no case matched".to_owned(), None));
840 }
841 let labels = cases
842 .iter()
843 .map(|(label, _)| label.as_str())
844 .collect::<Vec<_>>();
845 let probes = self.add_branch(node, kind, &labels);
846 let fallthroughs = cases
853 .iter()
854 .map(|(_, clause)| clause.and_then(|clause| ending_fallthrough(clause)))
855 .collect::<Vec<_>>();
856 let flag = format!("_scvFell{}", node.start_byte());
857 if fallthroughs.iter().any(Option::is_some) {
858 let statement = node
860 .parent()
861 .filter(|parent| parent.kind() == "labeled_statement")
862 .unwrap_or(node);
863 self.edit(statement.start_byte(), 90, format!("var {flag} bool; "));
864 for fallthrough in fallthroughs.iter().flatten() {
865 self.call_before(fallthrough.start_byte(), format!("{flag} = true"));
866 }
867 }
868 for (index, (probe, (_, clause))) in probes.iter().zip(cases.iter()).enumerate() {
869 match clause {
870 Some(clause) => {
871 let at = clause
872 .children(&mut clause.walk())
873 .find(|child| child.kind() == "statement_list")
874 .map(|body| body.start_byte())
875 .unwrap_or_else(|| clause.end_byte());
876 if index > 0 && fallthroughs[index - 1].is_some() {
877 self.edit(
878 at,
879 100,
880 format!("if {flag} {{ {flag} = false }} else {{ {alias}.A({probe}) }}; "),
881 );
882 } else {
883 self.call_before(at, format!("{alias}.A({probe})"));
884 }
885 }
886 None => {
887 let at = node.end_byte().saturating_sub(1);
895 let after_a_clause = cases.iter().any(|(_, clause)| clause.is_some());
896 self.edit(
897 at,
898 100,
899 format!(
900 "{}default: {alias}.A({probe}) ",
901 if after_a_clause { "; " } else { "" }
902 ),
903 );
904 }
905 }
906 }
907 }
908 "binary_expression"
909 if is_logical(node, self.source)
910 && !logical_parent(node, self.source)
911 && !self
912 .conditions
913 .contains(&(node.start_byte(), node.end_byte()))
914 && in_function_body(node) =>
915 {
916 if self.generics {
920 let probes = self.add_branch(node, "value", &["true", "false"]);
921 let mut leaves = Vec::new();
922 condition_nodes(node, self.source, &mut leaves);
923 let typed = leaves
924 .iter()
925 .any(|leaf| !untyped_boolean(*leaf, self.source));
926 if let Some(index) = self.add_decision_wrapping(node, "value", true) {
927 let (open, close) = if typed {
928 (
929 format!("{alias}.BV({}, {}, {index}, ", probes[0], probes[1]),
930 ")",
931 )
932 } else {
933 (
934 format!("({alias}.BD({}, {}, {index}, ", probes[0], probes[1]),
935 ") == true)",
936 )
937 };
938 self.edit(node.start_byte(), 5, open);
939 self.edit(node.end_byte(), 5, close.to_owned());
940 }
941 } else {
942 let (line, column) = self.position(node);
943 let id = self.limitation_id(node, "value-decision-before-generics");
944 self.unmeasured_values
945 .push((id, line, column, self.text(node)));
946 }
947 }
948 kind if is_statement(kind) && in_statement_position(node) => {
949 self.add_point(node, PointKind::Statement, None);
950 }
951 _ => {}
952 }
953 if in_statement_position(node)
956 && matches!(
957 node.kind(),
958 "if_statement"
959 | "for_statement"
960 | "expression_switch_statement"
961 | "type_switch_statement"
962 | "select_statement"
963 )
964 {
965 self.add_point(node, PointKind::Statement, None);
966 }
967 let mut cursor = node.walk();
968 for child in node.children(&mut cursor) {
969 if child.is_named() {
970 self.walk(child);
971 }
972 }
973 }
974}
975
976fn condition_nodes<'t>(node: Node<'t>, source: &str, out: &mut Vec<Node<'t>>) {
982 match node.kind() {
983 "binary_expression" => {
984 let operator = node
985 .child_by_field_name("operator")
986 .map(|op| &source[op.byte_range()])
987 .unwrap_or("");
988 if operator == "&&" || operator == "||" {
989 if let Some(left) = node.child_by_field_name("left") {
990 condition_nodes(left, source, out);
991 }
992 if let Some(right) = node.child_by_field_name("right") {
993 condition_nodes(right, source, out);
994 }
995 return;
996 }
997 out.push(node);
998 }
999 "parenthesized_expression" => {
1000 let mut cursor = node.walk();
1001 match node.children(&mut cursor).find(|child| child.is_named()) {
1002 Some(inner) => condition_nodes(inner, source, out),
1003 None => out.push(node),
1004 }
1005 }
1006 _ => out.push(node),
1007 }
1008}
1009
1010fn is_logical(node: Node, source: &str) -> bool {
1011 node.kind() == "binary_expression"
1012 && node
1013 .child_by_field_name("operator")
1014 .is_some_and(|op| matches!(&source[op.byte_range()], "&&" | "||"))
1015}
1016
1017fn logical_parent(node: Node, source: &str) -> bool {
1020 let mut parent = node.parent();
1021 while let Some(candidate) =
1022 parent.filter(|candidate| candidate.kind() == "parenthesized_expression")
1023 {
1024 parent = candidate.parent();
1025 }
1026 parent.is_some_and(|parent| is_logical(parent, source))
1027}
1028
1029fn unparenthesized(node: Node) -> Node {
1030 if node.kind() == "parenthesized_expression"
1031 && let Some(inner) = node
1032 .children(&mut node.walk())
1033 .find(|child| child.is_named())
1034 {
1035 return unparenthesized(inner);
1036 }
1037 node
1038}
1039
1040fn in_function_body(node: Node) -> bool {
1043 let mut current = node.parent();
1044 while let Some(ancestor) = current {
1045 match ancestor.kind() {
1046 "const_declaration" => return false,
1047 "block" => return true,
1048 _ => current = ancestor.parent(),
1049 }
1050 }
1051 false
1052}
1053
1054fn untyped_boolean(node: Node, source: &str) -> bool {
1057 match node.kind() {
1058 "true" | "false" => true,
1059 "parenthesized_expression" => node
1060 .children(&mut node.walk())
1061 .find(|child| child.is_named())
1062 .is_some_and(|inner| untyped_boolean(inner, source)),
1063 "unary_expression" => {
1064 node.child_by_field_name("operator")
1065 .is_some_and(|op| &source[op.byte_range()] == "!")
1066 && node
1067 .child_by_field_name("operand")
1068 .is_some_and(|operand| untyped_boolean(operand, source))
1069 }
1070 "binary_expression" => match node
1071 .child_by_field_name("operator")
1072 .map(|op| &source[op.byte_range()])
1073 {
1074 Some("==" | "!=" | "<" | "<=" | ">" | ">=") => true,
1075 Some("&&" | "||") => {
1076 node.child_by_field_name("left")
1077 .is_some_and(|left| untyped_boolean(left, source))
1078 && node
1079 .child_by_field_name("right")
1080 .is_some_and(|right| untyped_boolean(right, source))
1081 }
1082 _ => false,
1083 },
1084 _ => false,
1085 }
1086}
1087
1088#[derive(Debug, Clone, PartialEq, Eq)]
1095pub struct GoEdit {
1096 pub at: usize,
1097 pub rank: i32,
1098 pub text: String,
1099}
1100
1101pub fn rewrite(source: &str, edits: &[GoEdit]) -> String {
1109 let mut ordered = edits.to_vec();
1110 ordered.sort_by(|a, b| b.at.cmp(&a.at).then(b.rank.cmp(&a.rank)));
1111 let lines = std::iter::once(0)
1112 .chain(source.match_indices('\n').map(|(at, _)| at + 1))
1113 .collect::<Vec<_>>();
1114 let mut out = source.to_owned();
1115 let mut placed = None;
1116 for edit in ordered {
1117 if edit.at > source.len() {
1118 continue;
1119 }
1120 if placed != Some(edit.at) {
1121 placed = Some(edit.at);
1122 let line = lines.partition_point(|start| *start <= edit.at);
1123 let column = edit.at - lines[line - 1] + 1;
1124 out.insert_str(edit.at, &format!("/*line :{line}:{column}*/"));
1125 }
1126 out.insert_str(edit.at, &edit.text);
1127 }
1128 out
1129}
1130
1131pub fn import_edit(source: &str, alias: &str, path: &str) -> Option<GoEdit> {
1137 let tree = parse(source).ok()?;
1138 let mut cursor = tree.root_node().walk();
1139 let package = tree
1140 .root_node()
1141 .children(&mut cursor)
1142 .find(|child| child.kind() == "package_clause")?;
1143 Some(GoEdit {
1144 at: package.end_byte(),
1145 rank: 0,
1146 text: format!("; import {alias} \"{path}\""),
1147 })
1148}
1149
1150pub fn build_go_obligations(
1151 file: &str,
1152 source: &str,
1153 next_probe: &mut u64,
1154 next_decision: &mut u32,
1155) -> Result<GoFileObligations, GoInstrumenterError> {
1156 build_go_obligations_with_alias(file, source, next_probe, next_decision, RUNTIME_ALIAS)
1157}
1158
1159pub const RUNTIME_ALIAS: &str = "__supercov";
1162
1163pub const HITS_VARIABLE: &str = "__supercovHits";
1166
1167pub const RUNTIME_IMPORT: &str = "github.com/supercorp-ai/supercov/runtime/go/supercov";
1169
1170pub fn build_go_obligations_with_alias(
1171 file: &str,
1172 source: &str,
1173 next_probe: &mut u64,
1174 next_decision: &mut u32,
1175 alias: &str,
1176) -> Result<GoFileObligations, GoInstrumenterError> {
1177 build_go_obligations_for(file, source, next_probe, next_decision, alias, true)
1178}
1179
1180pub fn build_go_obligations_for(
1185 file: &str,
1186 source: &str,
1187 next_probe: &mut u64,
1188 next_decision: &mut u32,
1189 alias: &str,
1190 generics: bool,
1191) -> Result<GoFileObligations, GoInstrumenterError> {
1192 let tree = parse(source)?;
1193 let decision_base = *next_decision;
1194 let mut collector = Collector {
1195 file,
1196 source,
1197 alias,
1198 next_probe,
1199 decision_base,
1200 edits: Vec::new(),
1201 points: Vec::new(),
1202 branches: Vec::new(),
1203 decisions: Vec::new(),
1204 probes: BTreeMap::new(),
1205 limitations: Vec::new(),
1206 widths: Vec::new(),
1207 generics,
1208 conditions: std::collections::HashSet::new(),
1209 unmeasured_values: Vec::new(),
1210 };
1211 let mut cursor = tree.root_node().walk();
1212 for child in tree.root_node().children(&mut cursor) {
1213 if child.is_named() {
1214 collector.walk(child);
1215 }
1216 }
1217 if let Some((id, line, column, source_text)) = collector.unmeasured_values.first().cloned() {
1219 let count = collector.unmeasured_values.len();
1220 collector.limitations.push(serde_json::json!({
1221 "id": id,
1222 "kind": "value-decision-before-generics",
1223 "file": file,
1224 "source": source_text,
1225 "line": line,
1226 "column": column,
1227 "reason": format!("{count} `&&`/`||` used as a value in this file: keeping its type through a probe needs generics, and the module's go directive is older than 1.18"),
1228 }));
1229 }
1230 *next_decision += collector.widths.len() as u32;
1231 let mut edits = collector.edits;
1232 let calls_runtime = edits
1236 .iter()
1237 .any(|edit| edit.text.contains(&format!("{alias}.")));
1238 if calls_runtime && let Some(import) = import_edit(source, alias, RUNTIME_IMPORT) {
1239 edits.push(import);
1240 }
1241 Ok(GoFileObligations {
1242 manifest: CoverageManifest {
1243 decisions: collector.decisions,
1244 points: collector.points,
1245 branches: collector.branches,
1246 limitations: collector.limitations,
1247 unmeasured: Vec::new(),
1248 scope: None,
1249 assertion_sites: Vec::new(),
1250 },
1251 probes: collector.probes,
1252 edits,
1253 decision_widths: collector.widths,
1254 })
1255}
1256
1257#[cfg(test)]
1258mod tests {
1259 use super::*;
1260
1261 fn assert_indexable(limitations: &[serde_json::Value]) -> Vec<String> {
1270 assert!(!limitations.is_empty(), "nothing to check");
1271 for limitation in limitations {
1272 for field in ["id", "kind", "file", "source", "reason"] {
1273 assert!(
1274 limitation.get(field).and_then(|v| v.as_str()).is_some(),
1275 "a limitation needs a string {field}: {limitation}"
1276 );
1277 }
1278 for field in ["line", "column"] {
1279 assert!(
1280 limitation.get(field).and_then(|v| v.as_u64()).is_some(),
1281 "a limitation needs a number {field}: {limitation}"
1282 );
1283 }
1284 }
1285 let mut kinds = limitations
1286 .iter()
1287 .filter_map(|limitation| limitation["kind"].as_str().map(str::to_owned))
1288 .collect::<Vec<_>>();
1289 kinds.sort();
1290 kinds.dedup();
1291 kinds
1292 }
1293
1294 #[test]
1296 fn a_file_that_does_not_parse_says_where() {
1297 let broken = "package main\n\nfunc f() int {\n\treturn 1 )\n}\n";
1298 let message = parse(broken).expect_err("does not parse").to_string();
1299 assert!(message.contains("line 4"), "{message}");
1300 assert!(message.contains("return 1"), "{message}");
1301 assert!(
1302 !message.contains("byte"),
1303 "an offset is not somewhere a reader can look: {message}"
1304 );
1305 }
1306
1307 #[test]
1308 fn every_limitation_carries_what_the_index_stores() {
1309 const EVERY: &str = r#"package main
1310
1311func walk(items []int) int {
1312 sum := 0
1313 for _, item := range items {
1314 sum += item
1315 }
1316 for {
1317 break
1318 }
1319 return sum
1320}
1321"#;
1322 let mut next = 0;
1323 let mut decisions = 0;
1324 let obligations =
1325 build_go_obligations("walk.go", EVERY, &mut next, &mut decisions).expect("go");
1326 assert_eq!(
1327 assert_indexable(&obligations.manifest.limitations),
1328 ["loop-without-condition"]
1329 );
1330 }
1331
1332 const SAMPLE: &str = r#"package main
1333
1334import "fmt"
1335
1336func classify(a int, b bool) string {
1337 if a > 10 && b {
1338 return "big"
1339 }
1340 for i := 0; i < a; i++ {
1341 fmt.Println(i)
1342 }
1343 switch {
1344 case a == 0:
1345 return "zero"
1346 default:
1347 return "small"
1348 }
1349}
1350"#;
1351
1352 fn obligations(source: &str) -> GoFileObligations {
1353 let mut next = 0;
1354 let mut decisions = 0;
1355 build_go_obligations("main.go", source, &mut next, &mut decisions).expect("obligations")
1356 }
1357
1358 fn rewritten(source: &str) -> String {
1361 let mut next = 0;
1362 let mut decisions = 0;
1363 let go =
1364 build_go_obligations("x.go", source, &mut next, &mut decisions).expect("obligations");
1365 let out = rewrite(source, &go.edits);
1366 parse(&out)
1367 .unwrap_or_else(|error| panic!("rewritten source does not parse: {error}\n{out}"));
1368 out
1369 }
1370
1371 #[test]
1372 fn what_follows_an_insertion_is_where_it_was_written() {
1373 const SOURCE: &str =
1376 "package main\n\nfunc seal(key string) string {\n\treturn key + missing\n}\n";
1377 let mut next = 0;
1378 let mut decisions = 0;
1379 let obligations = build_go_obligations("seal.go", SOURCE, &mut next, &mut decisions)
1380 .expect("obligations");
1381 let rewritten = rewrite(SOURCE, &obligations.edits);
1382 assert!(
1384 rewritten.contains("/*line :4:2*/return key + missing"),
1385 "{rewritten}"
1386 );
1387 assert_eq!(
1388 rewritten.lines().count(),
1389 SOURCE.lines().count(),
1390 "{rewritten}"
1391 );
1392 }
1393
1394 #[test]
1395 fn a_switch_given_a_default_keeps_its_lines() {
1396 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";
1400 let mut next = 0;
1401 let mut decisions = 0;
1402 let obligations = build_go_obligations("kind.go", SOURCE, &mut next, &mut decisions)
1403 .expect("obligations");
1404 let rewritten = rewrite(SOURCE, &obligations.edits);
1405 assert_eq!(
1406 rewritten.lines().count(),
1407 SOURCE.lines().count(),
1408 "{rewritten}"
1409 );
1410 assert_eq!(rewritten.matches("default: ").count(), 3, "{rewritten}");
1411 assert_eq!(rewritten.matches("; default: ").count(), 2, "{rewritten}");
1414 parse(&rewritten.replace(RUNTIME_IMPORT, "example.com/kind/supercov")).expect("still Go");
1415 }
1416
1417 #[test]
1418 fn a_probe_never_lands_where_go_does_not_allow_a_statement() {
1419 let out = rewritten(
1423 "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",
1424 );
1425 assert!(
1426 !out.contains("for __supercov"),
1427 "probe in a for-clause init:\n{out}"
1428 );
1429 assert!(
1430 !out.contains("; __supercov.P"),
1431 "probe in a for-clause post:\n{out}"
1432 );
1433 assert!(
1434 out.contains("if b := a;"),
1435 "the if initialiser survived intact:\n{out}"
1436 );
1437 assert!(
1438 out.contains("switch c := a;"),
1439 "the switch initialiser survived intact:\n{out}"
1440 );
1441 }
1442
1443 #[test]
1444 fn wrapping_a_condition_preserves_short_circuit_order() {
1445 let out = rewritten(
1450 "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",
1451 );
1452 let condition = out
1453 .lines()
1454 .find(|line| line.contains("if "))
1455 .expect("the if survived");
1456 let branch = condition.find(".BD(").expect("branch and decision wrapper");
1457 let first = condition.find(".C(").expect("first condition wrapper");
1458 assert!(
1459 branch < first,
1460 "the branch must enclose its conditions: {condition}"
1461 );
1462 assert_eq!(
1463 condition.matches(".C(").count(),
1464 2,
1465 "one wrapper per condition: {condition}"
1466 );
1467 assert!(
1468 condition.contains("&&"),
1469 "the operator itself is untouched: {condition}"
1470 );
1471 }
1472
1473 #[test]
1474 fn a_single_condition_branch_uses_the_wrapper_that_inlines() {
1475 let out = rewritten(
1479 "package main\nfunc f(a int) bool {\n\tif a > 10 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1480 );
1481 assert!(out.contains(".B("), "{out}");
1482 assert!(!out.contains(".BD("), "no decision here to close:\n{out}");
1483 }
1484
1485 #[test]
1486 fn a_switch_without_a_default_gains_one_so_matching_nothing_is_observable() {
1487 let out = rewritten(
1490 "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1491 );
1492 assert!(out.contains("default:"), "{out}");
1493
1494 let existing = rewritten(
1496 "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\tdefault:\n\t\treturn\n\t}\n}\n",
1497 );
1498 assert_eq!(existing.matches("default:").count(), 1, "{existing}");
1499 }
1500
1501 #[test]
1502 fn a_range_loop_records_whether_it_ran_and_a_bare_loop_a_limitation() {
1503 let mut next = 0;
1509 let mut decisions = 0;
1510 let go = build_go_obligations(
1511 "x.go",
1512 "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}\nfunc g(xs []int) {\n\tfor range xs {\n\t}\n\tgoto end\nend:\n}\n",
1513 &mut next,
1514 &mut decisions,
1515 )
1516 .unwrap();
1517 let loops = go
1518 .manifest
1519 .branches
1520 .iter()
1521 .filter(|b| b.kind == "for")
1522 .collect::<Vec<_>>();
1523 assert_eq!(loops.len(), 1, "{:?}", go.manifest.branches);
1524 assert_eq!(
1525 loops[0]
1526 .alternatives
1527 .iter()
1528 .map(|a| a.label.as_str())
1529 .collect::<Vec<_>>(),
1530 ["zero iterations", "entered"]
1531 );
1532 assert_eq!(
1533 go.manifest.limitations.len(),
1534 2,
1535 "{:?}",
1536 go.manifest.limitations
1537 );
1538 assert!(
1539 go.manifest
1540 .limitations
1541 .iter()
1542 .all(|limitation| limitation["kind"] == "loop-without-condition")
1543 );
1544 }
1545
1546 #[test]
1547 fn only_a_file_that_gained_a_probe_imports_the_runtime() {
1548 let out = rewritten("package main\nfunc f(a int) int {\n\treturn a\n}\n");
1551 assert!(
1552 out.contains(RUNTIME_ALIAS),
1553 "a function is itself an obligation:\n{out}"
1554 );
1555
1556 let mut next = 0;
1557 let mut decisions = 0;
1558 let bare = build_go_obligations(
1559 "t.go",
1560 "package main\n\ntype T struct{}\n",
1561 &mut next,
1562 &mut decisions,
1563 )
1564 .unwrap();
1565 assert!(bare.edits.is_empty(), "{:?}", bare.edits);
1566 assert_eq!(
1567 rewrite("package main\n\ntype T struct{}\n", &bare.edits),
1568 "package main\n\ntype T struct{}\n"
1569 );
1570 }
1571
1572 #[test]
1573 fn a_short_circuiting_operator_splits_a_decision_and_nothing_else_does() {
1574 let go = obligations(SAMPLE);
1578 let decision = go
1579 .manifest
1580 .decisions
1581 .iter()
1582 .find(|d| d.kind == "if")
1583 .expect("the if carries a decision");
1584 assert_eq!(decision.conditions, ["a > 10", "b"]);
1585
1586 let simple = obligations(
1589 "package main\nfunc f(a int) bool {\n\tif a > 1 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1590 );
1591 assert!(
1592 simple.manifest.decisions.is_empty(),
1593 "{:?}",
1594 simple.manifest.decisions
1595 );
1596 }
1597
1598 #[test]
1599 fn negation_and_parentheses_do_not_invent_conditions() {
1600 let go = obligations(
1604 "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",
1605 );
1606 let decision = &go.manifest.decisions[0];
1607 assert_eq!(decision.conditions, ["a > 1", "!b", "c"]);
1608 }
1609
1610 #[test]
1611 fn every_branching_construct_records_the_outcome_that_was_not_taken() {
1612 let go = obligations(SAMPLE);
1616 let by_kind = |kind: &str| {
1617 go.manifest
1618 .branches
1619 .iter()
1620 .find(|b| b.kind == kind)
1621 .map(|b| {
1622 b.alternatives
1623 .iter()
1624 .map(|a| a.label.clone())
1625 .collect::<Vec<_>>()
1626 })
1627 };
1628 assert_eq!(by_kind("if").unwrap(), ["true", "false"]);
1629 assert_eq!(by_kind("loop").unwrap(), ["true", "false"]);
1633 let switch = by_kind("switch").unwrap();
1634 assert!(switch.contains(&"default".to_owned()), "{switch:?}");
1635 assert!(
1636 !switch.contains(&"no case matched".to_owned()),
1637 "a default already covers it"
1638 );
1639
1640 let open = obligations(
1642 "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1643 );
1644 let labels = open
1645 .manifest
1646 .branches
1647 .iter()
1648 .find(|b| b.kind == "switch")
1649 .unwrap()
1650 .alternatives
1651 .iter()
1652 .map(|a| a.label.clone())
1653 .collect::<Vec<_>>();
1654 assert!(labels.contains(&"no case matched".to_owned()), "{labels:?}");
1655 }
1656
1657 #[test]
1658 fn functions_and_statements_are_separate_obligations_with_locations() {
1659 let go = obligations(SAMPLE);
1660 let functions = go
1661 .manifest
1662 .points
1663 .iter()
1664 .filter(|p| p.kind == PointKind::Function)
1665 .collect::<Vec<_>>();
1666 assert_eq!(functions.len(), 1);
1667 assert_eq!(functions[0].label.as_deref(), Some("classify"));
1668 assert_eq!(functions[0].line, 5);
1669
1670 let statements = go
1671 .manifest
1672 .points
1673 .iter()
1674 .filter(|p| p.kind == PointKind::Statement)
1675 .count();
1676 assert!(
1677 statements >= 6,
1678 "expected the body's statements, got {statements}"
1679 );
1680 assert!(
1683 go.manifest
1684 .points
1685 .iter()
1686 .all(|p| p.line > 0 && p.column > 0)
1687 );
1688 assert!(
1690 !go.manifest
1691 .points
1692 .iter()
1693 .any(|p| p.source.starts_with("import"))
1694 );
1695 }
1696
1697 #[test]
1698 fn every_obligation_has_a_probe_and_malformed_source_is_refused() {
1699 let go = obligations(SAMPLE);
1700 let expected = go.manifest.points.len()
1701 + go.manifest
1702 .branches
1703 .iter()
1704 .map(|b| b.alternatives.len())
1705 .sum::<usize>();
1706 assert_eq!(go.probes.len(), expected);
1710 assert_eq!(go.decision_widths.len(), go.manifest.decisions.len());
1713
1714 let mut next = 0;
1717 let mut decisions = 0;
1718 assert!(matches!(
1719 build_go_obligations(
1720 "broken.go",
1721 "package main\nfunc f( {",
1722 &mut next,
1723 &mut decisions
1724 ),
1725 Err(GoInstrumenterError::Parse(_))
1726 ));
1727 }
1728
1729 #[test]
1730 fn go_1_26_new_of_a_value_is_measured_rather_than_dropped() {
1731 let go = obligations(
1740 "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",
1741 );
1742 assert_eq!(
1743 go.manifest
1744 .points
1745 .iter()
1746 .filter(|p| p.kind == PointKind::Function)
1747 .count(),
1748 3,
1749 "every function in the file is an obligation: {:?}",
1750 go.manifest.points
1751 );
1752 assert_eq!(
1753 go.manifest.branches.len(),
1754 1,
1755 "the `if` beside it is still a branch with both arms to answer for"
1756 );
1757 assert!(
1762 go.manifest
1763 .points
1764 .iter()
1765 .any(|point| point.source.contains("new(\"hello\")")),
1766 "{:?}",
1767 go.manifest.points
1768 );
1769 assert!(
1770 !go.manifest
1771 .points
1772 .iter()
1773 .any(|point| point.source.contains("nEw")),
1774 "{:?}",
1775 go.manifest.points
1776 );
1777 }
1778
1779 #[test]
1780 fn new_of_a_type_is_left_exactly_as_it_was() {
1781 let go = obligations(
1786 "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",
1787 );
1788 assert_eq!(
1789 go.manifest
1790 .points
1791 .iter()
1792 .filter(|p| p.kind == PointKind::Function)
1793 .count(),
1794 1
1795 );
1796 let mixed = obligations(
1799 "package p\n\nfunc Mixed(v int) {\n\t_ = new(map[string][]chan int)\n\t_ = new(v)\n\t_ = new(\"x\")\n}\n",
1800 );
1801 assert_eq!(
1802 mixed
1803 .manifest
1804 .points
1805 .iter()
1806 .filter(|p| p.kind == PointKind::Function)
1807 .count(),
1808 1
1809 );
1810 }
1811
1812 #[test]
1813 fn a_method_called_new_is_not_the_builtin() {
1814 let go = obligations(
1819 "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",
1820 );
1821 assert!(
1822 go.manifest
1823 .points
1824 .iter()
1825 .any(|point| point.source.contains("p.new(2)")),
1826 "{:?}",
1827 go.manifest.points
1828 );
1829 }
1830
1831 #[test]
1832 fn a_file_with_runes_outside_ascii_is_scanned_without_splitting_one() {
1833 let go = obligations(
1840 "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",
1841 );
1842 assert_eq!(
1843 go.manifest
1844 .points
1845 .iter()
1846 .filter(|p| p.kind == PointKind::Function)
1847 .count(),
1848 1
1849 );
1850 }
1851
1852 #[test]
1853 fn the_scan_never_mistakes_text_for_code() {
1854 let go = obligations(concat!(
1860 "package p\n\n",
1861 "func f(v int) *int {\n",
1862 "\t_ = \"new(\\\"x\\\") and an unclosed ( paren\"\n",
1864 "\t_ = `raw new(\"y\") with ) and \\ backslash`\n",
1865 "\t_ = '('\n",
1866 "\t// new(\"comment\") and a stray )\n",
1867 "\t/* new(\"block\") ) */\n",
1868 "\treturn new(v + 1)\n}\n",
1870 ));
1871 assert_eq!(
1872 go.manifest
1873 .points
1874 .iter()
1875 .filter(|p| p.kind == PointKind::Function)
1876 .count(),
1877 1
1878 );
1879
1880 let untouched =
1883 obligations("package p\n\nfunc g() string {\n\treturn `new(\"only in text\")`\n}\n");
1884 assert!(
1885 untouched
1886 .manifest
1887 .points
1888 .iter()
1889 .any(|point| point.source.contains("new(\"only in text\")")),
1890 "{:?}",
1891 untouched.manifest.points
1892 );
1893 }
1894
1895 #[test]
1896 fn no_shape_of_input_can_make_the_scan_panic() {
1897 let awkward = concat!(
1904 "package p\n\n",
1905 "// gr\u{fc}\u{df}e \u{2014} new(\"in a comment\")\n",
1906 "func f(sch\u{f6}n int) *int {\n",
1907 "\t_ = \"\u{fc}ber new(\\\"in a string\\\"\"\n",
1908 "\t_ = `\u{e4}\u{f6}\u{fc} new(\n",
1909 "\t_ = '\u{fc}'\n",
1910 "\t/* new(\"unterminated\n",
1911 "\treturn new(sch\u{f6}n + 1)\n}\n",
1912 );
1913 for end in 0..=awkward.len() {
1914 if !awkward.is_char_boundary(end) {
1917 continue;
1918 }
1919 let _ = parse(&awkward[..end]);
1920 if awkward.is_char_boundary(end) {
1923 let _ = parse(&awkward[end..]);
1924 }
1925 }
1926
1927 for source in [
1930 "package p\n\nfunc f() {\n\t_ = new(\n",
1931 "package p\n\nfunc f() {\n\t_ = new",
1932 "package p\n\nfunc f() {\n\t_ = new(",
1933 "package p\n\nfunc f() {\n\t_ = new()\n}\n",
1934 "new",
1935 "new(",
1936 "",
1937 ] {
1938 let _ = parse(source);
1939 }
1940 }
1941
1942 #[test]
1943 fn the_scan_reads_new_the_way_go_spells_it() {
1944 let go = obligations(concat!(
1950 "package p\n\n",
1951 "type pool struct{}\n\n",
1952 "func (p pool) new(v int) *int {\n\treturn &v\n}\n\n",
1953 "func renew(v string) *string {\n\treturn &v\n}\n\n",
1954 "func f(p pool, v int) *int {\n",
1955 "\t_ = new (\"spaced\")\n",
1957 "\t_ = new\t(\"tabbed\")\n",
1958 "\t_ = p.new(2)\n",
1960 "\t_ = renew(\"x\")\n",
1962 "\tnewValue := v\n",
1964 "\treturn new(newValue)\n}\n",
1965 ));
1966 assert_eq!(
1967 go.manifest
1968 .points
1969 .iter()
1970 .filter(|p| p.kind == PointKind::Function)
1971 .count(),
1972 3,
1973 "{:?}",
1974 go.manifest.points
1975 );
1976 assert!(
1978 go.manifest
1979 .points
1980 .iter()
1981 .any(|point| point.source.contains("new (\"spaced\")")),
1982 "{:?}",
1983 go.manifest.points
1984 );
1985 assert!(
1986 !go.manifest
1987 .points
1988 .iter()
1989 .any(|point| point.source.contains("nEw")),
1990 "{:?}",
1991 go.manifest.points
1992 );
1993 }
1994
1995 #[test]
1996 fn a_file_that_is_simply_broken_still_does_not_parse() {
1997 let mut next = 0;
2001 let mut decisions = 0;
2002 assert!(matches!(
2003 build_go_obligations(
2004 "broken.go",
2005 "package p\n\nfunc f() {\n\t_ = new(1)\n\tif {\n}\n",
2006 &mut next,
2007 &mut decisions
2008 ),
2009 Err(GoInstrumenterError::Parse(_))
2010 ));
2011 }
2012
2013 #[test]
2014 fn decisions_are_numbered_across_the_module_not_within_a_file() {
2015 let mut next = 0;
2020 let mut decisions = 0;
2021 let first = build_go_obligations(
2022 "a.go",
2023 "package p\n\nfunc A(x, y bool) bool {\n\tif x && y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
2024 &mut next,
2025 &mut decisions,
2026 )
2027 .unwrap();
2028 let second = build_go_obligations(
2029 "b.go",
2030 "package p\n\nfunc B(x, y bool) bool {\n\tif x || y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
2031 &mut next,
2032 &mut decisions,
2033 )
2034 .unwrap();
2035
2036 let referenced = |obligations: &GoFileObligations| {
2037 obligations
2038 .edits
2039 .iter()
2040 .filter_map(|edit| {
2041 let at = edit.text.find(".C(")?;
2042 edit.text[at + 3..]
2043 .split(',')
2044 .next()?
2045 .trim()
2046 .parse::<u32>()
2047 .ok()
2048 })
2049 .collect::<std::collections::BTreeSet<_>>()
2050 };
2051 assert_eq!(referenced(&first), [0].into());
2052 assert_eq!(referenced(&second), [1].into());
2053 assert_eq!(decisions, 2, "the module numbered two decisions in all");
2054 assert_eq!(first.decision_widths, [2]);
2057 assert_eq!(second.decision_widths, [2]);
2058 }
2059}