Skip to main content

the_code_graph_parser/
go.rs

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
16/// Parser for Go (.go) files.
17pub 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
68// ---------------------------------------------------------------------------
69// Core extraction
70// ---------------------------------------------------------------------------
71
72fn 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
117// ---------------------------------------------------------------------------
118// Symbol extraction helpers
119// ---------------------------------------------------------------------------
120
121/// Build a Location from a tree-sitter node.
122fn 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, // Convert 0-based to 1-based
128        line_end: end.row + 1,
129        col_start: start.column,
130        col_end: end.column,
131    }
132}
133
134/// Get node text as a string.
135fn node_text<'a>(node: Node, source: &'a [u8]) -> &'a str {
136    node.utf8_text(source).unwrap_or("")
137}
138
139/// Check if a name is exported (starts with uppercase letter).
140fn 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
148/// Detect Go test functions (Test*, Bench*, Example*, Fuzz*).
149fn 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
156/// Create a Contains edge from file path to a qualified name.
157fn 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
166/// Extract a `function_declaration` node.
167fn 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
200/// Extract the receiver type name from a method_declaration's receiver parameter list.
201/// Handles: `(r Foo)`, `(r *Foo)`, `(Foo)`, `(*Foo)`.
202fn extract_receiver_type(node: Node, source: &[u8]) -> Option<String> {
203    let receiver = node.child_by_field_name("receiver")?;
204    // receiver is a parameter_list; find the first parameter_declaration
205    let mut cursor = receiver.walk();
206    for child in receiver.children(&mut cursor) {
207        if child.kind() == "parameter_declaration" {
208            // The type field holds the receiver type
209            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
217/// Unwrap pointer type (`*Foo` → `Foo`) or return the type identifier as-is.
218fn unwrap_pointer_type(node: Node, source: &[u8]) -> String {
219    match node.kind() {
220        "pointer_type" => {
221            // pointer_type has one child: the pointed-to type
222            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            // e.g. pkg.Bar — use the full text
232            node_text(node, source).to_string()
233        }
234        _ => node_text(node, source).to_string(),
235    }
236}
237
238/// Extract a `method_declaration` node.
239fn 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    // ChildOf edge: method → receiver type
279    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
290/// Extract a `type_declaration` node (may contain multiple `type_spec` or `type_alias`).
291fn 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
312/// Extract a `type_spec` (struct, interface, or named type).
313fn 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    // For structs: scan for embedding
354    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
359/// Extract a `type_alias` (type Alias = Other).
360fn 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
394/// Scan struct fields for embedded types and emit Embeds edges.
395/// An embedded field has no `name` field — only a `type` field.
396fn 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            // Embedded field: has a type but no name
414            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
432/// Extract a `const_declaration` node.
433fn 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
448/// Extract a `var_declaration` node.
449fn 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
471/// Extract names from a `const_spec` or `var_spec` node.
472/// Both have one or more `name` children (identifiers).
473fn 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        // Names are direct identifier children with field name "name"
484        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
509// ---------------------------------------------------------------------------
510// Import extraction
511// ---------------------------------------------------------------------------
512
513/// Extract all import declarations from the root node.
514fn 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
527/// Parse a single `import_declaration` node. It may contain one or more `import_spec`.
528fn 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
552/// Strip leading/trailing double-quote characters from a Go string literal.
553fn strip_go_quotes(s: &str) -> &str {
554    s.trim_matches('"')
555}
556
557/// Parse a single `import_spec` node into a `RawImport`.
558fn 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    // Check the optional name field
565    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// ---------------------------------------------------------------------------
606// Tests
607// ---------------------------------------------------------------------------
608
609#[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    // -----------------------------------------------------------------------
622    // AC22: function declaration → Function symbol
623    // -----------------------------------------------------------------------
624
625    #[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    // -----------------------------------------------------------------------
656    // AC23: method declaration → Method symbol + ChildOf edge
657    // -----------------------------------------------------------------------
658
659    #[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    // -----------------------------------------------------------------------
688    // AC24: struct with embedding → Struct symbol + Embeds edge
689    // -----------------------------------------------------------------------
690
691    #[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    // -----------------------------------------------------------------------
734    // AC25: interface declaration → Interface symbol
735    // -----------------------------------------------------------------------
736
737    #[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    // -----------------------------------------------------------------------
746    // AC26: side-effect import → is_side_effect = true
747    // -----------------------------------------------------------------------
748
749    #[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    // -----------------------------------------------------------------------
762    // AC27: dot import → is_namespace = true
763    // -----------------------------------------------------------------------
764
765    #[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    // -----------------------------------------------------------------------
778    // AC28: visibility rules (uppercase = Public, lowercase = Private)
779    // -----------------------------------------------------------------------
780
781    #[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    // -----------------------------------------------------------------------
806    // AC49: Invalid/empty source → no panic
807    // -----------------------------------------------------------------------
808
809    #[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    // -----------------------------------------------------------------------
819    // AC50: Source with errors → partial extraction
820    // -----------------------------------------------------------------------
821
822    #[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        // tree-sitter is fault-tolerant; at least some symbols extracted
827        assert!(
828            result.symbols.iter().any(|s| s.name == "Valid"),
829            "should extract 'Valid' even with syntax errors"
830        );
831    }
832
833    // -----------------------------------------------------------------------
834    // Type alias
835    // -----------------------------------------------------------------------
836
837    #[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    // -----------------------------------------------------------------------
845    // Const and Var declarations
846    // -----------------------------------------------------------------------
847
848    #[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    // -----------------------------------------------------------------------
865    // Test function detection
866    // -----------------------------------------------------------------------
867
868    #[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    // -----------------------------------------------------------------------
894    // Import: regular import, alias import, grouped imports
895    // -----------------------------------------------------------------------
896
897    #[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    // -----------------------------------------------------------------------
964    // Location
965    // -----------------------------------------------------------------------
966
967    #[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    // -----------------------------------------------------------------------
976    // Qualified name format
977    // -----------------------------------------------------------------------
978
979    #[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    // -----------------------------------------------------------------------
995    // Integration test
996    // -----------------------------------------------------------------------
997
998    #[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        // Symbols
1040        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        // Test detection
1074        let test_sym = result.symbols.iter().find(|s| s.name == "TestDog").unwrap();
1075        assert!(test_sym.is_test);
1076
1077        // Visibility
1078        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        // Edges
1092        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        // Imports
1117        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}