1use std::cell::RefCell;
2use std::path::Path;
3
4use tree_sitter::{Node, Parser};
5use tree_sitter_language::LanguageFn;
6
7use domain::error::CodeGraphError;
8use domain::model::{Edge, EdgeKind, Language, Location, SymbolKind, SymbolNode, Visibility};
9
10use crate::{ImportName, LanguageParser, ParseResult, RawImport};
11
12thread_local! {
13 static GO_PARSER: RefCell<Parser> = RefCell::new(Parser::new());
14}
15
16pub struct GoParser {
18 lang: LanguageFn,
19}
20
21impl GoParser {
22 pub fn new() -> Self {
23 Self {
24 lang: tree_sitter_go::LANGUAGE,
25 }
26 }
27}
28
29impl Default for GoParser {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35impl LanguageParser for GoParser {
36 fn language(&self) -> Language {
37 Language::Go
38 }
39
40 fn file_extensions(&self) -> &[&str] {
41 &["go"]
42 }
43
44 fn parse(&self, source: &[u8], path: &Path) -> domain::error::Result<ParseResult> {
45 let lang: tree_sitter::Language = self.lang.into();
46
47 GO_PARSER.with(|parser_cell| {
48 let mut parser = parser_cell.borrow_mut();
49 parser
50 .set_language(&lang)
51 .map_err(|e| CodeGraphError::Parse {
52 file: path.to_path_buf(),
53 message: format!("failed to set language: {e}"),
54 })?;
55
56 let tree = parser
57 .parse(source, None)
58 .ok_or_else(|| CodeGraphError::Parse {
59 file: path.to_path_buf(),
60 message: "tree-sitter parse returned None".into(),
61 })?;
62
63 extract_all(source, path, &tree)
64 })
65 }
66}
67
68fn extract_all(
73 source: &[u8],
74 path: &Path,
75 tree: &tree_sitter::Tree,
76) -> domain::error::Result<ParseResult> {
77 let mut symbols = Vec::new();
78 let mut edges = Vec::new();
79 let file_path = path.to_string_lossy().to_string();
80 let root = tree.root_node();
81 let mut cursor = root.walk();
82
83 for child in root.children(&mut cursor) {
84 if !child.is_named() {
85 continue;
86 }
87 match child.kind() {
88 "function_declaration" => {
89 extract_function(source, &file_path, child, &mut symbols, &mut edges);
90 }
91 "method_declaration" => {
92 extract_method(source, &file_path, child, &mut symbols, &mut edges);
93 }
94 "type_declaration" => {
95 extract_type_declaration(source, &file_path, child, &mut symbols, &mut edges);
96 }
97 "const_declaration" => {
98 extract_const_declaration(source, &file_path, child, &mut symbols, &mut edges);
99 }
100 "var_declaration" => {
101 extract_var_declaration(source, &file_path, child, &mut symbols, &mut edges);
102 }
103 _ => {}
104 }
105 }
106
107 let imports = extract_imports(source, &root);
108
109 Ok(ParseResult {
110 symbols,
111 edges,
112 imports,
113 exports: Vec::new(),
114 })
115}
116
117fn node_location(file_path: &str, node: Node) -> Location {
123 let start = node.start_position();
124 let end = node.end_position();
125 Location {
126 file: file_path.into(),
127 line_start: start.row + 1, line_end: end.row + 1,
129 col_start: start.column,
130 col_end: end.column,
131 }
132}
133
134fn node_text<'a>(node: Node, source: &'a [u8]) -> &'a str {
136 node.utf8_text(source).unwrap_or("")
137}
138
139fn go_visibility(name: &str) -> (Visibility, bool) {
141 if name.starts_with(|c: char| c.is_ascii_uppercase()) {
142 (Visibility::Public, true)
143 } else {
144 (Visibility::Private, false)
145 }
146}
147
148fn is_go_test(name: &str) -> bool {
150 name.starts_with("Test")
151 || name.starts_with("Bench")
152 || name.starts_with("Example")
153 || name.starts_with("Fuzz")
154}
155
156fn contains_edge(file_path: &str, qualified_name: &str) -> Edge {
158 Edge {
159 kind: EdgeKind::Contains,
160 source: file_path.to_string(),
161 target: qualified_name.to_string(),
162 metadata: None,
163 }
164}
165
166fn extract_function(
168 source: &[u8],
169 file_path: &str,
170 node: Node,
171 symbols: &mut Vec<SymbolNode>,
172 edges: &mut Vec<Edge>,
173) {
174 let name_node = match node.child_by_field_name("name") {
175 Some(n) => n,
176 None => return,
177 };
178 let name = node_text(name_node, source).to_string();
179 if name.is_empty() {
180 return;
181 }
182 let qualified_name = format!("{file_path}::{name}");
183 let (visibility, is_exported) = go_visibility(&name);
184
185 symbols.push(SymbolNode {
186 name: name.clone(),
187 qualified_name: qualified_name.clone(),
188 kind: SymbolKind::Function,
189 location: node_location(file_path, node),
190 visibility,
191 is_exported,
192 is_async: false,
193 is_test: is_go_test(&name),
194 decorators: Vec::new(),
195 signature: None,
196 });
197 edges.push(contains_edge(file_path, &qualified_name));
198}
199
200fn extract_receiver_type(node: Node, source: &[u8]) -> Option<String> {
203 let receiver = node.child_by_field_name("receiver")?;
204 let mut cursor = receiver.walk();
206 for child in receiver.children(&mut cursor) {
207 if child.kind() == "parameter_declaration" {
208 if let Some(type_node) = child.child_by_field_name("type") {
210 return Some(unwrap_pointer_type(type_node, source));
211 }
212 }
213 }
214 None
215}
216
217fn unwrap_pointer_type(node: Node, source: &[u8]) -> String {
219 match node.kind() {
220 "pointer_type" => {
221 let mut cursor = node.walk();
223 for child in node.children(&mut cursor) {
224 if child.is_named() {
225 return node_text(child, source).to_string();
226 }
227 }
228 node_text(node, source).trim_start_matches('*').to_string()
229 }
230 "qualified_type" => {
231 node_text(node, source).to_string()
233 }
234 _ => node_text(node, source).to_string(),
235 }
236}
237
238fn extract_method(
240 source: &[u8],
241 file_path: &str,
242 node: Node,
243 symbols: &mut Vec<SymbolNode>,
244 edges: &mut Vec<Edge>,
245) {
246 let name_node = match node.child_by_field_name("name") {
247 Some(n) => n,
248 None => return,
249 };
250 let method_name = node_text(name_node, source).to_string();
251 if method_name.is_empty() {
252 return;
253 }
254
255 let receiver_type = extract_receiver_type(node, source);
256 let (visibility, is_exported) = go_visibility(&method_name);
257
258 let qualified_name = if let Some(ref rt) = receiver_type {
259 format!("{file_path}::{rt}.{method_name}")
260 } else {
261 format!("{file_path}::{method_name}")
262 };
263
264 symbols.push(SymbolNode {
265 name: method_name.clone(),
266 qualified_name: qualified_name.clone(),
267 kind: SymbolKind::Method,
268 location: node_location(file_path, node),
269 visibility,
270 is_exported,
271 is_async: false,
272 is_test: is_go_test(&method_name),
273 decorators: Vec::new(),
274 signature: None,
275 });
276 edges.push(contains_edge(file_path, &qualified_name));
277
278 if let Some(ref rt) = receiver_type {
280 let struct_qn = format!("{file_path}::{rt}");
281 edges.push(Edge {
282 kind: EdgeKind::ChildOf,
283 source: qualified_name,
284 target: struct_qn,
285 metadata: None,
286 });
287 }
288}
289
290fn extract_type_declaration(
292 source: &[u8],
293 file_path: &str,
294 node: Node,
295 symbols: &mut Vec<SymbolNode>,
296 edges: &mut Vec<Edge>,
297) {
298 let mut cursor = node.walk();
299 for child in node.children(&mut cursor) {
300 match child.kind() {
301 "type_spec" => {
302 extract_type_spec(source, file_path, child, symbols, edges);
303 }
304 "type_alias" => {
305 extract_type_alias(source, file_path, child, symbols, edges);
306 }
307 _ => {}
308 }
309 }
310}
311
312fn extract_type_spec(
314 source: &[u8],
315 file_path: &str,
316 node: Node,
317 symbols: &mut Vec<SymbolNode>,
318 edges: &mut Vec<Edge>,
319) {
320 let name_node = match node.child_by_field_name("name") {
321 Some(n) => n,
322 None => return,
323 };
324 let name = node_text(name_node, source).to_string();
325 if name.is_empty() {
326 return;
327 }
328
329 let type_node = node.child_by_field_name("type");
330 let kind = match type_node.as_ref().map(|n| n.kind()) {
331 Some("struct_type") => SymbolKind::Struct,
332 Some("interface_type") => SymbolKind::Interface,
333 _ => SymbolKind::TypeAlias,
334 };
335
336 let qualified_name = format!("{file_path}::{name}");
337 let (visibility, is_exported) = go_visibility(&name);
338
339 symbols.push(SymbolNode {
340 name: name.clone(),
341 qualified_name: qualified_name.clone(),
342 kind,
343 location: node_location(file_path, node),
344 visibility,
345 is_exported,
346 is_async: false,
347 is_test: false,
348 decorators: Vec::new(),
349 signature: None,
350 });
351 edges.push(contains_edge(file_path, &qualified_name));
352
353 if let Some(struct_node) = type_node.filter(|n| n.kind() == "struct_type") {
355 extract_struct_embeddings(source, file_path, &qualified_name, struct_node, edges);
356 }
357}
358
359fn extract_type_alias(
361 source: &[u8],
362 file_path: &str,
363 node: Node,
364 symbols: &mut Vec<SymbolNode>,
365 edges: &mut Vec<Edge>,
366) {
367 let name_node = match node.child_by_field_name("name") {
368 Some(n) => n,
369 None => return,
370 };
371 let name = node_text(name_node, source).to_string();
372 if name.is_empty() {
373 return;
374 }
375
376 let qualified_name = format!("{file_path}::{name}");
377 let (visibility, is_exported) = go_visibility(&name);
378
379 symbols.push(SymbolNode {
380 name: name.clone(),
381 qualified_name: qualified_name.clone(),
382 kind: SymbolKind::TypeAlias,
383 location: node_location(file_path, node),
384 visibility,
385 is_exported,
386 is_async: false,
387 is_test: false,
388 decorators: Vec::new(),
389 signature: None,
390 });
391 edges.push(contains_edge(file_path, &qualified_name));
392}
393
394fn extract_struct_embeddings(
397 source: &[u8],
398 file_path: &str,
399 struct_qn: &str,
400 struct_node: Node,
401 edges: &mut Vec<Edge>,
402) {
403 let mut cursor = struct_node.walk();
404 for child in struct_node.children(&mut cursor) {
405 if child.kind() != "field_declaration_list" {
406 continue;
407 }
408 let mut list_cursor = child.walk();
409 for field in child.children(&mut list_cursor) {
410 if field.kind() != "field_declaration" {
411 continue;
412 }
413 if field.child_by_field_name("name").is_none() {
415 if let Some(type_node) = field.child_by_field_name("type") {
416 let embedded_name = unwrap_pointer_type(type_node, source);
417 if !embedded_name.is_empty() {
418 let embedded_qn = format!("{file_path}::{embedded_name}");
419 edges.push(Edge {
420 kind: EdgeKind::Embeds,
421 source: struct_qn.to_string(),
422 target: embedded_qn,
423 metadata: None,
424 });
425 }
426 }
427 }
428 }
429 }
430}
431
432fn extract_const_declaration(
434 source: &[u8],
435 file_path: &str,
436 node: Node,
437 symbols: &mut Vec<SymbolNode>,
438 edges: &mut Vec<Edge>,
439) {
440 let mut cursor = node.walk();
441 for child in node.children(&mut cursor) {
442 if child.kind() == "const_spec" {
443 extract_spec_names(source, file_path, child, SymbolKind::Const, symbols, edges);
444 }
445 }
446}
447
448fn extract_var_declaration(
450 source: &[u8],
451 file_path: &str,
452 node: Node,
453 symbols: &mut Vec<SymbolNode>,
454 edges: &mut Vec<Edge>,
455) {
456 let mut cursor = node.walk();
457 for child in node.children(&mut cursor) {
458 if child.kind() == "var_spec" {
459 extract_spec_names(
460 source,
461 file_path,
462 child,
463 SymbolKind::Variable,
464 symbols,
465 edges,
466 );
467 }
468 }
469}
470
471fn extract_spec_names(
474 source: &[u8],
475 file_path: &str,
476 node: Node,
477 kind: SymbolKind,
478 symbols: &mut Vec<SymbolNode>,
479 edges: &mut Vec<Edge>,
480) {
481 let mut cursor = node.walk();
482 for child in node.children(&mut cursor) {
483 if child.kind() == "identifier" {
485 let name = node_text(child, source).to_string();
486 if name.is_empty() {
487 continue;
488 }
489 let qualified_name = format!("{file_path}::{name}");
490 let (visibility, is_exported) = go_visibility(&name);
491
492 symbols.push(SymbolNode {
493 name: name.clone(),
494 qualified_name: qualified_name.clone(),
495 kind,
496 location: node_location(file_path, node),
497 visibility,
498 is_exported,
499 is_async: false,
500 is_test: false,
501 decorators: Vec::new(),
502 signature: None,
503 });
504 edges.push(contains_edge(file_path, &qualified_name));
505 }
506 }
507}
508
509fn extract_imports(source: &[u8], root: &Node) -> Vec<RawImport> {
515 let mut imports = Vec::new();
516 let mut cursor = root.walk();
517
518 for child in root.children(&mut cursor) {
519 if child.kind() == "import_declaration" {
520 extract_import_declaration(source, child, &mut imports);
521 }
522 }
523
524 imports
525}
526
527fn extract_import_declaration(source: &[u8], node: Node, imports: &mut Vec<RawImport>) {
529 let mut cursor = node.walk();
530 for child in node.children(&mut cursor) {
531 match child.kind() {
532 "import_spec" => {
533 if let Some(imp) = parse_import_spec(source, child) {
534 imports.push(imp);
535 }
536 }
537 "import_spec_list" => {
538 let mut list_cursor = child.walk();
539 for spec in child.children(&mut list_cursor) {
540 if spec.kind() == "import_spec" {
541 if let Some(imp) = parse_import_spec(source, spec) {
542 imports.push(imp);
543 }
544 }
545 }
546 }
547 _ => {}
548 }
549 }
550}
551
552fn strip_go_quotes(s: &str) -> &str {
554 s.trim_matches('"')
555}
556
557fn parse_import_spec(source: &[u8], node: Node) -> Option<RawImport> {
559 let path_node = node.child_by_field_name("path")?;
560 let raw_path = node_text(path_node, source);
561 let specifier = strip_go_quotes(raw_path).to_string();
562 let line = node.start_position().row + 1;
563
564 let name_node = node.child_by_field_name("name");
566
567 let (is_side_effect, is_namespace, alias) = match name_node {
568 Some(n) => match n.kind() {
569 "blank_identifier" => (true, false, None),
570 "dot" => (false, true, None),
571 "package_identifier" => {
572 let alias_text = node_text(n, source).to_string();
573 (false, false, Some(alias_text))
574 }
575 _ => (false, false, None),
576 },
577 None => (false, false, None),
578 };
579
580 let names = if is_side_effect || is_namespace {
581 Vec::new()
582 } else {
583 let pkg = specifier
584 .split('/')
585 .next_back()
586 .unwrap_or(&specifier)
587 .to_string();
588 vec![ImportName {
589 name: pkg,
590 alias,
591 is_type: false,
592 }]
593 };
594
595 Some(RawImport {
596 specifier,
597 names,
598 is_type_only: false,
599 is_side_effect,
600 is_namespace,
601 line,
602 })
603}
604
605#[cfg(test)]
610mod tests {
611 use super::*;
612 use domain::model::{EdgeKind, SymbolKind, Visibility};
613
614 fn parse_go(source: &str) -> ParseResult {
615 let parser = GoParser::new();
616 parser
617 .parse(source.as_bytes(), Path::new("test.go"))
618 .expect("parse failed")
619 }
620
621 #[test]
626 fn ac22_function_declaration_exported() {
627 let result = parse_go("package main\n\nfunc Foo() {}");
628 let sym = result.symbols.iter().find(|s| s.name == "Foo").unwrap();
629 assert_eq!(sym.kind, SymbolKind::Function);
630 assert_eq!(sym.visibility, Visibility::Public);
631 assert!(sym.is_exported);
632 assert!(!sym.is_test);
633 }
634
635 #[test]
636 fn ac22_function_declaration_unexported() {
637 let result = parse_go("package main\n\nfunc helper() {}");
638 let sym = result.symbols.iter().find(|s| s.name == "helper").unwrap();
639 assert_eq!(sym.kind, SymbolKind::Function);
640 assert_eq!(sym.visibility, Visibility::Private);
641 assert!(!sym.is_exported);
642 }
643
644 #[test]
645 fn ac22_function_contains_edge() {
646 let result = parse_go("package main\n\nfunc Foo() {}");
647 let edge = result
648 .edges
649 .iter()
650 .find(|e| e.kind == EdgeKind::Contains && e.target == "test.go::Foo")
651 .expect("Contains edge missing");
652 assert_eq!(edge.source, "test.go");
653 }
654
655 #[test]
660 fn ac23_method_declaration_value_receiver() {
661 let result = parse_go("package main\n\ntype Bar struct {}\n\nfunc (r Bar) Method() {}");
662 let sym = result.symbols.iter().find(|s| s.name == "Method").unwrap();
663 assert_eq!(sym.kind, SymbolKind::Method);
664 assert_eq!(sym.qualified_name, "test.go::Bar.Method");
665 }
666
667 #[test]
668 fn ac23_method_declaration_pointer_receiver() {
669 let result = parse_go("package main\n\ntype Bar struct {}\n\nfunc (r *Bar) Method() {}");
670 let sym = result.symbols.iter().find(|s| s.name == "Method").unwrap();
671 assert_eq!(sym.kind, SymbolKind::Method);
672 assert_eq!(sym.qualified_name, "test.go::Bar.Method");
673 }
674
675 #[test]
676 fn ac23_method_child_of_edge() {
677 let result = parse_go("package main\n\ntype Bar struct {}\n\nfunc (r *Bar) Method() {}");
678 let child_of = result
679 .edges
680 .iter()
681 .find(|e| e.kind == EdgeKind::ChildOf)
682 .expect("ChildOf edge missing");
683 assert_eq!(child_of.source, "test.go::Bar.Method");
684 assert_eq!(child_of.target, "test.go::Bar");
685 }
686
687 #[test]
692 fn ac24_struct_embedding() {
693 let result =
694 parse_go("package main\n\ntype Base struct {}\n\ntype Derived struct { Base }");
695 let sym = result.symbols.iter().find(|s| s.name == "Derived").unwrap();
696 assert_eq!(sym.kind, SymbolKind::Struct);
697
698 let embeds = result
699 .edges
700 .iter()
701 .find(|e| e.kind == EdgeKind::Embeds)
702 .expect("Embeds edge missing");
703 assert_eq!(embeds.source, "test.go::Derived");
704 assert_eq!(embeds.target, "test.go::Base");
705 }
706
707 #[test]
708 fn ac24_struct_pointer_embedding() {
709 let result =
710 parse_go("package main\n\ntype Base struct {}\n\ntype Derived struct { *Base }");
711 let embeds = result
712 .edges
713 .iter()
714 .find(|e| e.kind == EdgeKind::Embeds)
715 .expect("Embeds edge missing for pointer embedding");
716 assert_eq!(embeds.target, "test.go::Base");
717 }
718
719 #[test]
720 fn ac24_struct_no_embedding_for_named_field() {
721 let result = parse_go("package main\n\ntype Foo struct { bar string }");
722 let embeds_count = result
723 .edges
724 .iter()
725 .filter(|e| e.kind == EdgeKind::Embeds)
726 .count();
727 assert_eq!(
728 embeds_count, 0,
729 "named fields must not produce Embeds edges"
730 );
731 }
732
733 #[test]
738 fn ac25_interface_declaration() {
739 let result = parse_go("package main\n\ntype Baz interface { Method() }");
740 let sym = result.symbols.iter().find(|s| s.name == "Baz").unwrap();
741 assert_eq!(sym.kind, SymbolKind::Interface);
742 assert!(sym.is_exported);
743 }
744
745 #[test]
750 fn ac26_side_effect_import() {
751 let result = parse_go("package main\n\nimport _ \"lib/pq\"\n\nfunc main() {}");
752 let imp = result
753 .imports
754 .iter()
755 .find(|i| i.specifier == "lib/pq")
756 .expect("import not found");
757 assert!(imp.is_side_effect);
758 assert!(imp.names.is_empty());
759 }
760
761 #[test]
766 fn ac27_dot_import() {
767 let result = parse_go("package main\n\nimport . \"fmt\"\n\nfunc main() {}");
768 let imp = result
769 .imports
770 .iter()
771 .find(|i| i.specifier == "fmt")
772 .expect("import not found");
773 assert!(imp.is_namespace);
774 assert!(imp.names.is_empty());
775 }
776
777 #[test]
782 fn ac28_uppercase_is_public() {
783 let result = parse_go("package main\n\nfunc PublicFunc() {}");
784 let sym = result
785 .symbols
786 .iter()
787 .find(|s| s.name == "PublicFunc")
788 .unwrap();
789 assert_eq!(sym.visibility, Visibility::Public);
790 assert!(sym.is_exported);
791 }
792
793 #[test]
794 fn ac28_lowercase_is_private() {
795 let result = parse_go("package main\n\nfunc privateFunc() {}");
796 let sym = result
797 .symbols
798 .iter()
799 .find(|s| s.name == "privateFunc")
800 .unwrap();
801 assert_eq!(sym.visibility, Visibility::Private);
802 assert!(!sym.is_exported);
803 }
804
805 #[test]
810 fn ac49_empty_source_no_panic() {
811 let parser = GoParser::new();
812 let result = parser.parse(b"", Path::new("empty.go"));
813 assert!(result.is_ok());
814 let r = result.unwrap();
815 assert!(r.symbols.is_empty());
816 }
817
818 #[test]
823 fn ac50_partial_extraction_on_errors() {
824 let source = "package main\n\nfunc Valid() {}\nconst {{{;\nfunc AlsoValid() {}";
825 let result = parse_go(source);
826 assert!(
828 result.symbols.iter().any(|s| s.name == "Valid"),
829 "should extract 'Valid' even with syntax errors"
830 );
831 }
832
833 #[test]
838 fn type_alias_declaration() {
839 let result = parse_go("package main\n\ntype Alias = string");
840 let sym = result.symbols.iter().find(|s| s.name == "Alias").unwrap();
841 assert_eq!(sym.kind, SymbolKind::TypeAlias);
842 }
843
844 #[test]
849 fn const_declaration() {
850 let result = parse_go("package main\n\nconst X = 1");
851 let sym = result.symbols.iter().find(|s| s.name == "X").unwrap();
852 assert_eq!(sym.kind, SymbolKind::Const);
853 assert!(sym.is_exported);
854 }
855
856 #[test]
857 fn var_declaration() {
858 let result = parse_go("package main\n\nvar y string");
859 let sym = result.symbols.iter().find(|s| s.name == "y").unwrap();
860 assert_eq!(sym.kind, SymbolKind::Variable);
861 assert!(!sym.is_exported);
862 }
863
864 #[test]
869 fn test_function_detection() {
870 let result = parse_go("package main\n\nfunc TestFoo(t *testing.T) {}");
871 let sym = result.symbols.iter().find(|s| s.name == "TestFoo").unwrap();
872 assert!(sym.is_test);
873 }
874
875 #[test]
876 fn bench_function_detection() {
877 let result = parse_go("package main\n\nfunc BenchmarkFoo(b *testing.B) {}");
878 let sym = result
879 .symbols
880 .iter()
881 .find(|s| s.name == "BenchmarkFoo")
882 .unwrap();
883 assert!(sym.is_test);
884 }
885
886 #[test]
887 fn regular_function_not_test() {
888 let result = parse_go("package main\n\nfunc Process() {}");
889 let sym = result.symbols.iter().find(|s| s.name == "Process").unwrap();
890 assert!(!sym.is_test);
891 }
892
893 #[test]
898 fn regular_import() {
899 let result = parse_go("package main\n\nimport \"fmt\"\n\nfunc main() {}");
900 let imp = result
901 .imports
902 .iter()
903 .find(|i| i.specifier == "fmt")
904 .expect("fmt import not found");
905 assert!(!imp.is_side_effect);
906 assert!(!imp.is_namespace);
907 }
908
909 #[test]
910 fn aliased_import() {
911 let result = parse_go("package main\n\nimport myfmt \"fmt\"\n\nfunc main() {}");
912 let imp = result
913 .imports
914 .iter()
915 .find(|i| i.specifier == "fmt")
916 .expect("fmt import not found");
917 assert!(!imp.is_side_effect);
918 assert!(!imp.is_namespace);
919 assert_eq!(imp.names[0].alias, Some("myfmt".to_string()));
920 }
921
922 #[test]
923 fn grouped_imports() {
924 let source = r#"package main
925
926import (
927 "fmt"
928 "os"
929)
930
931func main() {}
932"#;
933 let result = parse_go(source);
934 assert!(result.imports.iter().any(|i| i.specifier == "fmt"));
935 assert!(result.imports.iter().any(|i| i.specifier == "os"));
936 }
937
938 #[test]
939 fn grouped_import_with_side_effect_and_alias() {
940 let source = r#"package main
941
942import (
943 "fmt"
944 _ "lib/pq"
945 . "math"
946 myfmt "fmt"
947)
948
949func main() {}
950"#;
951 let result = parse_go(source);
952 assert!(result
953 .imports
954 .iter()
955 .any(|i| i.specifier == "lib/pq" && i.is_side_effect));
956 assert!(result
957 .imports
958 .iter()
959 .any(|i| i.specifier == "math" && i.is_namespace));
960 assert!(result.imports.iter().any(|i| i.specifier == "fmt"));
961 }
962
963 #[test]
968 fn location_populated() {
969 let result = parse_go("package main\n\nfunc Foo() {}");
970 let sym = result.symbols.iter().find(|s| s.name == "Foo").unwrap();
971 assert_eq!(sym.location.file.to_string_lossy(), "test.go");
972 assert!(sym.location.line_start >= 1);
973 }
974
975 #[test]
980 fn qualified_name_function() {
981 let result = parse_go("package main\n\nfunc Foo() {}");
982 let sym = result.symbols.iter().find(|s| s.name == "Foo").unwrap();
983 assert_eq!(sym.qualified_name, "test.go::Foo");
984 }
985
986 #[test]
987 fn qualified_name_method() {
988 let result =
989 parse_go("package main\n\ntype MyStruct struct {}\n\nfunc (s *MyStruct) DoWork() {}");
990 let sym = result.symbols.iter().find(|s| s.name == "DoWork").unwrap();
991 assert_eq!(sym.qualified_name, "test.go::MyStruct.DoWork");
992 }
993
994 #[test]
999 fn integration_multi_construct_file() {
1000 let source = r#"package main
1001
1002import (
1003 "fmt"
1004 _ "lib/pq"
1005 . "math"
1006)
1007
1008const MaxItems = 100
1009
1010var globalVar string
1011
1012type Animal struct {
1013 Name string
1014}
1015
1016type Dog struct {
1017 Animal
1018 Breed string
1019}
1020
1021type Speaker interface {
1022 Speak() string
1023}
1024
1025type MyAlias = string
1026
1027func NewDog(name string) *Dog {
1028 return &Dog{}
1029}
1030
1031func (d *Dog) Bark() string {
1032 return "Woof"
1033}
1034
1035func TestDog(t interface{}) {}
1036"#;
1037 let result = parse_go(source);
1038
1039 assert!(result
1041 .symbols
1042 .iter()
1043 .any(|s| s.name == "MaxItems" && s.kind == SymbolKind::Const));
1044 assert!(result
1045 .symbols
1046 .iter()
1047 .any(|s| s.name == "globalVar" && s.kind == SymbolKind::Variable));
1048 assert!(result
1049 .symbols
1050 .iter()
1051 .any(|s| s.name == "Animal" && s.kind == SymbolKind::Struct));
1052 assert!(result
1053 .symbols
1054 .iter()
1055 .any(|s| s.name == "Dog" && s.kind == SymbolKind::Struct));
1056 assert!(result
1057 .symbols
1058 .iter()
1059 .any(|s| s.name == "Speaker" && s.kind == SymbolKind::Interface));
1060 assert!(result
1061 .symbols
1062 .iter()
1063 .any(|s| s.name == "MyAlias" && s.kind == SymbolKind::TypeAlias));
1064 assert!(result
1065 .symbols
1066 .iter()
1067 .any(|s| s.name == "NewDog" && s.kind == SymbolKind::Function));
1068 assert!(result
1069 .symbols
1070 .iter()
1071 .any(|s| s.name == "Bark" && s.kind == SymbolKind::Method));
1072
1073 let test_sym = result.symbols.iter().find(|s| s.name == "TestDog").unwrap();
1075 assert!(test_sym.is_test);
1076
1077 let max = result
1079 .symbols
1080 .iter()
1081 .find(|s| s.name == "MaxItems")
1082 .unwrap();
1083 assert_eq!(max.visibility, Visibility::Public);
1084 let gvar = result
1085 .symbols
1086 .iter()
1087 .find(|s| s.name == "globalVar")
1088 .unwrap();
1089 assert_eq!(gvar.visibility, Visibility::Private);
1090
1091 let child_of = result
1093 .edges
1094 .iter()
1095 .filter(|e| e.kind == EdgeKind::ChildOf)
1096 .count();
1097 assert!(child_of >= 1, "expected at least one ChildOf edge");
1098
1099 let embeds = result
1100 .edges
1101 .iter()
1102 .filter(|e| e.kind == EdgeKind::Embeds)
1103 .count();
1104 assert_eq!(
1105 embeds, 1,
1106 "expected one Embeds edge for Dog embedding Animal"
1107 );
1108
1109 let contains = result
1110 .edges
1111 .iter()
1112 .filter(|e| e.kind == EdgeKind::Contains)
1113 .count();
1114 assert!(contains >= 5, "expected many Contains edges");
1115
1116 assert!(result.imports.iter().any(|i| i.specifier == "fmt"));
1118 assert!(result
1119 .imports
1120 .iter()
1121 .any(|i| i.specifier == "lib/pq" && i.is_side_effect));
1122 assert!(result
1123 .imports
1124 .iter()
1125 .any(|i| i.specifier == "math" && i.is_namespace));
1126 }
1127}