1use 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
45pub(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 link_methods_to_receivers(source, &mut result.symbols);
78
79 result.imports.push(ExtractedImport {
82 path: ".::*".to_string(),
83 imported_symbols: vec![],
84 });
85
86 result
87}
88
89fn 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 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 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 Some(symbol_from_node(
157 name,
158 SymbolKind::Method,
159 node,
160 source,
161 None,
162 ))
163}
164
165fn receiver_type_name(node: &tree_sitter::Node, source: &str) -> Option<String> {
168 let receiver = node.child_by_field_name("receiver")?;
169 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 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 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 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 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
248fn 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
260fn 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 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 let name = child
298 .child_by_field_name("name")
299 .map(|n| node_text(&n, source))
300 .or_else(|| {
301 (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 collect_specs(&child, source, symbols, kind);
313 }
314 }
315 }
316}
317
318fn link_methods_to_receivers(source: &str, symbols: &mut [ExtractedSymbol]) {
323 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 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 while *method_idx < symbols.len() {
368 if symbols[*method_idx].kind == SymbolKind::Method {
369 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
391fn 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 collect_import_specs(&child, source, imports);
415 }
416 }
417 }
418}
419
420fn extract_import_spec(node: &tree_sitter::Node, source: &str) -> Option<ExtractedImport> {
421 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 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 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 Some(ExtractedImport {
443 path,
444 imported_symbols: vec![],
445 })
446 }
447 Some(".") => {
448 Some(ExtractedImport {
450 path: format!("{}::*", path),
451 imported_symbols: vec![],
452 })
453 }
454 Some(alias_name) => {
455 Some(ExtractedImport {
457 path,
458 imported_symbols: vec![alias_name.to_string()],
459 })
460 }
461 None => {
462 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
472fn 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 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
520fn 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
552pub(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 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 let first_segment = effective_path.split('/').next().unwrap_or(effective_path);
583 if !first_segment.contains('.') {
584 return None;
585 }
586
587 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 None
598 }
599}
600
601fn 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
618fn 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 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
639fn 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 #[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 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 #[test]
860 fn test_import_simple() {
861 let result = parse(
862 r#"package main
863import "fmt"
864"#,
865 );
866 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 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 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 #[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 #[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 #[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 assert_eq!(
1136 resolver.resolve_import("example.com/myproject/pkg", "main.go", tmp.path()),
1137 None
1138 );
1139 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 #[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 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}