use std::collections::{HashMap, HashSet};
use chrono::{DateTime, Utc};
use tree_sitter::{Node, Parser, TreeCursor};
use uuid::Uuid;
use crate::graph::types::{RelationType, Relationship, Symbol, SymbolType};
use crate::parser::python::{FileParseResult, RawImport};
pub fn parse_go_file(
file_path: &str,
source: &str,
project: &str,
file_mtime: DateTime<Utc>,
) -> FileParseResult {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_go::LANGUAGE.into())
.expect("failed to load Go grammar");
let Some(tree) = parser.parse(source, None) else {
tracing::warn!("tree-sitter failed to parse {file_path}");
return FileParseResult::default();
};
let source_bytes = source.as_bytes();
let root = tree.root_node();
let mut ctx = ParseContext {
file_path,
project,
file_mtime,
result: FileParseResult::default(),
name_to_id: HashMap::new(),
imported_names: HashSet::new(),
struct_fields: HashMap::new(),
};
let file_symbol_id = Uuid::new_v4();
ctx.result.symbols.push(Symbol {
id: file_symbol_id,
name: file_path.to_string(),
symbol_type: SymbolType::File,
file_path: file_path.to_string(),
start_line: Some(1),
end_line: Some(source.lines().count() as i32),
language: "go".to_string(),
project: project.to_string(),
signature: None,
file_mtime,
layer: None,
parent_symbol_id: None,
moniker: None,
});
let mut cursor = root.walk();
collect_imports(&root, source_bytes, &mut ctx, &mut cursor);
let mut cursor2 = root.walk();
collect_definitions(&root, file_symbol_id, source_bytes, &mut ctx, &mut cursor2);
let mut cursor3 = root.walk();
collect_calls(&root, source_bytes, &mut ctx, &mut cursor3);
let mut cursor4 = root.walk();
collect_type_annotations(&root, source_bytes, &mut ctx, &mut cursor4);
ctx.result
}
struct ParseContext<'a> {
file_path: &'a str,
project: &'a str,
file_mtime: DateTime<Utc>,
result: FileParseResult,
name_to_id: HashMap<String, Uuid>,
imported_names: HashSet<String>,
struct_fields: HashMap<(Uuid, String), Uuid>,
}
fn collect_imports<'a>(
node: &Node<'a>,
source: &[u8],
ctx: &mut ParseContext<'_>,
cursor: &mut TreeCursor<'a>,
) {
for child in node.children(cursor) {
if child.kind() == "import_declaration" {
process_import_declaration(&child, source, ctx);
}
}
}
fn process_import_declaration(node: &Node<'_>, source: &[u8], ctx: &mut ParseContext<'_>) {
let file_id = ctx.result.symbols[0].id;
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
match child.kind() {
"interpreted_string_literal" | "raw_string_literal" => {
let path = unquote(node_text(&child, source));
emit_import(file_id, &path, None, ctx);
}
"import_spec_list" => {
let mut list_cursor = child.walk();
for spec in child.named_children(&mut list_cursor) {
if spec.kind() == "import_spec" {
process_import_spec(&spec, source, file_id, ctx);
}
}
}
"import_spec" => {
process_import_spec(&child, source, file_id, ctx);
}
_ => {}
}
}
}
fn process_import_spec(node: &Node<'_>, source: &[u8], file_id: Uuid, ctx: &mut ParseContext<'_>) {
let path_node = node.child_by_field_name("path");
let alias_node = node.child_by_field_name("name");
let path = path_node
.map(|n| unquote(node_text(&n, source)))
.unwrap_or_default();
if path.is_empty() {
return;
}
let alias = alias_node.map(|n| node_text(&n, source));
emit_import(file_id, &path, alias.as_deref(), ctx);
}
fn emit_import(file_id: Uuid, import_path: &str, alias: Option<&str>, ctx: &mut ParseContext<'_>) {
if import_path.is_empty() {
return;
}
let local_name = match alias {
Some("_") | Some(".") | None => {
import_path
.rsplit('/')
.next()
.unwrap_or(import_path)
.to_string()
}
Some(a) => a.to_string(),
};
if !local_name.is_empty() && local_name != "_" && local_name != "." {
ctx.imported_names.insert(local_name);
}
ctx.result.raw_imports.push(RawImport {
source_id: file_id,
module_raw: import_path.to_string(),
is_relative: import_path.starts_with("./") || import_path.starts_with("../"),
dot_count: 0,
module_path: import_path.to_string(),
});
let target_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, import_path.as_bytes());
ctx.result.relationships.push(Relationship {
source_id: file_id,
target_id,
rel_type: RelationType::Imports,
confidence: 0.3,
});
}
fn collect_definitions<'a>(
node: &Node<'a>,
file_id: Uuid,
source: &[u8],
ctx: &mut ParseContext<'_>,
cursor: &mut TreeCursor<'a>,
) {
for child in node.children(cursor) {
match child.kind() {
"function_declaration" => {
process_function_declaration(&child, file_id, source, ctx);
}
"method_declaration" => {
process_method_declaration(&child, file_id, source, ctx);
}
"type_declaration" => {
process_type_declaration(&child, file_id, source, ctx);
}
_ => {}
}
}
}
fn process_function_declaration(
node: &Node<'_>,
file_id: Uuid,
source: &[u8],
ctx: &mut ParseContext<'_>,
) {
let name = node
.child_by_field_name("name")
.map(|n| node_text(&n, source))
.unwrap_or_else(|| "<anonymous>".to_string());
let signature = build_func_signature(node, &name, source, None);
let start_line = node.start_position().row as i32 + 1;
let end_line = node.end_position().row as i32 + 1;
let id = Uuid::new_v4();
ctx.name_to_id.insert(name.clone(), id);
ctx.result.symbols.push(Symbol {
id,
name: name.clone(),
symbol_type: SymbolType::Function,
file_path: ctx.file_path.to_string(),
start_line: Some(start_line),
end_line: Some(end_line),
language: "go".to_string(),
project: ctx.project.to_string(),
signature: Some(signature),
file_mtime: ctx.file_mtime,
layer: None,
parent_symbol_id: None,
moniker: None,
});
ctx.result.relationships.push(Relationship {
source_id: file_id,
target_id: id,
rel_type: RelationType::Defines,
confidence: 1.0,
});
}
fn process_method_declaration(
node: &Node<'_>,
file_id: Uuid,
source: &[u8],
ctx: &mut ParseContext<'_>,
) {
let name = node
.child_by_field_name("name")
.map(|n| node_text(&n, source))
.unwrap_or_else(|| "<anonymous>".to_string());
let receiver_type = node
.child_by_field_name("receiver")
.and_then(|recv| extract_receiver_type(&recv, source));
let signature = build_func_signature(node, &name, source, receiver_type.as_deref());
let start_line = node.start_position().row as i32 + 1;
let end_line = node.end_position().row as i32 + 1;
let id = Uuid::new_v4();
let qualified_name = if let Some(ref rt) = receiver_type {
format!("{rt}.{name}")
} else {
name.clone()
};
ctx.name_to_id.insert(qualified_name, id);
ctx.result.symbols.push(Symbol {
id,
name: name.clone(),
symbol_type: SymbolType::Method,
file_path: ctx.file_path.to_string(),
start_line: Some(start_line),
end_line: Some(end_line),
language: "go".to_string(),
project: ctx.project.to_string(),
signature: Some(signature),
file_mtime: ctx.file_mtime,
layer: None,
parent_symbol_id: None,
moniker: None,
});
let defines_source = if let Some(ref rt) = receiver_type {
ctx.name_to_id
.get(rt)
.copied()
.unwrap_or(file_id)
} else {
file_id
};
ctx.result.relationships.push(Relationship {
source_id: defines_source,
target_id: id,
rel_type: RelationType::Defines,
confidence: if receiver_type.is_some() && defines_source != file_id {
1.0
} else {
0.8
},
});
}
fn process_type_declaration(
node: &Node<'_>,
file_id: Uuid,
source: &[u8],
ctx: &mut ParseContext<'_>,
) {
let mut cursor = node.walk();
for spec in node.named_children(&mut cursor) {
if spec.kind() == "type_spec" {
process_type_spec(&spec, file_id, source, ctx);
}
}
}
fn process_type_spec(
node: &Node<'_>,
file_id: Uuid,
source: &[u8],
ctx: &mut ParseContext<'_>,
) {
let name = node
.child_by_field_name("name")
.map(|n| node_text(&n, source))
.unwrap_or_default();
if name.is_empty() {
return;
}
let type_node = node.child_by_field_name("type");
let type_kind = type_node.as_ref().map(|n| n.kind()).unwrap_or("");
let is_struct_or_iface = matches!(type_kind, "struct_type" | "interface_type");
if !is_struct_or_iface {
return;
}
let signature = format!("type {name} {type_kind}");
let start_line = node.start_position().row as i32 + 1;
let end_line = node.end_position().row as i32 + 1;
let id = Uuid::new_v4();
ctx.name_to_id.insert(name.clone(), id);
ctx.result.symbols.push(Symbol {
id,
name: name.clone(),
symbol_type: SymbolType::Class,
file_path: ctx.file_path.to_string(),
start_line: Some(start_line),
end_line: Some(end_line),
language: "go".to_string(),
project: ctx.project.to_string(),
signature: Some(signature),
file_mtime: ctx.file_mtime,
layer: None,
parent_symbol_id: None,
moniker: None,
});
ctx.result.relationships.push(Relationship {
source_id: file_id,
target_id: id,
rel_type: RelationType::Defines,
confidence: 1.0,
});
if let Some(type_body) = type_node {
collect_embeddings(&type_body, id, source, ctx);
if type_kind == "struct_type" {
collect_struct_fields(&type_body, id, source, ctx);
}
}
}
fn collect_struct_fields(
type_body: &Node<'_>,
struct_id: Uuid,
source: &[u8],
ctx: &mut ParseContext<'_>,
) {
let mut cursor = type_body.walk();
let list = type_body
.named_children(&mut cursor)
.find(|c| c.kind() == "field_declaration_list");
let Some(list) = list else {
return;
};
let mut lc = list.walk();
for child in list.named_children(&mut lc) {
if child.kind() != "field_declaration" {
continue;
}
let Some(name_node) = child.child_by_field_name("name") else {
continue; };
let name = node_text(&name_node, source);
if name.is_empty() {
continue;
}
let start_line = child.start_position().row as i32 + 1;
let end_line = child.end_position().row as i32 + 1;
let field_id = Uuid::new_v4();
ctx.struct_fields.insert((struct_id, name.clone()), field_id);
ctx.result.symbols.push(Symbol {
id: field_id,
name,
symbol_type: SymbolType::Field,
file_path: ctx.file_path.to_string(),
start_line: Some(start_line),
end_line: Some(end_line),
language: "go".to_string(),
project: ctx.project.to_string(),
signature: None,
file_mtime: ctx.file_mtime,
layer: None,
parent_symbol_id: Some(struct_id),
moniker: None,
});
ctx.result.relationships.push(Relationship {
source_id: struct_id,
target_id: field_id,
rel_type: RelationType::Defines,
confidence: 1.0,
});
}
}
fn collect_embeddings(
type_body: &Node<'_>,
owner_id: Uuid,
source: &[u8],
ctx: &mut ParseContext<'_>,
) {
let mut cursor = type_body.walk();
for child in type_body.named_children(&mut cursor) {
match child.kind() {
"field_declaration" => {
if child.child_by_field_name("name").is_none() {
if let Some(type_node) = child.child_by_field_name("type") {
let embedded = strip_pointer(node_text(&type_node, source));
let base_name = embedded.split('.').last().unwrap_or(&embedded).to_string();
if !base_name.is_empty() {
emit_inherits(owner_id, &base_name, ctx);
}
}
}
}
"type_name" | "qualified_type_identifier" => {
let embedded = node_text(&child, source);
let base_name = embedded.split('.').last().unwrap_or(&embedded).to_string();
if !base_name.is_empty() {
emit_inherits(owner_id, &base_name, ctx);
}
}
_ => {}
}
}
}
fn emit_inherits(owner_id: Uuid, base_name: &str, ctx: &mut ParseContext<'_>) {
let (target_id, confidence) = if let Some(&id) = ctx.name_to_id.get(base_name) {
(id, 1.0_f32)
} else if ctx.imported_names.contains(base_name) {
(Uuid::new_v5(&Uuid::NAMESPACE_OID, base_name.as_bytes()), 0.8)
} else {
(Uuid::new_v5(&Uuid::NAMESPACE_OID, base_name.as_bytes()), 0.5)
};
ctx.result.relationships.push(Relationship {
source_id: owner_id,
target_id,
rel_type: RelationType::Inherits,
confidence,
});
}
fn collect_calls<'a>(
node: &Node<'a>,
source: &[u8],
ctx: &mut ParseContext<'_>,
cursor: &mut TreeCursor<'a>,
) {
for child in node.children(cursor) {
if child.kind() == "call_expression" {
process_call(&child, source, ctx);
}
if child.kind() == "selector_expression" {
process_receiver_field_read(&child, source, ctx);
}
let mut inner = child.walk();
collect_calls(&child, source, ctx, &mut inner);
}
}
fn process_call(node: &Node<'_>, source: &[u8], ctx: &mut ParseContext<'_>) {
let Some(function_node) = node.child_by_field_name("function") else {
return;
};
let (callee_name, is_qualified) = extract_callee(&function_node, source);
if callee_name.is_empty() {
return;
}
let caller_id = find_enclosing_function(node, ctx);
let (target_id, confidence) = if let Some(&id) = ctx.name_to_id.get(&callee_name) {
(id, 1.0_f32)
} else if ctx.imported_names.contains(callee_name.split('.').next().unwrap_or("")) {
(
Uuid::new_v5(&Uuid::NAMESPACE_OID, callee_name.as_bytes()),
0.8,
)
} else if is_qualified {
(
Uuid::new_v5(&Uuid::NAMESPACE_OID, callee_name.as_bytes()),
0.6,
)
} else {
(
Uuid::new_v5(&Uuid::NAMESPACE_OID, callee_name.as_bytes()),
0.5,
)
};
let source_id = caller_id.unwrap_or(ctx.result.symbols[0].id);
ctx.result.relationships.push(Relationship {
source_id,
target_id,
rel_type: RelationType::Calls,
confidence,
});
}
fn extract_callee(node: &Node<'_>, source: &[u8]) -> (String, bool) {
match node.kind() {
"identifier" => (node_text(node, source), false),
"selector_expression" => {
let field = node
.child_by_field_name("field")
.map(|n| node_text(&n, source))
.unwrap_or_default();
(field, true)
}
_ => (String::new(), false),
}
}
fn find_enclosing_function(call_node: &Node<'_>, ctx: &ParseContext<'_>) -> Option<Uuid> {
let call_start = call_node.start_position().row as i32 + 1;
let mut best: Option<(Uuid, i32)> = None;
for sym in &ctx.result.symbols {
if !matches!(sym.symbol_type, SymbolType::Function | SymbolType::Method) {
continue;
}
let (start, end) = match (sym.start_line, sym.end_line) {
(Some(s), Some(e)) => (s, e),
_ => continue,
};
if call_start >= start && call_start <= end {
let range = end - start;
let current_best = best.map(|(_, r)| r).unwrap_or(i32::MAX);
if range < current_best {
best = Some((sym.id, range));
}
}
}
best.map(|(id, _)| id)
}
fn find_enclosing_receiver(node: &Node<'_>, source: &[u8]) -> Option<(String, String)> {
let mut current = node.parent()?;
loop {
if current.kind() == "method_declaration" {
if let Some(recv) = current.child_by_field_name("receiver") {
let type_name = extract_receiver_type(&recv, source).unwrap_or_default();
let mut cursor = recv.walk();
for param in recv.named_children(&mut cursor) {
if param.kind() == "parameter_declaration" {
if let Some(name_node) = param.child_by_field_name("name") {
let var_name = node_text(&name_node, source);
if !var_name.is_empty() && !type_name.is_empty() {
return Some((var_name, type_name));
}
}
}
}
}
return None;
}
current = current.parent()?;
}
}
fn process_receiver_field_read(node: &Node<'_>, source: &[u8], ctx: &mut ParseContext<'_>) {
let Some(obj) = node.child_by_field_name("operand") else {
return;
};
if obj.kind() != "identifier" {
return;
}
let obj_name = node_text(&obj, source);
let Some(field_node) = node.child_by_field_name("field") else {
return;
};
let field_name = node_text(&field_node, source);
if field_name.is_empty() {
return;
}
let Some((recv_var, type_name)) = find_enclosing_receiver(node, source) else {
return;
};
if obj_name != recv_var {
return;
}
if let Some(parent) = node.parent() {
if parent.kind() == "call_expression"
&& parent.child_by_field_name("function").map(|f| f.id()) == Some(node.id())
{
return;
}
}
let Some(&struct_id) = ctx.name_to_id.get(&type_name) else {
return;
};
let Some(&field_id) = ctx.struct_fields.get(&(struct_id, field_name.clone())) else {
return;
};
let source_id = find_enclosing_function(node, ctx)
.unwrap_or_else(|| ctx.result.symbols[0].id);
ctx.result.relationships.push(Relationship {
source_id,
target_id: field_id,
rel_type: RelationType::References,
confidence: 1.0,
});
}
fn collect_type_annotations<'a>(
node: &Node<'a>,
source: &[u8],
ctx: &mut ParseContext<'_>,
cursor: &mut TreeCursor<'a>,
) {
for child in node.children(cursor) {
match child.kind() {
"function_declaration" | "method_declaration" => {
process_func_type_annotations(&child, source, ctx);
}
_ => {}
}
}
}
fn process_func_type_annotations(node: &Node<'_>, source: &[u8], ctx: &mut ParseContext<'_>) {
let func_id = {
let name = node
.child_by_field_name("name")
.map(|n| node_text(&n, source))
.unwrap_or_default();
if name.is_empty() {
return;
}
let receiver_type = node
.child_by_field_name("receiver")
.and_then(|recv| extract_receiver_type(&recv, source));
let key = if let Some(ref rt) = receiver_type {
format!("{rt}.{name}")
} else {
name
};
match ctx.name_to_id.get(&key).copied() {
Some(id) => id,
None => return,
}
};
if let Some(params_node) = node.child_by_field_name("parameters") {
for type_name in extract_type_identifiers(¶ms_node, source) {
emit_uses_type(func_id, &type_name, ctx);
}
}
if let Some(result_node) = node.child_by_field_name("result") {
for type_name in extract_type_identifiers(&result_node, source) {
emit_uses_type(func_id, &type_name, ctx);
}
}
}
fn extract_type_identifiers(node: &Node<'_>, source: &[u8]) -> Vec<String> {
let mut result = Vec::new();
collect_type_ids_recursive(node, source, &mut result);
result
}
fn collect_type_ids_recursive(node: &Node<'_>, source: &[u8], out: &mut Vec<String>) {
match node.kind() {
"type_identifier" => {
let name = node_text(node, source);
if !name.is_empty() && !is_go_builtin(&name) {
out.push(name);
}
}
"qualified_type" => {
let full = node_text(node, source);
let name = full.split('.').last().unwrap_or(&full).trim().to_string();
if !name.is_empty() && !is_go_builtin(&name) {
out.push(name);
}
}
"pointer_type" | "slice_type" | "channel_type" => {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
collect_type_ids_recursive(&child, source, out);
}
}
"map_type" => {
if let Some(key) = node.child_by_field_name("key") {
collect_type_ids_recursive(&key, source, out);
}
if let Some(val) = node.child_by_field_name("value") {
collect_type_ids_recursive(&val, source, out);
}
}
_ => {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
collect_type_ids_recursive(&child, source, out);
}
}
}
}
fn is_go_builtin(name: &str) -> bool {
matches!(
name,
"int"
| "int8"
| "int16"
| "int32"
| "int64"
| "uint"
| "uint8"
| "uint16"
| "uint32"
| "uint64"
| "float32"
| "float64"
| "complex64"
| "complex128"
| "byte"
| "rune"
| "string"
| "bool"
| "error"
| "any"
| "comparable"
| "uintptr"
)
}
fn emit_uses_type(source_id: Uuid, type_name: &str, ctx: &mut ParseContext<'_>) {
let (target_id, confidence) = if let Some(&id) = ctx.name_to_id.get(type_name) {
(id, 1.0_f32)
} else if ctx.imported_names.contains(type_name) {
(Uuid::new_v5(&Uuid::NAMESPACE_OID, type_name.as_bytes()), 0.8)
} else {
(Uuid::new_v5(&Uuid::NAMESPACE_OID, type_name.as_bytes()), 0.5)
};
ctx.result.relationships.push(Relationship {
source_id,
target_id,
rel_type: RelationType::UsesType,
confidence,
});
}
fn build_func_signature(
node: &Node<'_>,
name: &str,
source: &[u8],
receiver_type: Option<&str>,
) -> String {
let receiver_text = node
.child_by_field_name("receiver")
.map(|n| format!("{} ", node_text(&n, source)));
let params = node
.child_by_field_name("parameters")
.map(|n| node_text(&n, source))
.unwrap_or_else(|| "()".to_string());
let result = node
.child_by_field_name("result")
.map(|n| format!(" {}", node_text(&n, source)))
.unwrap_or_default();
let _ = receiver_type;
format!(
"func {}{}{}{}",
receiver_text.as_deref().unwrap_or(""),
name,
params,
result
)
}
fn extract_receiver_type(recv_node: &Node<'_>, source: &[u8]) -> Option<String> {
let mut cursor = recv_node.walk();
for child in recv_node.named_children(&mut cursor) {
if child.kind() == "parameter_declaration" {
if let Some(type_node) = child.child_by_field_name("type") {
let raw = node_text(&type_node, source);
let clean = strip_pointer(raw);
let base = clean.split('.').last().unwrap_or(&clean).to_string();
if !base.is_empty() {
return Some(base);
}
}
}
}
None
}
fn strip_pointer(s: String) -> String {
s.trim_start_matches('*').trim().to_string()
}
fn unquote(s: String) -> String {
s.trim_matches('"').trim_matches('`').to_string()
}
fn node_text(node: &Node<'_>, source: &[u8]) -> String {
node.utf8_text(source)
.unwrap_or("")
.trim()
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
fn parse(source: &str) -> FileParseResult {
parse_go_file("test.go", source, "proj", Utc::now())
}
fn field_symbols(result: &FileParseResult) -> Vec<&Symbol> {
result
.symbols
.iter()
.filter(|s| s.symbol_type == SymbolType::Field)
.collect()
}
fn references_rels(result: &FileParseResult) -> Vec<&Relationship> {
result
.relationships
.iter()
.filter(|r| r.rel_type == RelationType::References)
.collect()
}
#[test]
fn test_struct_fields_captured() {
let source = r#"
package main
type Server struct {
Logger
addr string
port int
}
"#;
let result = parse(source);
let fields = field_symbols(&result);
let names: Vec<_> = fields.iter().map(|s| s.name.as_str()).collect();
assert_eq!(names, vec!["addr", "port"], "fields: {fields:?}");
let struct_id = result
.symbols
.iter()
.find(|s| s.symbol_type == SymbolType::Class && s.name == "Server")
.map(|s| s.id)
.unwrap();
for f in &fields {
assert_eq!(f.parent_symbol_id, Some(struct_id));
assert!(
result.relationships.iter().any(|r| {
r.rel_type == RelationType::Defines
&& r.source_id == struct_id
&& r.target_id == f.id
}),
"missing Defines(struct -> field {})",
f.name
);
}
}
#[test]
fn test_receiver_field_read_emits_references() {
let source = r#"
package main
type Server struct {
addr string
}
func (s *Server) Address() string {
return s.addr
}
"#;
let result = parse(source);
let addr = result
.symbols
.iter()
.find(|s| s.symbol_type == SymbolType::Field && s.name == "addr")
.map(|s| s.id)
.expect("addr field should exist");
let address = result
.symbols
.iter()
.find(|s| s.symbol_type == SymbolType::Method && s.name == "Address")
.map(|s| s.id)
.expect("Address method should exist");
let refs = references_rels(&result);
assert!(
refs.iter()
.any(|r| r.source_id == address && r.target_id == addr),
"expected References(Server.Address -> addr), refs: {refs:?}"
);
}
#[test]
fn test_receiver_method_call_not_a_field_reference() {
let source = r#"
package main
type Server struct {
addr string
}
func (s *Server) Start() string {
return s.addr
}
func (s *Server) Run() string {
return s.Start()
}
"#;
let result = parse(source);
let refs = references_rels(&result);
assert_eq!(
refs.len(),
1,
"expected exactly 1 References edge (s.addr), got: {refs:?}"
);
}
}