use std::path::{Path, PathBuf};
use tracing::warn;
use tree_sitter::Language;
use astmap_core::SymbolKind;
use super::{
collect_type_refs_by_kind, find_enclosing_symbol, node_text, parse_with_tree_sitter,
symbol_from_node, ExtractedCall, ExtractedImport, ExtractedSymbol, LanguageParser,
LanguageResolver, LanguageSupport, ParseResult,
};
pub(super) fn lang() -> LanguageSupport {
LanguageSupport {
name: "go",
extensions: &["go"],
parser: &GoParser,
resolver_factory: |root| Box::new(GoResolver::new(root)),
config_files: &["go.mod"],
sibling_fn: go_sibling_expansion,
}
}
fn go_sibling_expansion(rel_path: &str) -> Option<astmap_core::SiblingExpansion> {
if !rel_path.ends_with(".go") {
return None;
}
match rel_path.rsplit_once('/') {
Some((dir, _)) => Some(astmap_core::SiblingExpansion {
prefix: format!("{}/", dir),
extension: "go".to_string(),
root_only: false,
}),
None => Some(astmap_core::SiblingExpansion {
prefix: String::new(),
extension: "go".to_string(),
root_only: true,
}),
}
}
pub(crate) struct GoParser;
impl LanguageParser for GoParser {
fn parse(&self, source: &str, _file_path: &Path) -> ParseResult {
parse_go_file(source)
}
fn language_name(&self) -> &str {
"go"
}
}
fn go_language() -> Language {
tree_sitter_go::LANGUAGE.into()
}
fn parse_go_file(source: &str) -> ParseResult {
let mut result = parse_with_tree_sitter(
source,
&go_language(),
|root, src, symbols, imports| {
extract_from_node(root, src, symbols, imports);
},
extract_calls,
|root, src, symbols| {
collect_type_refs_by_kind(root, src, symbols, "type_identifier", &is_go_builtin)
},
);
link_methods_to_receivers(source, &mut result.symbols);
result.imports.push(ExtractedImport {
path: ".::*".to_string(),
imported_symbols: vec![],
});
result
}
fn extract_from_node(
node: tree_sitter::Node,
source: &str,
symbols: &mut Vec<ExtractedSymbol>,
imports: &mut Vec<ExtractedImport>,
) {
match node.kind() {
"function_declaration" => {
if let Some(sym) = extract_function(&node, source) {
symbols.push(sym);
return;
}
}
"method_declaration" => {
if let Some(sym) = extract_method(&node, source) {
symbols.push(sym);
return;
}
}
"type_declaration" => {
extract_type_declaration(&node, source, symbols);
return;
}
"const_declaration" => {
extract_const_or_var(&node, source, symbols, SymbolKind::Const);
return;
}
"var_declaration" => {
extract_const_or_var(&node, source, symbols, SymbolKind::Variable);
return;
}
"import_declaration" => {
imports.extend(extract_imports(&node, source));
return;
}
_ => {}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
extract_from_node(child, source, symbols, imports);
}
}
}
fn extract_function(node: &tree_sitter::Node, source: &str) -> Option<ExtractedSymbol> {
let name_node = node.child_by_field_name("name")?;
let name = node_text(&name_node, source);
Some(symbol_from_node(
name,
SymbolKind::Function,
node,
source,
None,
))
}
fn extract_method(node: &tree_sitter::Node, source: &str) -> Option<ExtractedSymbol> {
let name_node = node.child_by_field_name("name")?;
let name = node_text(&name_node, source);
Some(symbol_from_node(
name,
SymbolKind::Method,
node,
source,
None,
))
}
fn receiver_type_name(node: &tree_sitter::Node, source: &str) -> Option<String> {
let receiver = node.child_by_field_name("receiver")?;
for i in 0..receiver.child_count() {
if let Some(child) = receiver.child(i as u32) {
if let Some(name) = find_type_identifier(&child, source) {
return Some(name);
}
}
}
None
}
fn find_type_identifier(node: &tree_sitter::Node, source: &str) -> Option<String> {
if node.kind() == "type_identifier" {
return Some(node_text(node, source));
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if let Some(name) = find_type_identifier(&child, source) {
return Some(name);
}
}
}
None
}
fn extract_type_declaration(
node: &tree_sitter::Node,
source: &str,
symbols: &mut Vec<ExtractedSymbol>,
) {
collect_type_specs(node, source, symbols);
}
fn collect_type_specs(node: &tree_sitter::Node, source: &str, symbols: &mut Vec<ExtractedSymbol>) {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
match child.kind() {
"type_spec" => {
if let Some(sym) = extract_type_spec(&child, source) {
symbols.push(sym);
}
}
"type_alias" => {
if let Some(sym) = extract_type_alias(&child, source) {
symbols.push(sym);
}
}
_ => {
collect_type_specs(&child, source, symbols);
}
}
}
}
}
fn extract_type_spec(node: &tree_sitter::Node, source: &str) -> Option<ExtractedSymbol> {
let name = first_type_identifier(node, source)?;
let kind = determine_type_kind(node);
Some(symbol_from_node(name, kind, node, source, None))
}
fn extract_type_alias(node: &tree_sitter::Node, source: &str) -> Option<ExtractedSymbol> {
let name = first_type_identifier(node, source)?;
Some(symbol_from_node(
name,
SymbolKind::TypeAlias,
node,
source,
None,
))
}
fn first_type_identifier(node: &tree_sitter::Node, source: &str) -> Option<String> {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if child.kind() == "type_identifier" {
return Some(node_text(&child, source));
}
}
}
None
}
fn determine_type_kind(node: &tree_sitter::Node) -> SymbolKind {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
match child.kind() {
"struct_type" => return SymbolKind::Struct,
"interface_type" => return SymbolKind::Interface,
_ => {}
}
}
}
SymbolKind::TypeAlias
}
fn extract_const_or_var(
node: &tree_sitter::Node,
source: &str,
symbols: &mut Vec<ExtractedSymbol>,
kind: SymbolKind,
) {
collect_specs(node, source, symbols, kind);
}
fn collect_specs(
node: &tree_sitter::Node,
source: &str,
symbols: &mut Vec<ExtractedSymbol>,
kind: SymbolKind,
) {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
let child_kind = child.kind();
if child_kind == "const_spec" || child_kind == "var_spec" {
let name = child
.child_by_field_name("name")
.map(|n| node_text(&n, source))
.or_else(|| {
(0..child.child_count())
.filter_map(|j| child.child(j as u32))
.find(|c| c.kind() == "identifier")
.map(|c| node_text(&c, source))
});
if let Some(name) = name {
symbols.push(symbol_from_node(name, kind, &child, source, None));
}
} else {
collect_specs(&child, source, symbols, kind);
}
}
}
}
fn link_methods_to_receivers(source: &str, symbols: &mut [ExtractedSymbol]) {
let type_indices: Vec<(String, usize)> = symbols
.iter()
.enumerate()
.filter(|(_, sym)| {
matches!(
sym.kind,
SymbolKind::Struct | SymbolKind::Interface | SymbolKind::TypeAlias
)
})
.map(|(idx, sym)| (sym.name.clone(), idx))
.collect();
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&go_language())
.expect("failed to set Go language");
let tree = match parser.parse(source, None) {
Some(t) => t,
None => return,
};
let mut method_idx = 0;
collect_method_receivers(
tree.root_node(),
source,
symbols,
&type_indices,
&mut method_idx,
);
}
fn collect_method_receivers(
node: tree_sitter::Node,
source: &str,
symbols: &mut [ExtractedSymbol],
type_indices: &[(String, usize)],
method_idx: &mut usize,
) {
if node.kind() == "method_declaration" {
if let Some(recv_type) = receiver_type_name(&node, source) {
while *method_idx < symbols.len() {
if symbols[*method_idx].kind == SymbolKind::Method {
if let Some((_, type_idx)) =
type_indices.iter().find(|(name, _)| *name == recv_type)
{
symbols[*method_idx].parent_index = Some(*type_idx);
}
*method_idx += 1;
break;
}
*method_idx += 1;
}
}
return;
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
collect_method_receivers(child, source, symbols, type_indices, method_idx);
}
}
}
fn extract_imports(node: &tree_sitter::Node, source: &str) -> Vec<ExtractedImport> {
let mut imports = Vec::new();
collect_import_specs(node, source, &mut imports);
imports
}
fn collect_import_specs(
node: &tree_sitter::Node,
source: &str,
imports: &mut Vec<ExtractedImport>,
) {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if child.kind() == "import_spec" {
if let Some(imp) = extract_import_spec(&child, source) {
imports.push(imp);
}
} else {
collect_import_specs(&child, source, imports);
}
}
}
}
fn extract_import_spec(node: &tree_sitter::Node, source: &str) -> Option<ExtractedImport> {
let path_node = node.child_by_field_name("path").or_else(|| {
(0..node.child_count())
.filter_map(|i| node.child(i as u32))
.find(|c| c.kind() == "interpreted_string_literal")
})?;
let raw_path = node_text(&path_node, source);
let path = raw_path.trim_matches('"').to_string();
if path.is_empty() {
return None;
}
let name_node = node.child_by_field_name("name");
let alias = name_node.map(|n| node_text(&n, source));
match alias.as_deref() {
Some("_") => {
Some(ExtractedImport {
path,
imported_symbols: vec![],
})
}
Some(".") => {
Some(ExtractedImport {
path: format!("{}::*", path),
imported_symbols: vec![],
})
}
Some(alias_name) => {
Some(ExtractedImport {
path,
imported_symbols: vec![alias_name.to_string()],
})
}
None => {
let pkg_name = path.rsplit('/').next().unwrap_or(&path).to_string();
Some(ExtractedImport {
path,
imported_symbols: vec![pkg_name],
})
}
}
}
fn extract_calls(
root: tree_sitter::Node,
source: &str,
symbols: &[ExtractedSymbol],
) -> Vec<ExtractedCall> {
let mut calls = Vec::new();
collect_calls(root, source, symbols, &mut calls);
calls
}
fn collect_calls(
node: tree_sitter::Node,
source: &str,
symbols: &[ExtractedSymbol],
calls: &mut Vec<ExtractedCall>,
) {
if node.kind() == "call_expression" {
if let Some(func_node) = node.child_by_field_name("function") {
let raw_callee = node_text(&func_node, source);
let callee_name = raw_callee
.rsplit('.')
.next()
.unwrap_or(&raw_callee)
.to_string();
let call_line = node.start_position().row + 1;
if let Some(caller) = find_enclosing_symbol(symbols, call_line) {
calls.push(ExtractedCall {
caller_name: caller.to_string(),
callee_name,
line: call_line,
});
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
collect_calls(child, source, symbols, calls);
}
}
}
fn is_go_builtin(name: &str) -> bool {
matches!(
name,
"bool"
| "int"
| "int8"
| "int16"
| "int32"
| "int64"
| "uint"
| "uint8"
| "uint16"
| "uint32"
| "uint64"
| "uintptr"
| "float32"
| "float64"
| "complex64"
| "complex128"
| "string"
| "byte"
| "rune"
| "error"
| "any"
| "comparable"
)
}
pub(crate) struct GoResolver {
module_path: Option<String>,
}
impl GoResolver {
fn new(project_root: &Path) -> Self {
let module_path = read_go_module_path(project_root);
Self { module_path }
}
}
impl LanguageResolver for GoResolver {
fn resolve_import(
&self,
import_path: &str,
source_file: &str,
project_root: &Path,
) -> Option<PathBuf> {
let effective_path = import_path.strip_suffix("::*").unwrap_or(import_path);
if effective_path == "." {
let source_abs = project_root.join(source_file);
let source_dir = source_abs.parent()?;
return find_first_go_file(source_dir, source_file);
}
let first_segment = effective_path.split('/').next().unwrap_or(effective_path);
if !first_segment.contains('.') {
return None;
}
if let Some(ref module_path) = self.module_path {
if let Some(local_path) = effective_path.strip_prefix(module_path.as_str()) {
let local_path = local_path.strip_prefix('/').unwrap_or(local_path);
let pkg_dir = project_root.join(local_path);
return find_first_go_file_in_dir(&pkg_dir);
}
}
None
}
}
fn read_go_module_path(project_root: &Path) -> Option<String> {
let go_mod = project_root.join("go.mod");
let content = std::fs::read_to_string(&go_mod).ok()?;
for line in content.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("module") {
let module = rest.trim();
if !module.is_empty() {
return Some(module.to_string());
}
}
}
warn!("go.mod found but no module directive");
None
}
fn find_first_go_file(dir: &Path, exclude_source: &str) -> Option<PathBuf> {
let entries = std::fs::read_dir(dir).ok()?;
for entry in entries {
let entry = entry.ok()?;
let path = entry.path();
if path.extension().is_some_and(|e| e == "go") {
let file_name = path.file_name()?.to_str()?;
let source_name = Path::new(exclude_source)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if file_name != source_name {
return Some(path);
}
}
}
None
}
fn find_first_go_file_in_dir(dir: &Path) -> Option<PathBuf> {
let entries = std::fs::read_dir(dir).ok()?;
for entry in entries {
let entry = entry.ok()?;
let path = entry.path();
if path.extension().is_some_and(|e| e == "go") {
return Some(path);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use astmap_core::SymbolKind;
fn parse(source: &str) -> ParseResult {
GoParser.parse(source, &PathBuf::from("test.go"))
}
#[test]
fn test_parse_function() {
let result = parse("package main\nfunc hello() {}");
assert!(!result.has_errors);
let funcs: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Function)
.collect();
assert_eq!(funcs.len(), 1);
assert_eq!(funcs[0].name, "hello");
}
#[test]
fn test_parse_method() {
let source = r#"package main
type User struct { Name string }
func (u *User) Greet() string { return u.Name }
"#;
let result = parse(source);
assert!(!result.has_errors);
let methods: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.collect();
assert_eq!(methods.len(), 1);
assert_eq!(methods[0].name, "Greet");
assert!(
methods[0].parent_index.is_some(),
"method should be linked to receiver type"
);
}
#[test]
fn test_parse_method_pointer_vs_value_receiver() {
let source = r#"package main
type User struct {}
func (u *User) PtrMethod() {}
func (u User) ValMethod() {}
"#;
let result = parse(source);
let methods: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.collect();
assert_eq!(methods.len(), 2);
assert!(
methods[0].parent_index.is_some(),
"pointer receiver method should be linked"
);
assert!(
methods[1].parent_index.is_some(),
"value receiver method should be linked"
);
}
#[test]
fn test_parse_struct() {
let source = r#"package main
type User struct {
Name string
Age int
}
"#;
let result = parse(source);
assert!(!result.has_errors);
let structs: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Struct)
.collect();
assert_eq!(structs.len(), 1);
assert_eq!(structs[0].name, "User");
}
#[test]
fn test_parse_interface() {
let source = r#"package main
type Reader interface {
Read(p []byte) (n int, err error)
}
"#;
let result = parse(source);
assert!(!result.has_errors);
let ifaces: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Interface)
.collect();
assert_eq!(ifaces.len(), 1);
assert_eq!(ifaces[0].name, "Reader");
}
#[test]
fn test_parse_type_alias() {
let source = r#"package main
type MyInt = int
type Duration int64
"#;
let result = parse(source);
let aliases: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::TypeAlias)
.collect();
assert_eq!(aliases.len(), 2);
assert_eq!(aliases[0].name, "MyInt");
assert_eq!(aliases[1].name, "Duration");
}
#[test]
fn test_parse_const() {
let source = r#"package main
const MaxSize = 100
const (
A = 1
B = 2
)
"#;
let result = parse(source);
let consts: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Const)
.collect();
assert_eq!(consts.len(), 3);
assert_eq!(consts[0].name, "MaxSize");
assert_eq!(consts[1].name, "A");
assert_eq!(consts[2].name, "B");
}
#[test]
fn test_parse_var() {
let source = r#"package main
var GlobalFlag = true
var (
X = 1
Y = "hello"
)
"#;
let result = parse(source);
let vars: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Variable)
.collect();
assert_eq!(vars.len(), 3);
assert_eq!(vars[0].name, "GlobalFlag");
assert_eq!(vars[1].name, "X");
assert_eq!(vars[2].name, "Y");
}
#[test]
fn test_parse_exported_vs_unexported() {
let source = r#"package main
func ExportedFunc() {}
func unexportedFunc() {}
type ExportedType struct {}
type unexportedType struct {}
"#;
let result = parse(source);
let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"ExportedFunc"));
assert!(names.contains(&"unexportedFunc"));
assert!(names.contains(&"ExportedType"));
assert!(names.contains(&"unexportedType"));
}
#[test]
fn test_parse_nested_struct_method() {
let source = r#"package main
type Server struct { Port int }
func (s *Server) Start() {}
func (s *Server) Stop() {}
"#;
let result = parse(source);
let server_idx = result
.symbols
.iter()
.position(|s| s.name == "Server")
.unwrap();
let methods: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.collect();
assert_eq!(methods.len(), 2);
assert_eq!(methods[0].parent_index, Some(server_idx));
assert_eq!(methods[1].parent_index, Some(server_idx));
}
#[test]
fn test_import_simple() {
let result = parse(
r#"package main
import "fmt"
"#,
);
let real_imports: Vec<_> = result.imports.iter().filter(|i| i.path != ".::*").collect();
assert_eq!(real_imports.len(), 1);
assert_eq!(real_imports[0].path, "fmt");
assert_eq!(real_imports[0].imported_symbols, vec!["fmt"]);
}
#[test]
fn test_import_grouped() {
let result = parse(
r#"package main
import (
"fmt"
"net/http"
)
"#,
);
let real_imports: Vec<_> = result.imports.iter().filter(|i| i.path != ".::*").collect();
assert_eq!(real_imports.len(), 2);
assert_eq!(real_imports[0].path, "fmt");
assert_eq!(real_imports[1].path, "net/http");
assert_eq!(real_imports[1].imported_symbols, vec!["http"]);
}
#[test]
fn test_import_aliased() {
let result = parse(
r#"package main
import f "fmt"
"#,
);
let real_imports: Vec<_> = result.imports.iter().filter(|i| i.path != ".::*").collect();
assert_eq!(real_imports.len(), 1);
assert_eq!(real_imports[0].path, "fmt");
assert_eq!(real_imports[0].imported_symbols, vec!["f"]);
}
#[test]
fn test_import_dot() {
let result = parse(
r#"package main
import . "fmt"
"#,
);
let dot_imports: Vec<_> = result
.imports
.iter()
.filter(|i| i.path == "fmt::*")
.collect();
assert_eq!(dot_imports.len(), 1);
assert!(dot_imports[0].imported_symbols.is_empty());
let plain_imports: Vec<_> = result
.imports
.iter()
.filter(|i| !i.path.ends_with("::*"))
.collect();
assert_eq!(plain_imports.len(), 0);
}
#[test]
fn test_import_blank() {
let result = parse(
r#"package main
import _ "database/sql"
"#,
);
let real_imports: Vec<_> = result.imports.iter().filter(|i| i.path != ".::*").collect();
assert_eq!(real_imports.len(), 1);
assert_eq!(real_imports[0].path, "database/sql");
assert!(real_imports[0].imported_symbols.is_empty());
}
#[test]
fn test_synthetic_package_import() {
let result = parse("package main\nfunc main() {}");
let synthetic: Vec<_> = result.imports.iter().filter(|i| i.path == ".::*").collect();
assert_eq!(
synthetic.len(),
1,
"synthetic .::* import should be present"
);
assert!(synthetic[0].imported_symbols.is_empty());
}
#[test]
fn test_extract_calls_simple() {
let source = r#"package main
func helper() {}
func main() {
helper()
}
"#;
let result = parse(source);
let callee_names: Vec<&str> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(callee_names.contains(&"helper"));
}
#[test]
fn test_extract_calls_method() {
let source = r#"package main
type S struct {}
func (s *S) Do() {}
func main() {
s := S{}
s.Do()
}
"#;
let result = parse(source);
let callee_names: Vec<&str> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(callee_names.contains(&"Do"));
}
#[test]
fn test_extract_calls_package_qualified() {
let source = r#"package main
import "fmt"
func main() {
fmt.Println("hello")
}
"#;
let result = parse(source);
let callee_names: Vec<&str> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(
callee_names.contains(&"Println"),
"should strip package qualifier"
);
}
#[test]
fn test_extract_type_refs_param() {
let source = r#"package main
type Config struct {}
func process(c Config) {}
"#;
let result = parse(source);
let type_names: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(type_names.contains(&"Config"));
}
#[test]
fn test_extract_type_refs_return() {
let source = r#"package main
type Result struct {}
func create() Result { return Result{} }
"#;
let result = parse(source);
let type_names: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(type_names.contains(&"Result"));
}
#[test]
fn test_extract_type_refs_field() {
let source = r#"package main
type Address struct { City string }
type User struct {
Addr Address
}
"#;
let result = parse(source);
let type_names: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(type_names.contains(&"Address"));
}
#[test]
fn test_extract_type_refs_builtin_filtered() {
let source = r#"package main
func foo(s string, n int, b bool) float64 { return 0.0 }
"#;
let result = parse(source);
let type_names: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(
!type_names.contains(&"string"),
"builtins should be filtered"
);
assert!(!type_names.contains(&"int"));
assert!(!type_names.contains(&"bool"));
assert!(!type_names.contains(&"float64"));
}
#[test]
fn test_resolve_stdlib_skip() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("go.mod"),
"module example.com/myproject\n\ngo 1.21\n",
)
.unwrap();
let resolver = GoResolver::new(tmp.path());
assert_eq!(resolver.resolve_import("fmt", "main.go", tmp.path()), None);
assert_eq!(
resolver.resolve_import("net/http", "main.go", tmp.path()),
None
);
}
#[test]
fn test_resolve_external_skip() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("go.mod"),
"module example.com/myproject\n\ngo 1.21\n",
)
.unwrap();
let resolver = GoResolver::new(tmp.path());
assert_eq!(
resolver.resolve_import("github.com/other/pkg", "main.go", tmp.path()),
None
);
}
#[test]
fn test_resolve_local_package() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("go.mod"),
"module example.com/myproject\n\ngo 1.21\n",
)
.unwrap();
let pkg_dir = tmp.path().join("pkg");
std::fs::create_dir_all(&pkg_dir).unwrap();
std::fs::write(pkg_dir.join("service.go"), "package pkg\n").unwrap();
let resolver = GoResolver::new(tmp.path());
let resolved = resolver.resolve_import("example.com/myproject/pkg", "main.go", tmp.path());
assert!(resolved.is_some(), "local package should resolve");
assert!(resolved.unwrap().ends_with("service.go"));
}
#[test]
fn test_resolve_no_gomod() {
let tmp = tempfile::tempdir().unwrap();
let resolver = GoResolver::new(tmp.path());
assert_eq!(
resolver.resolve_import("example.com/myproject/pkg", "main.go", tmp.path()),
None
);
assert_eq!(resolver.resolve_import("fmt", "main.go", tmp.path()), None);
}
#[test]
fn test_resolve_dot_import() {
let tmp = tempfile::tempdir().unwrap();
let pkg_dir = tmp.path().join("pkg");
std::fs::create_dir_all(&pkg_dir).unwrap();
std::fs::write(pkg_dir.join("types.go"), "package pkg\n").unwrap();
std::fs::write(pkg_dir.join("service.go"), "package pkg\n").unwrap();
let resolver = GoResolver::new(tmp.path());
let resolved = resolver.resolve_import(".::*", "pkg/service.go", tmp.path());
assert!(
resolved.is_some(),
"synthetic .::* should resolve to a sibling"
);
let resolved_path = resolved.unwrap();
assert!(resolved_path.ends_with("types.go"));
}
#[test]
fn test_resolve_gomod_with_comments() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("go.mod"),
"// This is a comment\n\nmodule example.com/myproject\n\ngo 1.21\n",
)
.unwrap();
let resolver = GoResolver::new(tmp.path());
assert!(
resolver.module_path.is_some(),
"should parse module path despite comments"
);
assert_eq!(
resolver.module_path.as_deref(),
Some("example.com/myproject")
);
}
#[test]
fn test_empty_file() {
let result = parse("");
assert!(
result.symbols.is_empty()
|| result
.symbols
.iter()
.all(|s| s.kind != SymbolKind::Function)
);
}
#[test]
fn test_package_only() {
let result = parse("package main\n");
assert!(!result.has_errors);
let real_symbols: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind != SymbolKind::Module)
.collect();
assert!(real_symbols.is_empty());
}
#[test]
fn test_malformed_file() {
let result = parse("package main\nfunc {{{ invalid syntax");
assert!(result.has_errors);
}
}