Skip to main content

astmap_languages/language/
go.rs

1// Go language support: parser + resolver
2
3use std::path::{Path, PathBuf};
4
5use tracing::warn;
6use tree_sitter::Language;
7
8use astmap_core::SymbolKind;
9
10use super::{
11    collect_type_refs_by_kind, find_enclosing_symbol, node_text, parse_with_tree_sitter,
12    symbol_from_node, ExtractedCall, ExtractedImport, ExtractedSymbol, LanguageParser,
13    LanguageResolver, LanguageSupport, ParseResult,
14};
15
16pub(super) fn lang() -> LanguageSupport {
17    LanguageSupport {
18        name: "go",
19        extensions: &["go"],
20        parser: &GoParser,
21        resolver_factory: |root| Box::new(GoResolver::new(root)),
22        config_files: &["go.mod"],
23        sibling_fn: go_sibling_expansion,
24    }
25}
26
27fn go_sibling_expansion(rel_path: &str) -> Option<astmap_core::SiblingExpansion> {
28    if !rel_path.ends_with(".go") {
29        return None;
30    }
31    match rel_path.rsplit_once('/') {
32        Some((dir, _)) => Some(astmap_core::SiblingExpansion {
33            prefix: format!("{}/", dir),
34            extension: "go".to_string(),
35            root_only: false,
36        }),
37        None => Some(astmap_core::SiblingExpansion {
38            prefix: String::new(),
39            extension: "go".to_string(),
40            root_only: true,
41        }),
42    }
43}
44
45// ── Parser ──
46
47pub(crate) struct GoParser;
48
49impl LanguageParser for GoParser {
50    fn parse(&self, source: &str, _file_path: &Path) -> ParseResult {
51        parse_go_file(source)
52    }
53
54    fn language_name(&self) -> &str {
55        "go"
56    }
57}
58
59fn go_language() -> Language {
60    tree_sitter_go::LANGUAGE.into()
61}
62
63fn parse_go_file(source: &str) -> ParseResult {
64    let mut result = parse_with_tree_sitter(
65        source,
66        &go_language(),
67        |root, src, symbols, imports| {
68            extract_from_node(root, src, symbols, imports);
69        },
70        extract_calls,
71        |root, src, symbols| {
72            collect_type_refs_by_kind(root, src, symbols, "type_identifier", &is_go_builtin)
73        },
74    );
75
76    // 2-pass: link methods to their receiver types
77    link_methods_to_receivers(source, &mut result.symbols);
78
79    // Emit synthetic package import for same-package cross-file resolution.
80    // The "::*" suffix triggers wildcard detection in imports.rs:103.
81    result.imports.push(ExtractedImport {
82        path: ".::*".to_string(),
83        imported_symbols: vec![],
84    });
85
86    result
87}
88
89// ---------------------------------------------------------------------------
90// Symbol extraction
91// ---------------------------------------------------------------------------
92
93fn extract_from_node(
94    node: tree_sitter::Node,
95    source: &str,
96    symbols: &mut Vec<ExtractedSymbol>,
97    imports: &mut Vec<ExtractedImport>,
98) {
99    match node.kind() {
100        "function_declaration" => {
101            if let Some(sym) = extract_function(&node, source) {
102                symbols.push(sym);
103                return;
104            }
105        }
106        "method_declaration" => {
107            // Methods are linked to receiver types in pass 2
108            if let Some(sym) = extract_method(&node, source) {
109                symbols.push(sym);
110                return;
111            }
112        }
113        "type_declaration" => {
114            extract_type_declaration(&node, source, symbols);
115            return;
116        }
117        "const_declaration" => {
118            extract_const_or_var(&node, source, symbols, SymbolKind::Const);
119            return;
120        }
121        "var_declaration" => {
122            extract_const_or_var(&node, source, symbols, SymbolKind::Variable);
123            return;
124        }
125        "import_declaration" => {
126            imports.extend(extract_imports(&node, source));
127            return;
128        }
129        _ => {}
130    }
131
132    // Default: recurse into children
133    for i in 0..node.child_count() {
134        if let Some(child) = node.child(i as u32) {
135            extract_from_node(child, source, symbols, imports);
136        }
137    }
138}
139
140fn extract_function(node: &tree_sitter::Node, source: &str) -> Option<ExtractedSymbol> {
141    let name_node = node.child_by_field_name("name")?;
142    let name = node_text(&name_node, source);
143    Some(symbol_from_node(
144        name,
145        SymbolKind::Function,
146        node,
147        source,
148        None,
149    ))
150}
151
152fn extract_method(node: &tree_sitter::Node, source: &str) -> Option<ExtractedSymbol> {
153    let name_node = node.child_by_field_name("name")?;
154    let name = node_text(&name_node, source);
155    // parent_index will be set in pass 2 (link_methods_to_receivers)
156    Some(symbol_from_node(
157        name,
158        SymbolKind::Method,
159        node,
160        source,
161        None,
162    ))
163}
164
165/// Extract the receiver type name from a method_declaration node.
166/// Handles both value receivers `(r Type)` and pointer receivers `(r *Type)`.
167fn receiver_type_name(node: &tree_sitter::Node, source: &str) -> Option<String> {
168    let receiver = node.child_by_field_name("receiver")?;
169    // receiver is a parameter_list: (r *Type) or (r Type)
170    // Walk children to find a type_identifier or pointer_type→type_identifier
171    for i in 0..receiver.child_count() {
172        if let Some(child) = receiver.child(i as u32) {
173            if let Some(name) = find_type_identifier(&child, source) {
174                return Some(name);
175            }
176        }
177    }
178    None
179}
180
181fn find_type_identifier(node: &tree_sitter::Node, source: &str) -> Option<String> {
182    if node.kind() == "type_identifier" {
183        return Some(node_text(node, source));
184    }
185    for i in 0..node.child_count() {
186        if let Some(child) = node.child(i as u32) {
187            if let Some(name) = find_type_identifier(&child, source) {
188                return Some(name);
189            }
190        }
191    }
192    None
193}
194
195fn extract_type_declaration(
196    node: &tree_sitter::Node,
197    source: &str,
198    symbols: &mut Vec<ExtractedSymbol>,
199) {
200    // type_declaration can contain:
201    // - direct type_spec / type_alias children (single type decl)
202    // - type_spec_list wrapping multiple type_spec / type_alias (grouped)
203    collect_type_specs(node, source, symbols);
204}
205
206fn collect_type_specs(node: &tree_sitter::Node, source: &str, symbols: &mut Vec<ExtractedSymbol>) {
207    for i in 0..node.child_count() {
208        if let Some(child) = node.child(i as u32) {
209            match child.kind() {
210                "type_spec" => {
211                    if let Some(sym) = extract_type_spec(&child, source) {
212                        symbols.push(sym);
213                    }
214                }
215                "type_alias" => {
216                    if let Some(sym) = extract_type_alias(&child, source) {
217                        symbols.push(sym);
218                    }
219                }
220                _ => {
221                    // Recurse into wrappers (e.g., type_spec_list)
222                    collect_type_specs(&child, source, symbols);
223                }
224            }
225        }
226    }
227}
228
229fn extract_type_spec(node: &tree_sitter::Node, source: &str) -> Option<ExtractedSymbol> {
230    // type_spec: type_identifier + type (struct_type, interface_type, etc.)
231    let name = first_type_identifier(node, source)?;
232    let kind = determine_type_kind(node);
233    Some(symbol_from_node(name, kind, node, source, None))
234}
235
236fn extract_type_alias(node: &tree_sitter::Node, source: &str) -> Option<ExtractedSymbol> {
237    // type_alias: type_identifier = type_identifier
238    let name = first_type_identifier(node, source)?;
239    Some(symbol_from_node(
240        name,
241        SymbolKind::TypeAlias,
242        node,
243        source,
244        None,
245    ))
246}
247
248/// Find the first type_identifier child (used as the type name).
249fn first_type_identifier(node: &tree_sitter::Node, source: &str) -> Option<String> {
250    for i in 0..node.child_count() {
251        if let Some(child) = node.child(i as u32) {
252            if child.kind() == "type_identifier" {
253                return Some(node_text(&child, source));
254            }
255        }
256    }
257    None
258}
259
260/// Determine the SymbolKind from the type body in a type_spec.
261fn determine_type_kind(node: &tree_sitter::Node) -> SymbolKind {
262    for i in 0..node.child_count() {
263        if let Some(child) = node.child(i as u32) {
264            match child.kind() {
265                "struct_type" => return SymbolKind::Struct,
266                "interface_type" => return SymbolKind::Interface,
267                _ => {}
268            }
269        }
270    }
271    SymbolKind::TypeAlias
272}
273
274fn extract_const_or_var(
275    node: &tree_sitter::Node,
276    source: &str,
277    symbols: &mut Vec<ExtractedSymbol>,
278    kind: SymbolKind,
279) {
280    // const/var declarations can have:
281    // - direct const_spec/var_spec children (single decl)
282    // - const_spec_list/var_spec_list wrapping multiple specs (grouped)
283    collect_specs(node, source, symbols, kind);
284}
285
286fn collect_specs(
287    node: &tree_sitter::Node,
288    source: &str,
289    symbols: &mut Vec<ExtractedSymbol>,
290    kind: SymbolKind,
291) {
292    for i in 0..node.child_count() {
293        if let Some(child) = node.child(i as u32) {
294            let child_kind = child.kind();
295            if child_kind == "const_spec" || child_kind == "var_spec" {
296                // var_spec uses "name" field for single, or first identifier child
297                let name = child
298                    .child_by_field_name("name")
299                    .map(|n| node_text(&n, source))
300                    .or_else(|| {
301                        // Fallback: find first identifier child
302                        (0..child.child_count())
303                            .filter_map(|j| child.child(j as u32))
304                            .find(|c| c.kind() == "identifier")
305                            .map(|c| node_text(&c, source))
306                    });
307                if let Some(name) = name {
308                    symbols.push(symbol_from_node(name, kind, &child, source, None));
309                }
310            } else {
311                // Recurse into list wrappers (const_spec_list, var_spec_list)
312                collect_specs(&child, source, symbols, kind);
313            }
314        }
315    }
316}
317
318// ---------------------------------------------------------------------------
319// 2-pass: link methods to receiver types
320// ---------------------------------------------------------------------------
321
322fn link_methods_to_receivers(source: &str, symbols: &mut [ExtractedSymbol]) {
323    // Build a map of type name → index for all struct/interface/type_alias symbols
324    let type_indices: Vec<(String, usize)> = symbols
325        .iter()
326        .enumerate()
327        .filter(|(_, sym)| {
328            matches!(
329                sym.kind,
330                SymbolKind::Struct | SymbolKind::Interface | SymbolKind::TypeAlias
331            )
332        })
333        .map(|(idx, sym)| (sym.name.clone(), idx))
334        .collect();
335
336    // Re-parse to get method_declaration nodes with receiver info.
337    // We need the AST again because ExtractedSymbol doesn't store receiver type.
338    let mut parser = tree_sitter::Parser::new();
339    parser
340        .set_language(&go_language())
341        .expect("failed to set Go language");
342    let tree = match parser.parse(source, None) {
343        Some(t) => t,
344        None => return,
345    };
346
347    let mut method_idx = 0;
348    collect_method_receivers(
349        tree.root_node(),
350        source,
351        symbols,
352        &type_indices,
353        &mut method_idx,
354    );
355}
356
357fn collect_method_receivers(
358    node: tree_sitter::Node,
359    source: &str,
360    symbols: &mut [ExtractedSymbol],
361    type_indices: &[(String, usize)],
362    method_idx: &mut usize,
363) {
364    if node.kind() == "method_declaration" {
365        if let Some(recv_type) = receiver_type_name(&node, source) {
366            // Find the corresponding method in symbols (by order of appearance)
367            while *method_idx < symbols.len() {
368                if symbols[*method_idx].kind == SymbolKind::Method {
369                    // Match by checking the receiver type against known types
370                    if let Some((_, type_idx)) =
371                        type_indices.iter().find(|(name, _)| *name == recv_type)
372                    {
373                        symbols[*method_idx].parent_index = Some(*type_idx);
374                    }
375                    *method_idx += 1;
376                    break;
377                }
378                *method_idx += 1;
379            }
380        }
381        return;
382    }
383
384    for i in 0..node.child_count() {
385        if let Some(child) = node.child(i as u32) {
386            collect_method_receivers(child, source, symbols, type_indices, method_idx);
387        }
388    }
389}
390
391// ---------------------------------------------------------------------------
392// Import extraction
393// ---------------------------------------------------------------------------
394
395fn extract_imports(node: &tree_sitter::Node, source: &str) -> Vec<ExtractedImport> {
396    let mut imports = Vec::new();
397    collect_import_specs(node, source, &mut imports);
398    imports
399}
400
401fn collect_import_specs(
402    node: &tree_sitter::Node,
403    source: &str,
404    imports: &mut Vec<ExtractedImport>,
405) {
406    for i in 0..node.child_count() {
407        if let Some(child) = node.child(i as u32) {
408            if child.kind() == "import_spec" {
409                if let Some(imp) = extract_import_spec(&child, source) {
410                    imports.push(imp);
411                }
412            } else {
413                // Recurse into import_spec_list and other wrappers
414                collect_import_specs(&child, source, imports);
415            }
416        }
417    }
418}
419
420fn extract_import_spec(node: &tree_sitter::Node, source: &str) -> Option<ExtractedImport> {
421    // Find the path: child_by_field_name("path") or first interpreted_string_literal
422    let path_node = node.child_by_field_name("path").or_else(|| {
423        (0..node.child_count())
424            .filter_map(|i| node.child(i as u32))
425            .find(|c| c.kind() == "interpreted_string_literal")
426    })?;
427    // Extract the string content (strip outer quotes)
428    let raw_path = node_text(&path_node, source);
429    let path = raw_path.trim_matches('"').to_string();
430
431    if path.is_empty() {
432        return None;
433    }
434
435    // Check for alias/name
436    let name_node = node.child_by_field_name("name");
437    let alias = name_node.map(|n| node_text(&n, source));
438
439    match alias.as_deref() {
440        Some("_") => {
441            // Blank import: import _ "side/effect" — skip, no symbols
442            Some(ExtractedImport {
443                path,
444                imported_symbols: vec![],
445            })
446        }
447        Some(".") => {
448            // Dot import: import . "fmt" — wildcard-like
449            Some(ExtractedImport {
450                path: format!("{}::*", path),
451                imported_symbols: vec![],
452            })
453        }
454        Some(alias_name) => {
455            // Aliased import: import f "fmt" — use alias as symbol name
456            Some(ExtractedImport {
457                path,
458                imported_symbols: vec![alias_name.to_string()],
459            })
460        }
461        None => {
462            // Normal import: import "fmt" — use last path segment as package name
463            let pkg_name = path.rsplit('/').next().unwrap_or(&path).to_string();
464            Some(ExtractedImport {
465                path,
466                imported_symbols: vec![pkg_name],
467            })
468        }
469    }
470}
471
472// ---------------------------------------------------------------------------
473// Call extraction
474// ---------------------------------------------------------------------------
475
476fn extract_calls(
477    root: tree_sitter::Node,
478    source: &str,
479    symbols: &[ExtractedSymbol],
480) -> Vec<ExtractedCall> {
481    let mut calls = Vec::new();
482    collect_calls(root, source, symbols, &mut calls);
483    calls
484}
485
486fn collect_calls(
487    node: tree_sitter::Node,
488    source: &str,
489    symbols: &[ExtractedSymbol],
490    calls: &mut Vec<ExtractedCall>,
491) {
492    if node.kind() == "call_expression" {
493        if let Some(func_node) = node.child_by_field_name("function") {
494            let raw_callee = node_text(&func_node, source);
495            // Strip package qualifier: fmt.Println → Println, obj.Method → Method
496            let callee_name = raw_callee
497                .rsplit('.')
498                .next()
499                .unwrap_or(&raw_callee)
500                .to_string();
501
502            let call_line = node.start_position().row + 1;
503            if let Some(caller) = find_enclosing_symbol(symbols, call_line) {
504                calls.push(ExtractedCall {
505                    caller_name: caller.to_string(),
506                    callee_name,
507                    line: call_line,
508                });
509            }
510        }
511    }
512
513    for i in 0..node.child_count() {
514        if let Some(child) = node.child(i as u32) {
515            collect_calls(child, source, symbols, calls);
516        }
517    }
518}
519
520// ---------------------------------------------------------------------------
521// Type reference extraction
522// ---------------------------------------------------------------------------
523
524fn is_go_builtin(name: &str) -> bool {
525    matches!(
526        name,
527        "bool"
528            | "int"
529            | "int8"
530            | "int16"
531            | "int32"
532            | "int64"
533            | "uint"
534            | "uint8"
535            | "uint16"
536            | "uint32"
537            | "uint64"
538            | "uintptr"
539            | "float32"
540            | "float64"
541            | "complex64"
542            | "complex128"
543            | "string"
544            | "byte"
545            | "rune"
546            | "error"
547            | "any"
548            | "comparable"
549    )
550}
551
552// ── Resolver ──
553
554pub(crate) struct GoResolver {
555    module_path: Option<String>,
556}
557
558impl GoResolver {
559    fn new(project_root: &Path) -> Self {
560        let module_path = read_go_module_path(project_root);
561        Self { module_path }
562    }
563}
564
565impl LanguageResolver for GoResolver {
566    fn resolve_import(
567        &self,
568        import_path: &str,
569        source_file: &str,
570        project_root: &Path,
571    ) -> Option<PathBuf> {
572        let effective_path = import_path.strip_suffix("::*").unwrap_or(import_path);
573
574        // Synthetic package import: resolve to same directory
575        if effective_path == "." {
576            let source_abs = project_root.join(source_file);
577            let source_dir = source_abs.parent()?;
578            return find_first_go_file(source_dir, source_file);
579        }
580
581        // Standard library: first path segment has no dot
582        let first_segment = effective_path.split('/').next().unwrap_or(effective_path);
583        if !first_segment.contains('.') {
584            return None;
585        }
586
587        // Local package: starts with module path
588        if let Some(ref module_path) = self.module_path {
589            if let Some(local_path) = effective_path.strip_prefix(module_path.as_str()) {
590                let local_path = local_path.strip_prefix('/').unwrap_or(local_path);
591                let pkg_dir = project_root.join(local_path);
592                return find_first_go_file_in_dir(&pkg_dir);
593            }
594        }
595
596        // External dependency: skip
597        None
598    }
599}
600
601/// Read the module path from go.mod.
602fn read_go_module_path(project_root: &Path) -> Option<String> {
603    let go_mod = project_root.join("go.mod");
604    let content = std::fs::read_to_string(&go_mod).ok()?;
605    for line in content.lines() {
606        let trimmed = line.trim();
607        if let Some(rest) = trimmed.strip_prefix("module") {
608            let module = rest.trim();
609            if !module.is_empty() {
610                return Some(module.to_string());
611            }
612        }
613    }
614    warn!("go.mod found but no module directive");
615    None
616}
617
618/// Find the first .go file in a directory (excluding the source file itself).
619fn find_first_go_file(dir: &Path, exclude_source: &str) -> Option<PathBuf> {
620    let entries = std::fs::read_dir(dir).ok()?;
621    for entry in entries {
622        let entry = entry.ok()?;
623        let path = entry.path();
624        if path.extension().is_some_and(|e| e == "go") {
625            // Don't return self
626            let file_name = path.file_name()?.to_str()?;
627            let source_name = Path::new(exclude_source)
628                .file_name()
629                .and_then(|n| n.to_str())
630                .unwrap_or("");
631            if file_name != source_name {
632                return Some(path);
633            }
634        }
635    }
636    None
637}
638
639/// Find the first .go file in a directory.
640fn find_first_go_file_in_dir(dir: &Path) -> Option<PathBuf> {
641    let entries = std::fs::read_dir(dir).ok()?;
642    for entry in entries {
643        let entry = entry.ok()?;
644        let path = entry.path();
645        if path.extension().is_some_and(|e| e == "go") {
646            return Some(path);
647        }
648    }
649    None
650}
651
652#[cfg(test)]
653mod tests {
654    use super::*;
655    use astmap_core::SymbolKind;
656
657    fn parse(source: &str) -> ParseResult {
658        GoParser.parse(source, &PathBuf::from("test.go"))
659    }
660
661    // --- Symbol extraction ---
662
663    #[test]
664    fn test_parse_function() {
665        let result = parse("package main\nfunc hello() {}");
666        assert!(!result.has_errors);
667        let funcs: Vec<_> = result
668            .symbols
669            .iter()
670            .filter(|s| s.kind == SymbolKind::Function)
671            .collect();
672        assert_eq!(funcs.len(), 1);
673        assert_eq!(funcs[0].name, "hello");
674    }
675
676    #[test]
677    fn test_parse_method() {
678        let source = r#"package main
679type User struct { Name string }
680func (u *User) Greet() string { return u.Name }
681"#;
682        let result = parse(source);
683        assert!(!result.has_errors);
684        let methods: Vec<_> = result
685            .symbols
686            .iter()
687            .filter(|s| s.kind == SymbolKind::Method)
688            .collect();
689        assert_eq!(methods.len(), 1);
690        assert_eq!(methods[0].name, "Greet");
691        // Should be linked to User struct
692        assert!(
693            methods[0].parent_index.is_some(),
694            "method should be linked to receiver type"
695        );
696    }
697
698    #[test]
699    fn test_parse_method_pointer_vs_value_receiver() {
700        let source = r#"package main
701type User struct {}
702func (u *User) PtrMethod() {}
703func (u User) ValMethod() {}
704"#;
705        let result = parse(source);
706        let methods: Vec<_> = result
707            .symbols
708            .iter()
709            .filter(|s| s.kind == SymbolKind::Method)
710            .collect();
711        assert_eq!(methods.len(), 2);
712        assert!(
713            methods[0].parent_index.is_some(),
714            "pointer receiver method should be linked"
715        );
716        assert!(
717            methods[1].parent_index.is_some(),
718            "value receiver method should be linked"
719        );
720    }
721
722    #[test]
723    fn test_parse_struct() {
724        let source = r#"package main
725type User struct {
726    Name string
727    Age  int
728}
729"#;
730        let result = parse(source);
731        assert!(!result.has_errors);
732        let structs: Vec<_> = result
733            .symbols
734            .iter()
735            .filter(|s| s.kind == SymbolKind::Struct)
736            .collect();
737        assert_eq!(structs.len(), 1);
738        assert_eq!(structs[0].name, "User");
739    }
740
741    #[test]
742    fn test_parse_interface() {
743        let source = r#"package main
744type Reader interface {
745    Read(p []byte) (n int, err error)
746}
747"#;
748        let result = parse(source);
749        assert!(!result.has_errors);
750        let ifaces: Vec<_> = result
751            .symbols
752            .iter()
753            .filter(|s| s.kind == SymbolKind::Interface)
754            .collect();
755        assert_eq!(ifaces.len(), 1);
756        assert_eq!(ifaces[0].name, "Reader");
757    }
758
759    #[test]
760    fn test_parse_type_alias() {
761        let source = r#"package main
762type MyInt = int
763type Duration int64
764"#;
765        let result = parse(source);
766        let aliases: Vec<_> = result
767            .symbols
768            .iter()
769            .filter(|s| s.kind == SymbolKind::TypeAlias)
770            .collect();
771        assert_eq!(aliases.len(), 2);
772        assert_eq!(aliases[0].name, "MyInt");
773        assert_eq!(aliases[1].name, "Duration");
774    }
775
776    #[test]
777    fn test_parse_const() {
778        let source = r#"package main
779const MaxSize = 100
780const (
781    A = 1
782    B = 2
783)
784"#;
785        let result = parse(source);
786        let consts: Vec<_> = result
787            .symbols
788            .iter()
789            .filter(|s| s.kind == SymbolKind::Const)
790            .collect();
791        assert_eq!(consts.len(), 3);
792        assert_eq!(consts[0].name, "MaxSize");
793        assert_eq!(consts[1].name, "A");
794        assert_eq!(consts[2].name, "B");
795    }
796
797    #[test]
798    fn test_parse_var() {
799        let source = r#"package main
800var GlobalFlag = true
801var (
802    X = 1
803    Y = "hello"
804)
805"#;
806        let result = parse(source);
807        let vars: Vec<_> = result
808            .symbols
809            .iter()
810            .filter(|s| s.kind == SymbolKind::Variable)
811            .collect();
812        assert_eq!(vars.len(), 3);
813        assert_eq!(vars[0].name, "GlobalFlag");
814        assert_eq!(vars[1].name, "X");
815        assert_eq!(vars[2].name, "Y");
816    }
817
818    #[test]
819    fn test_parse_exported_vs_unexported() {
820        let source = r#"package main
821func ExportedFunc() {}
822func unexportedFunc() {}
823type ExportedType struct {}
824type unexportedType struct {}
825"#;
826        let result = parse(source);
827        let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
828        assert!(names.contains(&"ExportedFunc"));
829        assert!(names.contains(&"unexportedFunc"));
830        assert!(names.contains(&"ExportedType"));
831        assert!(names.contains(&"unexportedType"));
832    }
833
834    #[test]
835    fn test_parse_nested_struct_method() {
836        let source = r#"package main
837type Server struct { Port int }
838func (s *Server) Start() {}
839func (s *Server) Stop() {}
840"#;
841        let result = parse(source);
842        let server_idx = result
843            .symbols
844            .iter()
845            .position(|s| s.name == "Server")
846            .unwrap();
847        let methods: Vec<_> = result
848            .symbols
849            .iter()
850            .filter(|s| s.kind == SymbolKind::Method)
851            .collect();
852        assert_eq!(methods.len(), 2);
853        assert_eq!(methods[0].parent_index, Some(server_idx));
854        assert_eq!(methods[1].parent_index, Some(server_idx));
855    }
856
857    // --- Import extraction ---
858
859    #[test]
860    fn test_import_simple() {
861        let result = parse(
862            r#"package main
863import "fmt"
864"#,
865        );
866        // Filter out the synthetic ".::*" import
867        let real_imports: Vec<_> = result.imports.iter().filter(|i| i.path != ".::*").collect();
868        assert_eq!(real_imports.len(), 1);
869        assert_eq!(real_imports[0].path, "fmt");
870        assert_eq!(real_imports[0].imported_symbols, vec!["fmt"]);
871    }
872
873    #[test]
874    fn test_import_grouped() {
875        let result = parse(
876            r#"package main
877import (
878    "fmt"
879    "net/http"
880)
881"#,
882        );
883        let real_imports: Vec<_> = result.imports.iter().filter(|i| i.path != ".::*").collect();
884        assert_eq!(real_imports.len(), 2);
885        assert_eq!(real_imports[0].path, "fmt");
886        assert_eq!(real_imports[1].path, "net/http");
887        assert_eq!(real_imports[1].imported_symbols, vec!["http"]);
888    }
889
890    #[test]
891    fn test_import_aliased() {
892        let result = parse(
893            r#"package main
894import f "fmt"
895"#,
896        );
897        let real_imports: Vec<_> = result.imports.iter().filter(|i| i.path != ".::*").collect();
898        assert_eq!(real_imports.len(), 1);
899        assert_eq!(real_imports[0].path, "fmt");
900        assert_eq!(real_imports[0].imported_symbols, vec!["f"]);
901    }
902
903    #[test]
904    fn test_import_dot() {
905        let result = parse(
906            r#"package main
907import . "fmt"
908"#,
909        );
910        // Dot import becomes wildcard: "fmt::*"
911        let dot_imports: Vec<_> = result
912            .imports
913            .iter()
914            .filter(|i| i.path == "fmt::*")
915            .collect();
916        assert_eq!(dot_imports.len(), 1);
917        assert!(dot_imports[0].imported_symbols.is_empty());
918        // No non-wildcard, non-synthetic imports
919        let plain_imports: Vec<_> = result
920            .imports
921            .iter()
922            .filter(|i| !i.path.ends_with("::*"))
923            .collect();
924        assert_eq!(plain_imports.len(), 0);
925    }
926
927    #[test]
928    fn test_import_blank() {
929        let result = parse(
930            r#"package main
931import _ "database/sql"
932"#,
933        );
934        let real_imports: Vec<_> = result.imports.iter().filter(|i| i.path != ".::*").collect();
935        assert_eq!(real_imports.len(), 1);
936        assert_eq!(real_imports[0].path, "database/sql");
937        assert!(real_imports[0].imported_symbols.is_empty());
938    }
939
940    #[test]
941    fn test_synthetic_package_import() {
942        let result = parse("package main\nfunc main() {}");
943        let synthetic: Vec<_> = result.imports.iter().filter(|i| i.path == ".::*").collect();
944        assert_eq!(
945            synthetic.len(),
946            1,
947            "synthetic .::* import should be present"
948        );
949        assert!(synthetic[0].imported_symbols.is_empty());
950    }
951
952    // --- Call extraction ---
953
954    #[test]
955    fn test_extract_calls_simple() {
956        let source = r#"package main
957func helper() {}
958func main() {
959    helper()
960}
961"#;
962        let result = parse(source);
963        let callee_names: Vec<&str> = result
964            .calls
965            .iter()
966            .map(|c| c.callee_name.as_str())
967            .collect();
968        assert!(callee_names.contains(&"helper"));
969    }
970
971    #[test]
972    fn test_extract_calls_method() {
973        let source = r#"package main
974type S struct {}
975func (s *S) Do() {}
976func main() {
977    s := S{}
978    s.Do()
979}
980"#;
981        let result = parse(source);
982        let callee_names: Vec<&str> = result
983            .calls
984            .iter()
985            .map(|c| c.callee_name.as_str())
986            .collect();
987        assert!(callee_names.contains(&"Do"));
988    }
989
990    #[test]
991    fn test_extract_calls_package_qualified() {
992        let source = r#"package main
993import "fmt"
994func main() {
995    fmt.Println("hello")
996}
997"#;
998        let result = parse(source);
999        let callee_names: Vec<&str> = result
1000            .calls
1001            .iter()
1002            .map(|c| c.callee_name.as_str())
1003            .collect();
1004        assert!(
1005            callee_names.contains(&"Println"),
1006            "should strip package qualifier"
1007        );
1008    }
1009
1010    // --- Type reference extraction ---
1011
1012    #[test]
1013    fn test_extract_type_refs_param() {
1014        let source = r#"package main
1015type Config struct {}
1016func process(c Config) {}
1017"#;
1018        let result = parse(source);
1019        let type_names: Vec<&str> = result
1020            .type_refs
1021            .iter()
1022            .map(|r| r.to_type.as_str())
1023            .collect();
1024        assert!(type_names.contains(&"Config"));
1025    }
1026
1027    #[test]
1028    fn test_extract_type_refs_return() {
1029        let source = r#"package main
1030type Result struct {}
1031func create() Result { return Result{} }
1032"#;
1033        let result = parse(source);
1034        let type_names: Vec<&str> = result
1035            .type_refs
1036            .iter()
1037            .map(|r| r.to_type.as_str())
1038            .collect();
1039        assert!(type_names.contains(&"Result"));
1040    }
1041
1042    #[test]
1043    fn test_extract_type_refs_field() {
1044        let source = r#"package main
1045type Address struct { City string }
1046type User struct {
1047    Addr Address
1048}
1049"#;
1050        let result = parse(source);
1051        let type_names: Vec<&str> = result
1052            .type_refs
1053            .iter()
1054            .map(|r| r.to_type.as_str())
1055            .collect();
1056        assert!(type_names.contains(&"Address"));
1057    }
1058
1059    #[test]
1060    fn test_extract_type_refs_builtin_filtered() {
1061        let source = r#"package main
1062func foo(s string, n int, b bool) float64 { return 0.0 }
1063"#;
1064        let result = parse(source);
1065        let type_names: Vec<&str> = result
1066            .type_refs
1067            .iter()
1068            .map(|r| r.to_type.as_str())
1069            .collect();
1070        assert!(
1071            !type_names.contains(&"string"),
1072            "builtins should be filtered"
1073        );
1074        assert!(!type_names.contains(&"int"));
1075        assert!(!type_names.contains(&"bool"));
1076        assert!(!type_names.contains(&"float64"));
1077    }
1078
1079    // --- Resolver ---
1080
1081    #[test]
1082    fn test_resolve_stdlib_skip() {
1083        let tmp = tempfile::tempdir().unwrap();
1084        std::fs::write(
1085            tmp.path().join("go.mod"),
1086            "module example.com/myproject\n\ngo 1.21\n",
1087        )
1088        .unwrap();
1089        let resolver = GoResolver::new(tmp.path());
1090        assert_eq!(resolver.resolve_import("fmt", "main.go", tmp.path()), None);
1091        assert_eq!(
1092            resolver.resolve_import("net/http", "main.go", tmp.path()),
1093            None
1094        );
1095    }
1096
1097    #[test]
1098    fn test_resolve_external_skip() {
1099        let tmp = tempfile::tempdir().unwrap();
1100        std::fs::write(
1101            tmp.path().join("go.mod"),
1102            "module example.com/myproject\n\ngo 1.21\n",
1103        )
1104        .unwrap();
1105        let resolver = GoResolver::new(tmp.path());
1106        assert_eq!(
1107            resolver.resolve_import("github.com/other/pkg", "main.go", tmp.path()),
1108            None
1109        );
1110    }
1111
1112    #[test]
1113    fn test_resolve_local_package() {
1114        let tmp = tempfile::tempdir().unwrap();
1115        std::fs::write(
1116            tmp.path().join("go.mod"),
1117            "module example.com/myproject\n\ngo 1.21\n",
1118        )
1119        .unwrap();
1120        let pkg_dir = tmp.path().join("pkg");
1121        std::fs::create_dir_all(&pkg_dir).unwrap();
1122        std::fs::write(pkg_dir.join("service.go"), "package pkg\n").unwrap();
1123
1124        let resolver = GoResolver::new(tmp.path());
1125        let resolved = resolver.resolve_import("example.com/myproject/pkg", "main.go", tmp.path());
1126        assert!(resolved.is_some(), "local package should resolve");
1127        assert!(resolved.unwrap().ends_with("service.go"));
1128    }
1129
1130    #[test]
1131    fn test_resolve_no_gomod() {
1132        let tmp = tempfile::tempdir().unwrap();
1133        let resolver = GoResolver::new(tmp.path());
1134        // Without go.mod, no module path — can't resolve anything with dots
1135        assert_eq!(
1136            resolver.resolve_import("example.com/myproject/pkg", "main.go", tmp.path()),
1137            None
1138        );
1139        // Stdlib should still be skipped
1140        assert_eq!(resolver.resolve_import("fmt", "main.go", tmp.path()), None);
1141    }
1142
1143    #[test]
1144    fn test_resolve_dot_import() {
1145        let tmp = tempfile::tempdir().unwrap();
1146        let pkg_dir = tmp.path().join("pkg");
1147        std::fs::create_dir_all(&pkg_dir).unwrap();
1148        std::fs::write(pkg_dir.join("types.go"), "package pkg\n").unwrap();
1149        std::fs::write(pkg_dir.join("service.go"), "package pkg\n").unwrap();
1150
1151        let resolver = GoResolver::new(tmp.path());
1152        let resolved = resolver.resolve_import(".::*", "pkg/service.go", tmp.path());
1153        assert!(
1154            resolved.is_some(),
1155            "synthetic .::* should resolve to a sibling"
1156        );
1157        let resolved_path = resolved.unwrap();
1158        assert!(resolved_path.ends_with("types.go"));
1159    }
1160
1161    #[test]
1162    fn test_resolve_gomod_with_comments() {
1163        let tmp = tempfile::tempdir().unwrap();
1164        std::fs::write(
1165            tmp.path().join("go.mod"),
1166            "// This is a comment\n\nmodule example.com/myproject\n\ngo 1.21\n",
1167        )
1168        .unwrap();
1169        let resolver = GoResolver::new(tmp.path());
1170        assert!(
1171            resolver.module_path.is_some(),
1172            "should parse module path despite comments"
1173        );
1174        assert_eq!(
1175            resolver.module_path.as_deref(),
1176            Some("example.com/myproject")
1177        );
1178    }
1179
1180    // --- Edge cases ---
1181
1182    #[test]
1183    fn test_empty_file() {
1184        let result = parse("");
1185        assert!(
1186            result.symbols.is_empty()
1187                || result
1188                    .symbols
1189                    .iter()
1190                    .all(|s| s.kind != SymbolKind::Function)
1191        );
1192    }
1193
1194    #[test]
1195    fn test_package_only() {
1196        let result = parse("package main\n");
1197        assert!(!result.has_errors);
1198        // Only synthetic import, no real symbols
1199        let real_symbols: Vec<_> = result
1200            .symbols
1201            .iter()
1202            .filter(|s| s.kind != SymbolKind::Module)
1203            .collect();
1204        assert!(real_symbols.is_empty());
1205    }
1206
1207    #[test]
1208    fn test_malformed_file() {
1209        let result = parse("package main\nfunc {{{ invalid syntax");
1210        assert!(result.has_errors);
1211    }
1212}