use std::collections::BTreeMap;
use tree_sitter::{Node, Parser};
use crate::coverage_analysis::PointKind;
use crate::coverage_report::{
BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GoInstrumenterError {
Parse(String),
}
impl std::fmt::Display for GoInstrumenterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GoInstrumenterError::Parse(detail) => write!(f, "Go parse error: {detail}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GoProbeTarget {
Statement {
id: String,
},
Function {
id: String,
},
Alternative {
branch: String,
alternative: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GoProbe {
pub id: u64,
pub target: GoProbeTarget,
pub at: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct GoFileObligations {
pub manifest: CoverageManifest,
pub probes: BTreeMap<u64, GoProbe>,
pub edits: Vec<GoEdit>,
pub decision_widths: Vec<u8>,
}
pub(crate) fn stable_obligation_id(
language: &str,
file: &str,
kind: &str,
start: usize,
end: usize,
) -> String {
use sha2::{Digest, Sha256};
let mut hash = Sha256::new();
for value in [file, kind, &start.to_string(), &end.to_string()] {
hash.update(value.as_bytes());
hash.update([0]);
}
let digest = hash.finalize();
let mut encoded = String::with_capacity(24);
for byte in &digest[..12] {
use std::fmt::Write as _;
write!(&mut encoded, "{byte:02x}").expect("writing to a string cannot fail");
}
format!("{language}:{kind}:{encoded}")
}
fn parse_exactly(source: &str) -> Result<tree_sitter::Tree, GoInstrumenterError> {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_go::LANGUAGE.into())
.map_err(|error| GoInstrumenterError::Parse(error.to_string()))?;
let tree = parser
.parse(source, None)
.ok_or_else(|| GoInstrumenterError::Parse("parser returned no tree".into()))?;
if tree.root_node().has_error() {
return Err(GoInstrumenterError::Parse(parse_failure(&tree, source)));
}
Ok(tree)
}
const NEW_VALUE_STAND_IN: &str = "nEw";
fn stand_in_for_new_values(source: &str) -> Option<String> {
let bytes = source.as_bytes();
let mut masked: Option<String> = None;
let mut at = 0;
while at < bytes.len() {
if let Some(past) = skip_literal_or_comment(bytes, at) {
at = past;
continue;
}
let Some(open) = builtin_new_at(bytes, at) else {
at += 1;
continue;
};
let Some(close) = closing_parenthesis(bytes, open) else {
at += 3;
continue;
};
if !parses_as_a_type(&source[open + 1..close]) {
masked
.get_or_insert_with(|| source.to_owned())
.replace_range(at..at + 3, NEW_VALUE_STAND_IN);
}
at += 3;
}
masked
}
fn builtin_new_at(bytes: &[u8], at: usize) -> Option<usize> {
if !bytes[at..].starts_with(b"new") {
return None;
}
if at > 0 && (is_identifier_byte(bytes[at - 1]) || bytes[at - 1] == b'.') {
return None;
}
let mut open = at + 3;
while open < bytes.len() && bytes[open].is_ascii_whitespace() {
open += 1;
}
(bytes.get(open) == Some(&b'(')).then_some(open)
}
fn is_identifier_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'_' || byte >= 0x80
}
fn closing_parenthesis(bytes: &[u8], open: usize) -> Option<usize> {
let mut depth = 0_usize;
let mut at = open;
while at < bytes.len() {
if let Some(past) = skip_literal_or_comment(bytes, at) {
at = past;
continue;
}
match bytes[at] {
b'(' => depth += 1,
b')' => {
depth = depth.saturating_sub(1);
if depth == 0 {
return Some(at);
}
}
_ => {}
}
at += 1;
}
None
}
fn parses_as_a_type(argument: &str) -> bool {
parse_exactly(&format!("package p\n\nvar _ {}\n", argument.trim())).is_ok()
}
fn skip_literal_or_comment(bytes: &[u8], at: usize) -> Option<usize> {
let rest = &bytes[at..];
if rest.starts_with(b"//") {
return Some(
bytes[at..]
.iter()
.position(|byte| *byte == b'\n')
.map_or(bytes.len(), |end| at + end),
);
}
if rest.starts_with(b"/*") {
return Some(
bytes[at + 2..]
.windows(2)
.position(|pair| pair == b"*/")
.map_or(bytes.len(), |end| at + 4 + end),
);
}
if bytes[at] == b'`' {
return Some(
bytes[at + 1..]
.iter()
.position(|byte| *byte == b'`')
.map_or(bytes.len(), |end| at + 2 + end),
);
}
let quote = match bytes[at] {
b'"' | b'\'' => bytes[at],
_ => return None,
};
let mut cursor = at + 1;
while cursor < bytes.len() {
match bytes[cursor] {
b'\\' => cursor += 2,
b'\n' => return Some(cursor),
byte if byte == quote => return Some(cursor + 1),
_ => cursor += 1,
}
}
Some(bytes.len())
}
pub fn parse(source: &str) -> Result<tree_sitter::Tree, GoInstrumenterError> {
let refusal = match parse_exactly(source) {
Ok(tree) => return Ok(tree),
Err(refusal) => refusal,
};
let stand_in = stand_in_for_new_values(source).ok_or_else(|| refusal.clone())?;
parse_exactly(&stand_in).map_err(|_| refusal)
}
pub(crate) fn parse_failure(tree: &tree_sitter::Tree, source: &str) -> String {
let mut deepest: Option<(usize, Node)> = None;
let mut stack = vec![(0_usize, tree.root_node())];
while let Some((depth, node)) = stack.pop() {
if (node.is_error() || node.is_missing()) && deepest.is_none_or(|(found, _)| depth > found)
{
deepest = Some((depth, node));
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
stack.push((depth + 1, child));
}
}
let Some((_, node)) = deepest else {
return "source does not parse".to_owned();
};
let start = node.start_position();
let mut line = source
.lines()
.nth(start.row)
.unwrap_or_default()
.trim()
.to_owned();
if line.chars().count() > 120 {
line = line.chars().take(117).collect::<String>() + "...";
}
let what = if node.is_missing() {
"missing syntax"
} else {
"unexpected syntax"
};
let end = node.end_position().row + 1;
let through = if end > start.row + 1 {
format!(" (through line {end})")
} else {
String::new()
};
format!(
"{what} at line {}, column {}{through}: {line}",
start.row + 1,
start.column + 1,
)
}
fn is_statement(kind: &str) -> bool {
matches!(
kind,
"assignment_statement"
| "break_statement"
| "const_declaration"
| "continue_statement"
| "dec_statement"
| "defer_statement"
| "expression_statement"
| "expression_switch_statement"
| "fallthrough_statement"
| "for_statement"
| "go_statement"
| "goto_statement"
| "if_statement"
| "inc_statement"
| "labeled_statement"
| "return_statement"
| "select_statement"
| "send_statement"
| "short_var_declaration"
| "type_switch_statement"
| "var_declaration"
)
}
struct Collector<'a> {
file: &'a str,
source: &'a str,
alias: &'a str,
next_probe: &'a mut u64,
edits: Vec<GoEdit>,
points: Vec<PointMeta>,
branches: Vec<BranchMeta>,
decisions: Vec<DecisionMeta>,
probes: BTreeMap<u64, GoProbe>,
limitations: Vec<serde_json::Value>,
widths: Vec<u8>,
decision_base: u32,
}
fn branch_wrapper(alias: &str, when_true: u64, when_false: u64, decision: Option<usize>) -> String {
match decision {
Some(index) => format!("{alias}.BD({when_true}, {when_false}, {index}, "),
None => format!("{alias}.B({when_true}, {when_false}, "),
}
}
fn in_statement_position(node: Node) -> bool {
node.parent()
.is_some_and(|parent| parent.kind() == "statement_list")
}
fn loop_condition<'t>(node: Node<'t>) -> Option<Node<'t>> {
if let Some(condition) = node.child_by_field_name("condition") {
return Some(condition);
}
let mut cursor = node.walk();
let clause = node
.children(&mut cursor)
.find(|child| child.kind() == "for_clause")?;
let mut inner = clause.walk();
clause
.children(&mut inner)
.find(|child| child.is_named() && child.kind().ends_with("_expression"))
}
impl<'a> Collector<'a> {
fn id(&mut self, node: Node, kind: &str) -> String {
stable_obligation_id("go", self.file, kind, node.start_byte(), node.end_byte())
}
fn limitation_id(&mut self, node: Node, kind: &str) -> String {
self.id(node, kind)
}
fn probe(&mut self, target: GoProbeTarget, at: usize) -> u64 {
*self.next_probe += 1;
let id = *self.next_probe;
self.probes.insert(id, GoProbe { id, target, at });
id
}
fn edit(&mut self, at: usize, rank: i32, text: String) {
self.edits.push(GoEdit { at, rank, text });
}
fn call_before(&mut self, at: usize, call: String) {
self.edit(at, 100, format!("{call}; "));
}
fn position(&self, node: Node) -> (usize, usize) {
let start = node.start_position();
(start.row + 1, start.column + 1)
}
fn text(&self, node: Node) -> String {
let raw = &self.source[node.byte_range()];
let line = raw.lines().next().unwrap_or("");
line.trim().to_owned()
}
fn add_point(&mut self, node: Node, kind: PointKind, label: Option<String>) {
let (line, column) = self.position(node);
let id = self.id(
node,
match kind {
PointKind::Function => "function",
PointKind::Statement => "statement",
},
);
let target = match kind {
PointKind::Function => GoProbeTarget::Function { id: id.clone() },
PointKind::Statement => GoProbeTarget::Statement { id: id.clone() },
};
let at = match kind {
PointKind::Function => match node.child_by_field_name("body") {
Some(body) => body.start_byte() + 1,
None => return,
},
PointKind::Statement => node.start_byte(),
};
let probe = self.probe(target, at);
self.call_before(at, format!("{HITS_VARIABLE}[{probe}] = 2"));
self.points.push(PointMeta {
id,
kind,
file: self.file.to_owned(),
line,
column,
source: self.text(node),
label,
});
}
fn add_branch(&mut self, node: Node, kind: &str, labels: &[&str]) -> Vec<u64> {
let (line, column) = self.position(node);
let id = self.id(node, "branch");
let mut probes = Vec::new();
let alternatives = labels
.iter()
.map(|label| {
let alternative = format!("{id}.{label}");
probes.push(self.probe(
GoProbeTarget::Alternative {
branch: id.clone(),
alternative: alternative.clone(),
},
node.start_byte(),
));
BranchAlternativeMeta {
id: alternative,
label: (*label).to_owned(),
}
})
.collect();
self.branches.push(BranchMeta {
id,
kind: kind.to_owned(),
file: self.file.to_owned(),
line,
column,
source: self.text(node),
alternatives,
});
probes
}
fn add_decision(&mut self, node: Node, kind: &str) -> Option<usize> {
let mut leaves = Vec::new();
condition_nodes(node, self.source, &mut leaves);
if leaves.len() < 2 {
return None;
}
let conditions = leaves
.iter()
.map(|leaf| self.source[leaf.byte_range()].trim().to_owned())
.collect::<Vec<_>>();
let (line, column) = self.position(node);
let id = self.id(node, "decision");
let index_of_decision = self.decision_base as usize + self.widths.len();
self.widths.push(leaves.len().min(64) as u8);
let alias = self.alias.to_owned();
for (index, leaf) in leaves.iter().enumerate() {
self.edit(
leaf.start_byte(),
20,
format!("{alias}.C({index_of_decision}, {index}, "),
);
self.edit(leaf.end_byte(), 20, ")".to_owned());
}
self.decisions.push(DecisionMeta {
id,
file: self.file.to_owned(),
line,
column,
source: self.text(node),
conditions,
kind: kind.to_owned(),
});
Some(index_of_decision)
}
fn walk(&mut self, node: Node) {
let alias = self.alias.to_owned();
match node.kind() {
"function_declaration" | "method_declaration" | "func_literal" => {
let label = node
.child_by_field_name("name")
.map(|name| self.source[name.byte_range()].to_owned());
self.add_point(node, PointKind::Function, label);
}
"if_statement" => {
if let Some(condition) = node.child_by_field_name("condition") {
let probes = self.add_branch(node, "if", &["true", "false"]);
let decision = self.add_decision(condition, "if");
self.edit(
condition.start_byte(),
5,
branch_wrapper(&alias, probes[0], probes[1], decision),
);
self.edit(condition.end_byte(), 5, ")".to_owned());
}
}
"for_statement" => {
match loop_condition(node) {
Some(condition) => {
let probes = self.add_branch(node, "loop", &["true", "false"]);
let decision = self.add_decision(condition, "loop");
self.edit(
condition.start_byte(),
5,
branch_wrapper(&alias, probes[0], probes[1], decision),
);
self.edit(condition.end_byte(), 5, ")".to_owned());
}
None => {
let (line, column) = self.position(node);
let limitation = self.limitation_id(node, "loop-without-condition");
self.limitations.push(serde_json::json!({
"id": limitation,
"kind": "loop-without-condition",
"file": self.file,
"source": self.text(node),
"line": line,
"column": column,
"reason": "a range or unconditional loop has no condition to observe, so no branch obligation is recorded for it",
}));
}
}
}
"expression_switch_statement" | "type_switch_statement" | "select_statement" => {
let kind = match node.kind() {
"expression_switch_statement" => "switch",
"type_switch_statement" => "type-switch",
_ => "select",
};
let mut cases = Vec::new();
let mut has_default = false;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"expression_case" | "type_case" | "communication_case" => {
cases.push((self.text(child), Some(child)))
}
"default_case" => {
has_default = true;
cases.push(("default".to_owned(), Some(child)));
}
_ => {}
}
}
if !has_default && node.kind() != "select_statement" {
cases.push(("no case matched".to_owned(), None));
}
let labels = cases
.iter()
.map(|(label, _)| label.as_str())
.collect::<Vec<_>>();
let probes = self.add_branch(node, kind, &labels);
for (probe, (_, clause)) in probes.iter().zip(cases.iter()) {
match clause {
Some(clause) => {
let at = clause
.children(&mut clause.walk())
.find(|child| child.kind() == "statement_list")
.map(|body| body.start_byte())
.unwrap_or_else(|| clause.end_byte());
self.call_before(at, format!("{alias}.A({probe})"));
}
None => {
let at = node.end_byte().saturating_sub(1);
self.edit(at, 100, format!("\ndefault:\n{alias}.A({probe})\n"));
}
}
}
}
kind if is_statement(kind) && in_statement_position(node) => {
self.add_point(node, PointKind::Statement, None);
}
_ => {}
}
if in_statement_position(node)
&& matches!(
node.kind(),
"if_statement"
| "for_statement"
| "expression_switch_statement"
| "type_switch_statement"
| "select_statement"
)
{
self.add_point(node, PointKind::Statement, None);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.is_named() {
self.walk(child);
}
}
}
}
fn condition_nodes<'t>(node: Node<'t>, source: &str, out: &mut Vec<Node<'t>>) {
match node.kind() {
"binary_expression" => {
let operator = node
.child_by_field_name("operator")
.map(|op| &source[op.byte_range()])
.unwrap_or("");
if operator == "&&" || operator == "||" {
if let Some(left) = node.child_by_field_name("left") {
condition_nodes(left, source, out);
}
if let Some(right) = node.child_by_field_name("right") {
condition_nodes(right, source, out);
}
return;
}
out.push(node);
}
"parenthesized_expression" => {
let mut cursor = node.walk();
match node.children(&mut cursor).find(|child| child.is_named()) {
Some(inner) => condition_nodes(inner, source, out),
None => out.push(node),
}
}
_ => out.push(node),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GoEdit {
pub at: usize,
pub rank: i32,
pub text: String,
}
pub fn rewrite(source: &str, edits: &[GoEdit]) -> String {
let mut ordered = edits.to_vec();
ordered.sort_by(|a, b| b.at.cmp(&a.at).then(b.rank.cmp(&a.rank)));
let mut out = source.to_owned();
for edit in ordered {
if edit.at > out.len() {
continue;
}
out.insert_str(edit.at, &edit.text);
}
out
}
pub fn import_edit(source: &str, alias: &str, path: &str) -> Option<GoEdit> {
let tree = parse(source).ok()?;
let mut cursor = tree.root_node().walk();
let package = tree
.root_node()
.children(&mut cursor)
.find(|child| child.kind() == "package_clause")?;
Some(GoEdit {
at: package.end_byte(),
rank: 0,
text: format!("\nimport {alias} \"{path}\""),
})
}
pub fn build_go_obligations(
file: &str,
source: &str,
next_probe: &mut u64,
next_decision: &mut u32,
) -> Result<GoFileObligations, GoInstrumenterError> {
build_go_obligations_with_alias(file, source, next_probe, next_decision, RUNTIME_ALIAS)
}
pub const RUNTIME_ALIAS: &str = "__supercov";
pub const HITS_VARIABLE: &str = "__supercovHits";
pub const RUNTIME_IMPORT: &str = "github.com/supercorp-ai/supercov/runtime/go/supercov";
pub fn build_go_obligations_with_alias(
file: &str,
source: &str,
next_probe: &mut u64,
next_decision: &mut u32,
alias: &str,
) -> Result<GoFileObligations, GoInstrumenterError> {
let tree = parse(source)?;
let decision_base = *next_decision;
let mut collector = Collector {
file,
source,
alias,
next_probe,
decision_base,
edits: Vec::new(),
points: Vec::new(),
branches: Vec::new(),
decisions: Vec::new(),
probes: BTreeMap::new(),
limitations: Vec::new(),
widths: Vec::new(),
};
let mut cursor = tree.root_node().walk();
for child in tree.root_node().children(&mut cursor) {
if child.is_named() {
collector.walk(child);
}
}
*next_decision += collector.widths.len() as u32;
let mut edits = collector.edits;
let calls_runtime = edits
.iter()
.any(|edit| edit.text.contains(&format!("{alias}.")));
if calls_runtime && let Some(import) = import_edit(source, alias, RUNTIME_IMPORT) {
edits.push(import);
}
Ok(GoFileObligations {
manifest: CoverageManifest {
decisions: collector.decisions,
points: collector.points,
branches: collector.branches,
limitations: collector.limitations,
unmeasured: Vec::new(),
scope: None,
},
probes: collector.probes,
edits,
decision_widths: collector.widths,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_indexable(limitations: &[serde_json::Value]) -> Vec<String> {
assert!(!limitations.is_empty(), "nothing to check");
for limitation in limitations {
for field in ["id", "kind", "file", "source", "reason"] {
assert!(
limitation.get(field).and_then(|v| v.as_str()).is_some(),
"a limitation needs a string {field}: {limitation}"
);
}
for field in ["line", "column"] {
assert!(
limitation.get(field).and_then(|v| v.as_u64()).is_some(),
"a limitation needs a number {field}: {limitation}"
);
}
}
let mut kinds = limitations
.iter()
.filter_map(|limitation| limitation["kind"].as_str().map(str::to_owned))
.collect::<Vec<_>>();
kinds.sort();
kinds.dedup();
kinds
}
#[test]
fn a_file_that_does_not_parse_says_where() {
let broken = "package main\n\nfunc f() int {\n\treturn 1 )\n}\n";
let message = parse(broken).expect_err("does not parse").to_string();
assert!(message.contains("line 4"), "{message}");
assert!(message.contains("return 1"), "{message}");
assert!(
!message.contains("byte"),
"an offset is not somewhere a reader can look: {message}"
);
}
#[test]
fn every_limitation_carries_what_the_index_stores() {
const EVERY: &str = r#"package main
func walk(items []int) int {
sum := 0
for _, item := range items {
sum += item
}
for {
break
}
return sum
}
"#;
let mut next = 0;
let mut decisions = 0;
let obligations =
build_go_obligations("walk.go", EVERY, &mut next, &mut decisions).expect("go");
assert_eq!(
assert_indexable(&obligations.manifest.limitations),
["loop-without-condition"]
);
}
const SAMPLE: &str = r#"package main
import "fmt"
func classify(a int, b bool) string {
if a > 10 && b {
return "big"
}
for i := 0; i < a; i++ {
fmt.Println(i)
}
switch {
case a == 0:
return "zero"
default:
return "small"
}
}
"#;
fn obligations(source: &str) -> GoFileObligations {
let mut next = 0;
let mut decisions = 0;
build_go_obligations("main.go", source, &mut next, &mut decisions).expect("obligations")
}
fn rewritten(source: &str) -> String {
let mut next = 0;
let mut decisions = 0;
let go =
build_go_obligations("x.go", source, &mut next, &mut decisions).expect("obligations");
let out = rewrite(source, &go.edits);
parse(&out)
.unwrap_or_else(|error| panic!("rewritten source does not parse: {error}\n{out}"));
out
}
#[test]
fn a_probe_never_lands_where_go_does_not_allow_a_statement() {
let out = rewritten(
"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",
);
assert!(
!out.contains("for __supercov"),
"probe in a for-clause init:\n{out}"
);
assert!(
!out.contains("; __supercov.P"),
"probe in a for-clause post:\n{out}"
);
assert!(
out.contains("if b := a;"),
"the if initialiser survived intact:\n{out}"
);
assert!(
out.contains("switch c := a;"),
"the switch initialiser survived intact:\n{out}"
);
}
#[test]
fn wrapping_a_condition_preserves_short_circuit_order() {
let out = rewritten(
"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",
);
let condition = out
.lines()
.find(|line| line.contains("if "))
.expect("the if survived");
let branch = condition.find(".BD(").expect("branch and decision wrapper");
let first = condition.find(".C(").expect("first condition wrapper");
assert!(
branch < first,
"the branch must enclose its conditions: {condition}"
);
assert_eq!(
condition.matches(".C(").count(),
2,
"one wrapper per condition: {condition}"
);
assert!(
condition.contains("&&"),
"the operator itself is untouched: {condition}"
);
}
#[test]
fn a_single_condition_branch_uses_the_wrapper_that_inlines() {
let out = rewritten(
"package main\nfunc f(a int) bool {\n\tif a > 10 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
);
assert!(out.contains(".B("), "{out}");
assert!(!out.contains(".BD("), "no decision here to close:\n{out}");
}
#[test]
fn a_switch_without_a_default_gains_one_so_matching_nothing_is_observable() {
let out = rewritten(
"package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
);
assert!(out.contains("default:"), "{out}");
let existing = rewritten(
"package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\tdefault:\n\t\treturn\n\t}\n}\n",
);
assert_eq!(existing.matches("default:").count(), 1, "{existing}");
}
#[test]
fn a_loop_with_no_condition_records_a_limitation_not_an_obligation() {
let mut next = 0;
let mut decisions = 0;
let go = build_go_obligations(
"x.go",
"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",
&mut next,
&mut decisions,
)
.unwrap();
assert!(
go.manifest.branches.iter().all(|b| b.kind != "loop"),
"{:?}",
go.manifest.branches
);
assert_eq!(
go.manifest.limitations.len(),
2,
"{:?}",
go.manifest.limitations
);
assert_eq!(go.manifest.limitations[0]["kind"], "loop-without-condition");
}
#[test]
fn only_a_file_that_gained_a_probe_imports_the_runtime() {
let out = rewritten("package main\nfunc f(a int) int {\n\treturn a\n}\n");
assert!(
out.contains(RUNTIME_ALIAS),
"a function is itself an obligation:\n{out}"
);
let mut next = 0;
let mut decisions = 0;
let bare = build_go_obligations(
"t.go",
"package main\n\ntype T struct{}\n",
&mut next,
&mut decisions,
)
.unwrap();
assert!(bare.edits.is_empty(), "{:?}", bare.edits);
assert_eq!(
rewrite("package main\n\ntype T struct{}\n", &bare.edits),
"package main\n\ntype T struct{}\n"
);
}
#[test]
fn a_short_circuiting_operator_splits_a_decision_and_nothing_else_does() {
let go = obligations(SAMPLE);
let decision = go
.manifest
.decisions
.iter()
.find(|d| d.kind == "if")
.expect("the if carries a decision");
assert_eq!(decision.conditions, ["a > 10", "b"]);
let simple = obligations(
"package main\nfunc f(a int) bool {\n\tif a > 1 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
);
assert!(
simple.manifest.decisions.is_empty(),
"{:?}",
simple.manifest.decisions
);
}
#[test]
fn negation_and_parentheses_do_not_invent_conditions() {
let go = obligations(
"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",
);
let decision = &go.manifest.decisions[0];
assert_eq!(decision.conditions, ["a > 1", "!b", "c"]);
}
#[test]
fn every_branching_construct_records_the_outcome_that_was_not_taken() {
let go = obligations(SAMPLE);
let by_kind = |kind: &str| {
go.manifest
.branches
.iter()
.find(|b| b.kind == kind)
.map(|b| {
b.alternatives
.iter()
.map(|a| a.label.clone())
.collect::<Vec<_>>()
})
};
assert_eq!(by_kind("if").unwrap(), ["true", "false"]);
assert_eq!(by_kind("loop").unwrap(), ["true", "false"]);
let switch = by_kind("switch").unwrap();
assert!(switch.contains(&"default".to_owned()), "{switch:?}");
assert!(
!switch.contains(&"no case matched".to_owned()),
"a default already covers it"
);
let open = obligations(
"package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
);
let labels = open
.manifest
.branches
.iter()
.find(|b| b.kind == "switch")
.unwrap()
.alternatives
.iter()
.map(|a| a.label.clone())
.collect::<Vec<_>>();
assert!(labels.contains(&"no case matched".to_owned()), "{labels:?}");
}
#[test]
fn functions_and_statements_are_separate_obligations_with_locations() {
let go = obligations(SAMPLE);
let functions = go
.manifest
.points
.iter()
.filter(|p| p.kind == PointKind::Function)
.collect::<Vec<_>>();
assert_eq!(functions.len(), 1);
assert_eq!(functions[0].label.as_deref(), Some("classify"));
assert_eq!(functions[0].line, 5);
let statements = go
.manifest
.points
.iter()
.filter(|p| p.kind == PointKind::Statement)
.count();
assert!(
statements >= 6,
"expected the body's statements, got {statements}"
);
assert!(
go.manifest
.points
.iter()
.all(|p| p.line > 0 && p.column > 0)
);
assert!(
!go.manifest
.points
.iter()
.any(|p| p.source.starts_with("import"))
);
}
#[test]
fn every_obligation_has_a_probe_and_malformed_source_is_refused() {
let go = obligations(SAMPLE);
let expected = go.manifest.points.len()
+ go.manifest
.branches
.iter()
.map(|b| b.alternatives.len())
.sum::<usize>();
assert_eq!(go.probes.len(), expected);
assert_eq!(go.decision_widths.len(), go.manifest.decisions.len());
let mut next = 0;
let mut decisions = 0;
assert!(matches!(
build_go_obligations(
"broken.go",
"package main\nfunc f( {",
&mut next,
&mut decisions
),
Err(GoInstrumenterError::Parse(_))
));
}
#[test]
fn go_1_26_new_of_a_value_is_measured_rather_than_dropped() {
let go = obligations(
"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",
);
assert_eq!(
go.manifest
.points
.iter()
.filter(|p| p.kind == PointKind::Function)
.count(),
3,
"every function in the file is an obligation: {:?}",
go.manifest.points
);
assert_eq!(
go.manifest.branches.len(),
1,
"the `if` beside it is still a branch with both arms to answer for"
);
assert!(
go.manifest
.points
.iter()
.any(|point| point.source.contains("new(\"hello\")")),
"{:?}",
go.manifest.points
);
assert!(
!go.manifest
.points
.iter()
.any(|point| point.source.contains("nEw")),
"{:?}",
go.manifest.points
);
}
#[test]
fn new_of_a_type_is_left_exactly_as_it_was() {
let go = obligations(
"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",
);
assert_eq!(
go.manifest
.points
.iter()
.filter(|p| p.kind == PointKind::Function)
.count(),
1
);
let mixed = obligations(
"package p\n\nfunc Mixed(v int) {\n\t_ = new(map[string][]chan int)\n\t_ = new(v)\n\t_ = new(\"x\")\n}\n",
);
assert_eq!(
mixed
.manifest
.points
.iter()
.filter(|p| p.kind == PointKind::Function)
.count(),
1
);
}
#[test]
fn a_method_called_new_is_not_the_builtin() {
let go = obligations(
"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",
);
assert!(
go.manifest
.points
.iter()
.any(|point| point.source.contains("p.new(2)")),
"{:?}",
go.manifest.points
);
}
#[test]
fn a_file_with_runes_outside_ascii_is_scanned_without_splitting_one() {
let go = obligations(
"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",
);
assert_eq!(
go.manifest
.points
.iter()
.filter(|p| p.kind == PointKind::Function)
.count(),
1
);
}
#[test]
fn the_scan_never_mistakes_text_for_code() {
let go = obligations(concat!(
"package p\n\n",
"func f(v int) *int {\n",
"\t_ = \"new(\\\"x\\\") and an unclosed ( paren\"\n",
"\t_ = `raw new(\"y\") with ) and \\ backslash`\n",
"\t_ = '('\n",
"\t// new(\"comment\") and a stray )\n",
"\t/* new(\"block\") ) */\n",
"\treturn new(v + 1)\n}\n",
));
assert_eq!(
go.manifest
.points
.iter()
.filter(|p| p.kind == PointKind::Function)
.count(),
1
);
let untouched =
obligations("package p\n\nfunc g() string {\n\treturn `new(\"only in text\")`\n}\n");
assert!(
untouched
.manifest
.points
.iter()
.any(|point| point.source.contains("new(\"only in text\")")),
"{:?}",
untouched.manifest.points
);
}
#[test]
fn no_shape_of_input_can_make_the_scan_panic() {
let awkward = concat!(
"package p\n\n",
"// gr\u{fc}\u{df}e \u{2014} new(\"in a comment\")\n",
"func f(sch\u{f6}n int) *int {\n",
"\t_ = \"\u{fc}ber new(\\\"in a string\\\"\"\n",
"\t_ = `\u{e4}\u{f6}\u{fc} new(\n",
"\t_ = '\u{fc}'\n",
"\t/* new(\"unterminated\n",
"\treturn new(sch\u{f6}n + 1)\n}\n",
);
for end in 0..=awkward.len() {
if !awkward.is_char_boundary(end) {
continue;
}
let _ = parse(&awkward[..end]);
if awkward.is_char_boundary(end) {
let _ = parse(&awkward[end..]);
}
}
for source in [
"package p\n\nfunc f() {\n\t_ = new(\n",
"package p\n\nfunc f() {\n\t_ = new",
"package p\n\nfunc f() {\n\t_ = new(",
"package p\n\nfunc f() {\n\t_ = new()\n}\n",
"new",
"new(",
"",
] {
let _ = parse(source);
}
}
#[test]
fn the_scan_reads_new_the_way_go_spells_it() {
let go = obligations(concat!(
"package p\n\n",
"type pool struct{}\n\n",
"func (p pool) new(v int) *int {\n\treturn &v\n}\n\n",
"func renew(v string) *string {\n\treturn &v\n}\n\n",
"func f(p pool, v int) *int {\n",
"\t_ = new (\"spaced\")\n",
"\t_ = new\t(\"tabbed\")\n",
"\t_ = p.new(2)\n",
"\t_ = renew(\"x\")\n",
"\tnewValue := v\n",
"\treturn new(newValue)\n}\n",
));
assert_eq!(
go.manifest
.points
.iter()
.filter(|p| p.kind == PointKind::Function)
.count(),
3,
"{:?}",
go.manifest.points
);
assert!(
go.manifest
.points
.iter()
.any(|point| point.source.contains("new (\"spaced\")")),
"{:?}",
go.manifest.points
);
assert!(
!go.manifest
.points
.iter()
.any(|point| point.source.contains("nEw")),
"{:?}",
go.manifest.points
);
}
#[test]
fn a_file_that_is_simply_broken_still_does_not_parse() {
let mut next = 0;
let mut decisions = 0;
assert!(matches!(
build_go_obligations(
"broken.go",
"package p\n\nfunc f() {\n\t_ = new(1)\n\tif {\n}\n",
&mut next,
&mut decisions
),
Err(GoInstrumenterError::Parse(_))
));
}
#[test]
fn decisions_are_numbered_across_the_module_not_within_a_file() {
let mut next = 0;
let mut decisions = 0;
let first = build_go_obligations(
"a.go",
"package p\n\nfunc A(x, y bool) bool {\n\tif x && y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
&mut next,
&mut decisions,
)
.unwrap();
let second = build_go_obligations(
"b.go",
"package p\n\nfunc B(x, y bool) bool {\n\tif x || y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
&mut next,
&mut decisions,
)
.unwrap();
let referenced = |obligations: &GoFileObligations| {
obligations
.edits
.iter()
.filter_map(|edit| {
let at = edit.text.find(".C(")?;
edit.text[at + 3..]
.split(',')
.next()?
.trim()
.parse::<u32>()
.ok()
})
.collect::<std::collections::BTreeSet<_>>()
};
assert_eq!(referenced(&first), [0].into());
assert_eq!(referenced(&second), [1].into());
assert_eq!(decisions, 2, "the module numbered two decisions in all");
assert_eq!(first.decision_widths, [2]);
assert_eq!(second.decision_widths, [2]);
}
}