Shared memory and context tools for agentic work.
Code Rooms
// === crates/m1nd-ingest/src/extract/go.rs ===
use super::{
strip_comments_and_strings, CommentSyntax, ExtractedEdge, ExtractedNode, ExtractionResult,
Extractor,
};
use m1nd_core::error::M1ndResult;
use m1nd_core::types::NodeType;
use regex::Regex;
/// Go extractor using regex.
pub struct GoExtractor {
re_func: Regex,
re_method: Regex,
re_struct: Regex,
re_interface: Regex,
re_import: Regex,
/// Qualified calls: pkg.Function( or receiver.Method(
re_method_call: Regex,
/// Plain function calls: identifier( not preceded by func/type keywords
re_fn_call: Regex,
}
impl GoExtractor {
pub fn new() -> Self {
Self {
re_func: Regex::new(r"^func\s+(\w+)\s*\(").unwrap(),
re_method: Regex::new(r"^func\s+\([^)]+\)\s+(\w+)\s*\(").unwrap(),
re_struct: Regex::new(r"^type\s+(\w+)\s+struct\b").unwrap(),
re_interface: Regex::new(r"^type\s+(\w+)\s+interface\b").unwrap(),
re_import: Regex::new(r#"^\s*"([^"]+)""#).unwrap(),
// Qualified call: qualifier.Method( — captures (qualifier, method)
// Handles both package-qualified calls (fmt.Println) and method calls (s.Run)
re_method_call: Regex::new(r"\b(\w+)\.(\w+)\s*\(").unwrap(),
// Plain bare call: identifier( — lowercase-or-mixed start, not a keyword
// \b ensures we don't match mid-word; requires lowercase first char to avoid
// matching type conversions like MyType( or struct literals.
re_fn_call: Regex::new(r"\b([a-z_]\w*)\s*\(").unwrap(),
impl Default for GoExtractor {
fn default() -> Self {
Self::new()
impl Extractor for GoExtractor {
fn extract(&self, content: &[u8], file_id: &str) -> M1ndResult<ExtractionResult> {
let text = String::from_utf8_lossy(content);
let cleaned_lines = strip_comments_and_strings(&text, CommentSyntax::GO);
let mut nodes = Vec::new();
let mut edges = Vec::new();
let mut unresolved_refs = Vec::new();
let file_label = file_id.rsplit("::").next().unwrap_or(file_id);
nodes.push(ExtractedNode {
id: file_id.to_string(),
label: file_label.to_string(),
node_type: NodeType::File,
tags: vec!["go".into()],
line: 1,
end_line: text.lines().count() as u32,
});
let mut in_import_block = false;
for (line_num, line) in cleaned_lines.iter().enumerate() {
let ln = (line_num + 1) as u32;
let trimmed = line.trim();
// ------------------------------------------------------------------
// Import block tracking
if trimmed.starts_with("import (") {
in_import_block = true;
continue;
if in_import_block {
if trimmed == ")" {
in_import_block = false;
if let Some(caps) = self.re_import.captures(trimmed) {
let path = caps.get(1).unwrap().as_str();
let ref_id = format!("ref::{}", path);
edges.push(ExtractedEdge {
source: file_id.to_string(),
target: ref_id.clone(),
relation: "imports".into(),
weight: 0.5,
unresolved_refs.push(ref_id);
// Definition extraction — struct / interface / method / func.
// We track `is_definition` so call-site detection is skipped on
// definition-only lines (conservative: avoids false-positive calls
// from the parameter list of a func declaration).
let is_definition = if let Some(caps) = self.re_struct.captures(line) {
let name = caps.get(1).unwrap().as_str();
let node_id = format!("{}::struct::{}", file_id, name);
id: node_id.clone(),
label: name.to_string(),
node_type: NodeType::Struct,
line: ln,
end_line: ln,
target: node_id,
relation: "contains".into(),
weight: 1.0,
true
} else if let Some(caps) = self.re_interface.captures(line) {
let node_id = format!("{}::interface::{}", file_id, name);
node_type: NodeType::Type,
tags: vec!["go".into(), "interface".into()],
} else if let Some(caps) = self.re_method.captures(line) {
// Disambiguate same-named defs in one file (a method named `Run` on
// two types, or a method sharing a free func's name) so add_node
// does not drop the sibling. First keeps the clean id.
let node_id = super::unique_node_id(&nodes, &format!("{}::fn::{}", file_id, name));
node_type: NodeType::Function,
tags: vec!["go".into(), "method".into()],
// Conservatively skip call-site scan on the func/method signature
// line to avoid false positives from parameter type names.
} else if let Some(caps) = self.re_func.captures(line) {
} else {
false
// Single-line import: import "pkg/path"
if trimmed.starts_with("import ") && !trimmed.contains('(') {
if let Some(caps) = self
.re_import
.captures(trimmed.trim_start_matches("import "))
{
// Import lines have no call sites.
// Call-site detection
//
// Only runs on lines that are NOT:
// • inside an import block (handled above via `continue`)
// • single-line imports (handled above via `continue`)
// • definition-only lines (is_definition == true)
// • package declaration lines
// • empty or comment-only lines
// Two patterns:
// 1. Qualified: pkg.Func( or receiver.Method( → re_method_call
// 2. Bare: funcName( → re_fn_call
if !is_definition
&& !trimmed.starts_with("package ")
&& !trimmed.starts_with("//")
&& !trimmed.is_empty()
// Pattern 1: qualifier.Method(
// qualifier len-1 and "err" are excluded (noisy: testing.T
// receiver `t`, error value `err`).
// For uppercase qualifier → ref to type; otherwise → ref to method.
for caps in self.re_method_call.captures_iter(line) {
let qualifier = caps.get(1).unwrap().as_str();
let method = caps.get(2).unwrap().as_str();
if qualifier.len() == 1 || qualifier == "err" {
let ref_target = if qualifier.chars().next().is_some_and(|c| c.is_uppercase()) {
format!("ref::{}", qualifier)
format!("ref::{}", method)
if !unresolved_refs.contains(&ref_target) {
target: ref_target.clone(),
relation: "calls".into(),
weight: 0.4,
unresolved_refs.push(ref_target);
// Pattern 2: bare funcName(
// Only lowercase-starting identifiers (Go unexported functions).
// Uppercase-starting identifiers handled by pattern 1 when called
// as pkg.NewX(); bare uppercase is typically a type conversion.
// Explicit keyword/builtin exclusion list is the primary false-positive gate.
for caps in self.re_fn_call.captures_iter(line) {
let fn_name = caps.get(1).unwrap().as_str();
if matches!(
fn_name,
// Go control-flow keywords
"if"
| "for"
| "switch"
| "select"
| "return"
| "go"
| "defer"
| "range"
| "case"
| "default"
// Go built-in functions
| "make"
| "new"
| "len"
| "cap"
| "append"
| "copy"
| "delete"
| "close"
| "panic"
| "recover"
| "print"
| "println"
// Type / structural keywords
| "chan"
| "map"
| "func"
| "type"
| "struct"
| "interface"
| "var"
| "const"
// Common testing receiver names (too noisy)
| "t"
| "b"
| "m"
) {
let ref_target = format!("ref::{}", fn_name);
Ok(ExtractionResult {
nodes,
edges,
unresolved_refs,
})
fn extensions(&self) -> &[&str] {
&["go"]
// ===========================================================================
// Unit tests
#[cfg(test)]
mod tests {
use super::*;
use crate::extract::Extractor;
fn extract(src: &str) -> ExtractionResult {
let extractor = GoExtractor::new();
extractor
.extract(src.as_bytes(), "file::testfile.go")
.unwrap()
fn has_edge(result: &ExtractionResult, relation: &str, target: &str) -> bool {
result
.edges
.iter()
.any(|e| e.relation == relation && e.target == target)
fn has_any_relation(result: &ExtractionResult, relation: &str) -> bool {
result.edges.iter().any(|e| e.relation == relation)
// -----------------------------------------------------------------------
// Existing behavior: contains + imports
#[test]
fn go_extractor_emits_struct_node() {
let result = extract("package main\n\ntype Config struct {\n Host string\n}\n");
assert!(
result.nodes.iter().any(|n| n.label == "Config"),
"Should extract struct node"
);
assert!(has_edge(
&result,
"contains",
"file::testfile.go::struct::Config"
));
fn go_extractor_emits_func_node() {
let result = extract("package main\n\nfunc Run() {\n}\n");
result.nodes.iter().any(|n| n.label == "Run"),
"Should extract func node"
assert!(has_edge(&result, "contains", "file::testfile.go::fn::Run"));
fn go_extractor_emits_import() {
let result = extract("package main\n\nimport (\n \"fmt\"\n \"github.com/org/repo/pkg/util\"\n)\n\nfunc main() {}\n");
has_edge(&result, "imports", "ref::fmt"),
"Should emit import edge for fmt"
has_edge(&result, "imports", "ref::github.com/org/repo/pkg/util"),
"Should emit import edge for github.com/org/repo/pkg/util"
// TASK A: calls edges
fn go_extractor_emits_calls_for_bare_function_call() {
let result = extract("package main\n\nfunc run() {\n helper()\n}\n");
has_edge(&result, "calls", "ref::helper"),
"Should emit calls edge for helper(). Edges: {:?}",
result.edges
fn go_extractor_emits_calls_for_qualified_call() {
let result = extract("package main\n\nfunc run() {\n client.Send()\n}\n");
// qualifier `client` is lowercase → ref to method `Send`
has_edge(&result, "calls", "ref::Send"),
"Should emit calls edge for client.Send(). Edges: {:?}",
fn go_extractor_does_not_emit_calls_for_if_keyword() {
let result = extract("package main\n\nfunc run() {\n if true {\n }\n}\n");
!has_edge(&result, "calls", "ref::if"),
"Must NOT emit calls edge for `if` keyword. Edges: {:?}",
fn go_extractor_does_not_emit_calls_for_for_keyword() {
let result =
extract("package main\n\nfunc run() {\n for i := 0; i < 10; i++ {\n }\n}\n");
!has_edge(&result, "calls", "ref::for"),
"Must NOT emit calls edge for `for` keyword. Edges: {:?}",
fn go_extractor_does_not_emit_calls_for_switch() {
extract("package main\n\nfunc run(x int) {\n switch x {\n case 1:\n }\n}\n");
!has_edge(&result, "calls", "ref::switch"),
"Must NOT emit calls edge for `switch`. Edges: {:?}",
fn go_extractor_does_not_emit_calls_for_make_builtin() {
extract("package main\n\nfunc run() {\n s := make([]int, 10)\n _ = s\n}\n");
!has_edge(&result, "calls", "ref::make"),
"Must NOT emit calls edge for built-in `make`. Edges: {:?}",
fn go_extractor_does_not_emit_calls_on_definition_line() {
// The func definition line itself should not generate a calls edge
// for the function name — only `contains` should appear.
let result = extract("package main\n\nfunc process(data []byte) {\n}\n");
// `process` should be a `contains` target, not a `calls` target
!has_edge(&result, "calls", "ref::process"),
"Must NOT emit calls edge for the function being defined. Edges: {:?}",
has_edge(&result, "contains", "file::testfile.go::fn::process"),
"Should emit contains edge for process"
fn go_extractor_emits_calls_from_non_definition_lines() {
let src = "package main\n\nfunc run() {\n processData()\n pkg.Execute()\n}\n";
let result = extract(src);
// processData is a bare lowercase call
has_edge(&result, "calls", "ref::processData"),
"Should emit calls edge for processData(). Edges: {:?}",
// pkg.Execute — qualifier `pkg` lowercase → ref to `Execute`
has_edge(&result, "calls", "ref::Execute"),
"Should emit calls edge for pkg.Execute(). Edges: {:?}",
fn go_extractor_sample_edges_for_reporting() {
// This test exists to demonstrate what edges are produced on a
// realistic Go snippet. It passes trivially; the println output
// is visible with `cargo test -- --nocapture`.
let src = r#"package main
import (
"fmt"
"github.com/org/repo/pkg/store"
)
type Server struct{}
func NewServer(db store.DB) *Server {
return &Server{}
func (s *Server) HandleRequest() (string, error) {
data, err := s.store.Query(42)
if err != nil {
return "", err
result := processData(data)
fmt.Printf("result: %v\n", result)
return buildResponse(result), nil
func processData(d []byte) []byte {
return compress(d)
"#;
let ext = GoExtractor::new();
let result = ext.extract(src.as_bytes(), "file::main.go").unwrap();
for e in &result.edges {
if e.relation == "calls" || e.relation == "imports" || e.relation == "contains" {
let _ = format!("{} --[{}]--> {}", e.source, e.relation, e.target);
// Minimal assertion: we must have both calls and imports
assert!(result.edges.iter().any(|e| e.relation == "calls"));
assert!(result.edges.iter().any(|e| e.relation == "imports"));
assert!(result.edges.iter().any(|e| e.relation == "contains"));
fn go_same_name_methods_in_one_file_get_distinct_ids() {
// A method named `Run` defined on two different types in one file. Both must
// survive as DISTINCT function nodes — without id disambiguation the second
// collides on `…::fn::Run` and add_node drops it.
let src = "package main\n\nfunc (a *A) Run() {}\nfunc (b *B) Run() {}\n";
let run_ids: Vec<&str> = result
.nodes
.filter(|n| n.label == "Run" && n.node_type == NodeType::Function)
.map(|n| n.id.as_str())
.collect();
assert_eq!(
run_ids.len(),
2,
"expected two distinct `Run` method nodes, got ids {:?}",
run_ids
run_ids.contains(&"file::testfile.go::fn::Run"),
"first occurrence should keep the clean id: {:?}",
run_ids.contains(&"file::testfile.go::fn::Run#2"),
"second occurrence should be disambiguated to `#2`: {:?}",
fn go_extractor_deduplicates_calls_edges() {
// Same callee on multiple lines → only one calls edge (dedup via unresolved_refs)
let src = "package main\n\nfunc run() {\n helper()\n helper()\n helper()\n}\n";
let count = result
.filter(|e| e.relation == "calls" && e.target == "ref::helper")
.count();
count, 1,
"Duplicate calls to helper() should produce exactly 1 edge, got {}",
count