Skip to main content

gitcortex_indexer/parser/
go.rs

1use std::{
2    collections::HashMap,
3    path::{Path, PathBuf},
4};
5
6use gitcortex_core::{
7    error::{GitCortexError, Result},
8    graph::{Edge, Node, NodeId, NodeMetadata, Span},
9    schema::{EdgeConfidence, EdgeKind, NodeKind, Visibility},
10};
11use tree_sitter::{Node as TsNode, Parser};
12
13use super::{capture_definition, LanguageParser, ParseResult};
14
15pub struct GoParser {
16    language: tree_sitter::Language,
17}
18
19impl GoParser {
20    pub fn new() -> Self {
21        Self {
22            language: tree_sitter_go::LANGUAGE.into(),
23        }
24    }
25}
26
27impl Default for GoParser {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl LanguageParser for GoParser {
34    fn extensions(&self) -> &[&str] {
35        &["go"]
36    }
37
38    fn parse(&self, path: &Path, source: &str) -> Result<ParseResult> {
39        let mut parser = Parser::new();
40        parser
41            .set_language(&self.language)
42            .map_err(|e| GitCortexError::Parse {
43                file: path.to_owned(),
44                message: e.to_string(),
45            })?;
46
47        let tree = parser
48            .parse(source, None)
49            .ok_or_else(|| GitCortexError::Parse {
50                file: path.to_owned(),
51                message: "tree-sitter returned no parse tree".into(),
52            })?;
53
54        let mut visitor = FileVisitor::new(path, source, tree.root_node());
55        visitor.collect_names(tree.root_node());
56        visitor.visit_source_file(tree.root_node());
57        visitor.collect_imports(tree.root_node());
58        visitor.collect_interface_assertions(tree.root_node());
59
60        Ok(ParseResult {
61            nodes: visitor.nodes,
62            edges: visitor.edges,
63            deferred_calls: visitor.deferred_calls,
64            deferred_uses: visitor.deferred_uses,
65            deferred_implements: visitor.deferred_implements,
66            deferred_imports: visitor.deferred_imports,
67            deferred_inherits: visitor.deferred_inherits,
68            deferred_throws: Vec::new(),
69            deferred_annotated: Vec::new(),
70            deferred_doc_refs: Vec::new(),
71        })
72    }
73}
74
75// ── Internal visitor ──────────────────────────────────────────────────────────
76
77struct FileVisitor<'src> {
78    source: &'src [u8],
79    file: PathBuf,
80    /// NodeId of the package node (anchor for Imports edges).
81    package_id: NodeId,
82    nodes: Vec<Node>,
83    edges: Vec<Edge>,
84    /// type name → NodeId (struct/interface)
85    type_index: HashMap<String, NodeId>,
86    /// function/method name → NodeId
87    fn_index: HashMap<String, NodeId>,
88    deferred_calls: Vec<(NodeId, String, u32)>,
89    deferred_uses: Vec<(NodeId, String)>,
90    deferred_implements: Vec<(NodeId, String)>,
91    deferred_imports: Vec<(NodeId, String)>,
92    deferred_inherits: Vec<(NodeId, String)>,
93}
94
95impl<'src> FileVisitor<'src> {
96    fn new(file: &Path, source: &'src str, root: TsNode<'_>) -> Self {
97        let package_id = NodeId::new();
98        // Extract the package name from the source (first package_clause in the tree).
99        let package_name = {
100            let mut c = root.walk();
101            let pkg_clause: Vec<TsNode<'_>> = root.named_children(&mut c).collect();
102            let name = pkg_clause
103                .iter()
104                .find(|n| n.kind() == "package_clause")
105                .and_then(|pc| {
106                    let mut cc = pc.walk();
107                    let ids: Vec<TsNode<'_>> = pc.named_children(&mut cc).collect();
108                    ids.into_iter()
109                        .find(|n| n.kind() == "package_identifier")
110                        .map(|n| n.utf8_text(source.as_bytes()).unwrap_or("main").to_owned())
111                })
112                .unwrap_or_else(|| {
113                    file.file_stem()
114                        .and_then(|s| s.to_str())
115                        .unwrap_or("main")
116                        .to_owned()
117                });
118            name
119        };
120        let package_node = Node {
121            id: package_id.clone(),
122            qualified_name: package_name.clone(),
123            kind: NodeKind::Module,
124            name: package_name,
125            file: file.to_owned(),
126            span: Span {
127                start_line: 1,
128                end_line: 1,
129            },
130            metadata: NodeMetadata {
131                loc: source.lines().count() as u32,
132                visibility: Visibility::Pub,
133                is_async: false,
134                is_unsafe: false,
135                ..Default::default()
136            },
137        };
138        let nodes = vec![package_node];
139        Self {
140            source: source.as_bytes(),
141            file: file.to_owned(),
142            package_id,
143            nodes,
144            edges: Vec::new(),
145            type_index: HashMap::new(),
146            fn_index: HashMap::new(),
147            deferred_calls: Vec::new(),
148            deferred_uses: Vec::new(),
149            deferred_implements: Vec::new(),
150            deferred_imports: Vec::new(),
151            deferred_inherits: Vec::new(),
152        }
153    }
154
155    fn text<'t>(&self, node: TsNode<'t>) -> &'src str {
156        node.utf8_text(self.source).unwrap_or("")
157    }
158
159    fn span(node: TsNode<'_>) -> Span {
160        Span {
161            start_line: node.start_position().row as u32 + 1,
162            end_line: node.end_position().row as u32 + 1,
163        }
164    }
165
166    /// In Go, exported = first letter is uppercase.
167    fn visibility(name: &str) -> Visibility {
168        if name
169            .chars()
170            .next()
171            .map(|c| c.is_uppercase())
172            .unwrap_or(false)
173        {
174            Visibility::Pub
175        } else {
176            Visibility::Private
177        }
178    }
179
180    fn qualified(scope: &[String], name: &str) -> String {
181        if scope.is_empty() {
182            name.to_owned()
183        } else {
184            format!("{}.{name}", scope.join("."))
185        }
186    }
187
188    fn make_node(
189        &self,
190        id: NodeId,
191        kind: NodeKind,
192        name: String,
193        scope: &[String],
194        ts_node: TsNode<'_>,
195    ) -> Node {
196        Node {
197            id,
198            qualified_name: Self::qualified(scope, &name),
199            kind,
200            name: name.clone(),
201            file: self.file.clone(),
202            span: Self::span(ts_node),
203            metadata: NodeMetadata {
204                loc: (ts_node.end_position().row - ts_node.start_position().row + 1) as u32,
205                visibility: Self::visibility(&name),
206                is_async: false,
207                is_unsafe: false,
208                definition: capture_definition(self.source, ts_node),
209                ..Default::default()
210            },
211        }
212    }
213
214    // ── Pass 1: pre-allocate NodeIds ──────────────────────────────────────────
215
216    fn collect_names(&mut self, node: TsNode<'_>) {
217        let mut cursor = node.walk();
218        let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
219        for child in children {
220            match child.kind() {
221                "function_declaration" => {
222                    if let Some(name_node) = child.child_by_field_name("name") {
223                        let name = self.text(name_node).to_owned();
224                        self.fn_index.entry(name).or_default();
225                    }
226                }
227                "method_declaration" => {
228                    if let Some(name_node) = child.child_by_field_name("name") {
229                        let name = self.text(name_node).to_owned();
230                        self.fn_index.entry(name).or_default();
231                    }
232                }
233                "type_declaration" => {
234                    self.collect_type_decl_names(child);
235                }
236                _ => {}
237            }
238        }
239    }
240
241    fn collect_type_decl_names(&mut self, decl: TsNode<'_>) {
242        let mut cursor = decl.walk();
243        for spec in decl.named_children(&mut cursor) {
244            if spec.kind() != "type_spec" {
245                continue;
246            }
247            if let Some(name_node) = spec.child_by_field_name("name") {
248                let name = self.text(name_node).to_owned();
249                if let Some(type_node) = spec.child_by_field_name("type") {
250                    if matches!(type_node.kind(), "struct_type" | "interface_type") {
251                        self.type_index.entry(name).or_default();
252                    }
253                }
254            }
255        }
256    }
257
258    // ── Pass 2: emit nodes + edges ────────────────────────────────────────────
259
260    fn visit_source_file(&mut self, node: TsNode<'_>) {
261        let mut cursor = node.walk();
262        let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
263        for child in children {
264            self.visit_top_level(child);
265        }
266    }
267
268    fn visit_top_level(&mut self, node: TsNode<'_>) {
269        match node.kind() {
270            "function_declaration" => self.visit_function(node, &[]),
271            "method_declaration" => self.visit_method(node),
272            "type_declaration" => self.visit_type_decl(node),
273            "const_declaration" => self.visit_const_decl(node),
274            _ => {}
275        }
276    }
277
278    fn visit_function(&mut self, node: TsNode<'_>, scope: &[String]) {
279        let Some(name_node) = node.child_by_field_name("name") else {
280            return;
281        };
282        let name = self.text(name_node).to_owned();
283        let id = self
284            .fn_index
285            .get(&name)
286            .cloned()
287            .unwrap_or_else(NodeId::new);
288        let mut graph_node =
289            self.make_node(id.clone(), NodeKind::Function, name.clone(), scope, node);
290
291        // init and main are package-level entry points — mark them as static.
292        if name == "init" || name == "main" {
293            graph_node.metadata.is_static = true;
294        }
295
296        // Capture generic type parameter constraints (Go 1.18+).
297        // tree-sitter-go uses "type_parameters" for `func Foo[T any, U comparable]()`.
298        graph_node.metadata.generic_bounds = self.collect_generic_bounds(node);
299
300        if let Some(body) = node.child_by_field_name("body") {
301            graph_node.metadata.lld.complexity = Some(super::cyclomatic_complexity(
302                body,
303                &super::complexity::go_decision,
304            ));
305        }
306
307        self.nodes.push(graph_node);
308
309        self.extract_fn_type_uses(node, &id);
310
311        if let Some(body) = node.child_by_field_name("body") {
312            self.collect_calls(body, &id);
313        }
314    }
315
316    fn visit_method(&mut self, node: TsNode<'_>) {
317        let Some(name_node) = node.child_by_field_name("name") else {
318            return;
319        };
320        let name = self.text(name_node).to_owned();
321
322        let receiver_type = self.receiver_type(node);
323        let scope: Vec<String> = receiver_type.into_iter().collect();
324
325        let container_id = scope.first().and_then(|t| self.type_index.get(t).cloned());
326        let id = self
327            .fn_index
328            .get(&name)
329            .cloned()
330            .unwrap_or_else(NodeId::new);
331        let mut graph_node = self.make_node(id.clone(), NodeKind::Method, name, &scope, node);
332
333        if let Some(body) = node.child_by_field_name("body") {
334            graph_node.metadata.lld.complexity = Some(super::cyclomatic_complexity(
335                body,
336                &super::complexity::go_decision,
337            ));
338        }
339
340        if let Some(cid) = container_id {
341            self.edges.push(Edge {
342                src: cid,
343                dst: id.clone(),
344                kind: EdgeKind::Contains,
345                line: None,
346                confidence: EdgeConfidence::Extracted,
347            });
348        }
349        self.nodes.push(graph_node);
350
351        self.extract_fn_type_uses(node, &id);
352
353        if let Some(body) = node.child_by_field_name("body") {
354            self.collect_calls(body, &id);
355        }
356    }
357
358    /// Extract the receiver type name from `func (r *ReceiverType) MethodName()`.
359    fn receiver_type(&self, method_node: TsNode<'_>) -> Option<String> {
360        let recv = method_node.child_by_field_name("receiver")?;
361        let mut cursor = recv.walk();
362        for param in recv.named_children(&mut cursor) {
363            if param.kind() != "parameter_declaration" {
364                continue;
365            }
366            if let Some(type_node) = param.child_by_field_name("type") {
367                return match type_node.kind() {
368                    "type_identifier" => Some(self.text(type_node).to_owned()),
369                    "pointer_type" => {
370                        let mut c = type_node.walk();
371                        let result = type_node
372                            .named_children(&mut c)
373                            .find(|n| n.kind() == "type_identifier")
374                            .map(|n| self.text(n).to_owned());
375                        result
376                    }
377                    _ => None,
378                };
379            }
380        }
381        None
382    }
383
384    fn visit_type_decl(&mut self, decl: TsNode<'_>) {
385        let mut cursor = decl.walk();
386        let specs: Vec<TsNode<'_>> = decl.named_children(&mut cursor).collect();
387        for spec in specs {
388            if spec.kind() != "type_spec" {
389                continue;
390            }
391            let Some(name_node) = spec.child_by_field_name("name") else {
392                continue;
393            };
394            let name = self.text(name_node).to_owned();
395            let Some(type_node) = spec.child_by_field_name("type") else {
396                continue;
397            };
398
399            match type_node.kind() {
400                "struct_type" => {
401                    let id = self
402                        .type_index
403                        .get(&name)
404                        .cloned()
405                        .unwrap_or_else(NodeId::new);
406                    let mut graph_node =
407                        self.make_node(id.clone(), NodeKind::Struct, name, &[], spec);
408                    // Capture generic type parameter constraints (Go 1.18+).
409                    graph_node.metadata.generic_bounds = self.collect_generic_bounds(spec);
410                    self.nodes.push(graph_node);
411                    // Struct field types → Uses edges; embedded fields → Inherits
412                    self.extract_struct_field_uses(type_node, &id);
413                }
414                "interface_type" => {
415                    let id = self
416                        .type_index
417                        .get(&name)
418                        .cloned()
419                        .unwrap_or_else(NodeId::new);
420                    let mut graph_node =
421                        self.make_node(id.clone(), NodeKind::Interface, name, &[], spec);
422                    // Capture generic type parameter constraints (Go 1.18+).
423                    graph_node.metadata.generic_bounds = self.collect_generic_bounds(spec);
424                    self.nodes.push(graph_node);
425                    // Interface method signatures → Method nodes
426                    self.extract_interface_methods(type_node, &id);
427                }
428                _ => {
429                    let id = NodeId::new();
430                    let graph_node = self.make_node(id, NodeKind::TypeAlias, name, &[], spec);
431                    self.nodes.push(graph_node);
432                }
433            }
434        }
435    }
436
437    fn visit_const_decl(&mut self, node: TsNode<'_>) {
438        let mut cursor = node.walk();
439        for spec in node.named_children(&mut cursor) {
440            if spec.kind() != "const_spec" {
441                continue;
442            }
443            let Some(name_node) = spec.child_by_field_name("name") else {
444                continue;
445            };
446            let name = self.text(name_node).to_owned();
447            let id = NodeId::new();
448            let mut graph_node = self.make_node(id, NodeKind::Constant, name, &[], spec);
449            graph_node.metadata.is_const = true;
450            self.nodes.push(graph_node);
451        }
452    }
453
454    // ── Pass 3: collect import declarations ───────────────────────────────────
455
456    fn collect_imports(&mut self, node: TsNode<'_>) {
457        let mut cursor = node.walk();
458        let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
459        for child in children {
460            if child.kind() != "import_declaration" {
461                continue;
462            }
463            // import_declaration contains import_spec or import_spec_list
464            let mut c = child.walk();
465            let decl_children: Vec<TsNode<'_>> = child.named_children(&mut c).collect();
466            for dc in decl_children {
467                match dc.kind() {
468                    "import_spec" => self.record_import_spec(dc),
469                    "import_spec_list" => {
470                        let mut cc = dc.walk();
471                        let specs: Vec<TsNode<'_>> = dc.named_children(&mut cc).collect();
472                        for spec in specs {
473                            if spec.kind() == "import_spec" {
474                                self.record_import_spec(spec);
475                            }
476                        }
477                    }
478                    _ => {}
479                }
480            }
481        }
482    }
483
484    fn record_import_spec(&mut self, spec: TsNode<'_>) {
485        // If there's an explicit alias (name field), use it. Otherwise derive from path.
486        let alias = spec
487            .child_by_field_name("name")
488            .map(|n| self.text(n).to_owned());
489
490        // Skip blank imports (`import _ "pkg"`)
491        if alias.as_deref() == Some("_") {
492            return;
493        }
494
495        let pkg_name = if let Some(alias) = alias {
496            alias
497        } else if let Some(path_node) = spec.child_by_field_name("path") {
498            // Derive package name from the last path segment, stripping quotes.
499            let raw = self.text(path_node).trim_matches('"').trim_matches('\'');
500            raw.split('/').next_back().unwrap_or(raw).to_owned()
501        } else {
502            return;
503        };
504
505        self.deferred_imports
506            .push((self.package_id.clone(), pkg_name));
507    }
508
509    // ── Pass 4: detect explicit interface assertions ──────────────────────────
510
511    /// Detect `var _ MyInterface = (*MyStruct)(nil)` patterns.
512    fn collect_interface_assertions(&mut self, node: TsNode<'_>) {
513        let mut cursor = node.walk();
514        let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
515        for child in children {
516            if child.kind() != "var_declaration" {
517                continue;
518            }
519            let mut c = child.walk();
520            let specs: Vec<TsNode<'_>> = child.named_children(&mut c).collect();
521            for spec in specs {
522                if spec.kind() != "var_spec" {
523                    continue;
524                }
525                let mut cc = spec.walk();
526                let spec_children: Vec<TsNode<'_>> = spec.named_children(&mut cc).collect();
527                // Expect: identifier "_", type_identifier (interface), expression_list (value)
528                if spec_children.len() < 3 {
529                    continue;
530                }
531                if spec_children[0].kind() != "identifier" || self.text(spec_children[0]) != "_" {
532                    continue;
533                }
534                if spec_children[1].kind() != "type_identifier" {
535                    continue;
536                }
537                let interface_name = self.text(spec_children[1]).to_owned();
538                // Walk the value expression to find identifiers matching known types
539                let value = spec_children[2];
540                let mut candidates = Vec::new();
541                self.collect_candidate_type_names(value, &mut candidates);
542                for struct_name in candidates {
543                    if let Some(struct_id) = self.type_index.get(&struct_name).cloned() {
544                        self.deferred_implements
545                            .push((struct_id, interface_name.clone()));
546                    }
547                }
548            }
549        }
550    }
551
552    /// Recursively collect identifier/type_identifier names that could be type names.
553    fn collect_candidate_type_names(&self, node: TsNode<'_>, out: &mut Vec<String>) {
554        match node.kind() {
555            "identifier" | "type_identifier" => {
556                let name = self.text(node).to_owned();
557                if name != "nil" && !is_builtin_go_type(&name) {
558                    out.push(name);
559                }
560            }
561            _ => {
562                let mut c = node.walk();
563                for child in node.named_children(&mut c) {
564                    self.collect_candidate_type_names(child, out);
565                }
566            }
567        }
568    }
569
570    // ── Type extraction helpers ───────────────────────────────────────────────
571
572    /// Extract Uses edges from a function/method's parameter list and result type.
573    fn extract_fn_type_uses(&mut self, fn_node: TsNode<'_>, fn_id: &NodeId) {
574        // Parameters
575        if let Some(params) = fn_node.child_by_field_name("parameters") {
576            let mut c = params.walk();
577            let param_list: Vec<TsNode<'_>> = params.named_children(&mut c).collect();
578            for param in param_list {
579                if param.kind() == "parameter_declaration"
580                    || param.kind() == "variadic_parameter_declaration"
581                {
582                    if let Some(type_node) = param.child_by_field_name("type") {
583                        for name in self.collect_type_idents(type_node) {
584                            self.deferred_uses.push((fn_id.clone(), name));
585                        }
586                    }
587                }
588            }
589        }
590        // Result (return types)
591        if let Some(result) = fn_node.child_by_field_name("result") {
592            match result.kind() {
593                "parameter_list" => {
594                    let mut c = result.walk();
595                    let ret_params: Vec<TsNode<'_>> = result.named_children(&mut c).collect();
596                    for rp in ret_params {
597                        if rp.kind() == "parameter_declaration" {
598                            if let Some(type_node) = rp.child_by_field_name("type") {
599                                for name in self.collect_type_idents(type_node) {
600                                    self.deferred_uses.push((fn_id.clone(), name));
601                                }
602                            }
603                        }
604                    }
605                }
606                // Single return type (no parens)
607                _ => {
608                    for name in self.collect_type_idents(result) {
609                        self.deferred_uses.push((fn_id.clone(), name));
610                    }
611                }
612            }
613        }
614    }
615
616    /// Extract Uses edges from struct field types.
617    /// Embedded (anonymous) fields also produce Inherits edges.
618    fn extract_struct_field_uses(&mut self, struct_type: TsNode<'_>, struct_id: &NodeId) {
619        let mut tw = struct_type.walk();
620        let top: Vec<TsNode<'_>> = struct_type.named_children(&mut tw).collect();
621        let Some(field_list) = top
622            .iter()
623            .find(|n| n.kind() == "field_declaration_list")
624            .copied()
625        else {
626            return;
627        };
628        let mut c = field_list.walk();
629        let fields: Vec<TsNode<'_>> = field_list.named_children(&mut c).collect();
630        for field in fields {
631            if field.kind() == "field_declaration" {
632                // An embedded (anonymous) field has no "name" field in tree-sitter-go —
633                // only a "type" field. Detect this by checking that there are no named
634                // children with field-name "name".
635                let has_name = field.child_by_field_name("name").is_some();
636                if let Some(type_node) = field.child_by_field_name("type") {
637                    let type_names = self.collect_type_idents(type_node);
638                    for name in &type_names {
639                        self.deferred_uses.push((struct_id.clone(), name.clone()));
640                    }
641                    // Embedded field (no explicit name) → structural inheritance
642                    if !has_name {
643                        for name in type_names {
644                            self.deferred_inherits.push((struct_id.clone(), name));
645                        }
646                    }
647                }
648            }
649        }
650    }
651
652    /// Capture method signatures from an interface body as Method nodes.
653    fn extract_interface_methods(&mut self, interface_type: TsNode<'_>, iface_id: &NodeId) {
654        let mut c = interface_type.walk();
655        let children: Vec<TsNode<'_>> = interface_type.named_children(&mut c).collect();
656        for child in children {
657            // tree-sitter-go uses `method_elem` for interface method signatures
658            if child.kind() == "method_elem" {
659                let mut cc = child.walk();
660                let method_children: Vec<TsNode<'_>> = child.named_children(&mut cc).collect();
661                // Name is the first `field_identifier` child
662                let Some(name_node) = method_children
663                    .iter()
664                    .find(|n| n.kind() == "field_identifier")
665                else {
666                    continue;
667                };
668                let name = self.text(*name_node).to_owned();
669                let id = NodeId::new();
670                let graph_node = self.make_node(id.clone(), NodeKind::Method, name, &[], child);
671                self.edges.push(Edge {
672                    src: iface_id.clone(),
673                    dst: id.clone(),
674                    kind: EdgeKind::Contains,
675                    line: None,
676                    confidence: EdgeConfidence::Extracted,
677                });
678                self.nodes.push(graph_node);
679            }
680        }
681    }
682
683    /// Parse `type_parameters` of a generic function or type declaration (Go 1.18+).
684    ///
685    /// For `func Map[T any, U comparable]()` this returns `["T any", "U comparable"]`.
686    /// For `type Set[E comparable] struct {}` this returns `["E comparable"]`.
687    fn collect_generic_bounds(&self, node: TsNode<'_>) -> Vec<String> {
688        let Some(type_params) = node.child_by_field_name("type_parameters") else {
689            return Vec::new();
690        };
691        let mut bounds = Vec::new();
692        let mut cursor = type_params.walk();
693        for child in type_params.named_children(&mut cursor) {
694            // tree-sitter-go models each type parameter as a `type_parameter_declaration`
695            // with a "name" field (the type variable) and a "type" field (the constraint).
696            if child.kind() == "type_parameter_declaration" {
697                let name = child
698                    .child_by_field_name("name")
699                    .map(|n| self.text(n))
700                    .unwrap_or("");
701                let constraint = child
702                    .child_by_field_name("type")
703                    .map(|n| self.text(n))
704                    .unwrap_or("");
705                if !name.is_empty() {
706                    let bound = if constraint.is_empty() {
707                        name.to_owned()
708                    } else {
709                        format!("{name} {constraint}")
710                    };
711                    bounds.push(bound);
712                }
713            }
714        }
715        bounds
716    }
717
718    /// Walk a Go type expression and collect non-builtin type_identifier names.
719    fn collect_type_idents(&self, node: TsNode<'_>) -> Vec<String> {
720        let mut names = Vec::new();
721        self.walk_type_idents(node, &mut names);
722        names
723    }
724
725    fn walk_type_idents(&self, node: TsNode<'_>, out: &mut Vec<String>) {
726        match node.kind() {
727            "type_identifier" => {
728                let name = self.text(node).to_owned();
729                if !is_builtin_go_type(&name) {
730                    out.push(name);
731                }
732            }
733            _ => {
734                let mut c = node.walk();
735                for child in node.named_children(&mut c) {
736                    self.walk_type_idents(child, out);
737                }
738            }
739        }
740    }
741
742    // ── Call collection ───────────────────────────────────────────────────────
743
744    fn collect_calls(&mut self, node: TsNode<'_>, caller_id: &NodeId) {
745        let mut cursor = node.walk();
746        let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
747        for child in children {
748            if child.kind() == "call_expression" {
749                if let Some(callee) = self.callee_name(child) {
750                    let line = child.start_position().row as u32 + 1;
751                    self.record_call(caller_id.clone(), callee, line);
752                }
753                if let Some(args) = child.child_by_field_name("arguments") {
754                    self.collect_calls(args, caller_id);
755                }
756            } else if child.kind() == "go_statement" {
757                // `go fn()` — record the call and mark it as async via deferred_calls
758                if let Some(call) = child.named_child(0) {
759                    if call.kind() == "call_expression" {
760                        if let Some(callee) = self.callee_name(call) {
761                            // Record as a regular deferred call; the goroutine is conceptually async
762                            let line = call.start_position().row as u32 + 1;
763                            self.deferred_calls.push((caller_id.clone(), callee, line));
764                        }
765                    }
766                }
767            } else {
768                self.collect_calls(child, caller_id);
769            }
770        }
771    }
772
773    fn callee_name(&self, call_expr: TsNode<'_>) -> Option<String> {
774        let func = call_expr.child_by_field_name("function")?;
775        match func.kind() {
776            "identifier" => Some(self.text(func).to_owned()),
777            "selector_expression" => func
778                .child_by_field_name("field")
779                .map(|n| self.text(n).to_owned()),
780            _ => None,
781        }
782    }
783
784    fn record_call(&mut self, caller_id: NodeId, callee_name: String, line: u32) {
785        if callee_name.is_empty() {
786            return;
787        }
788        if let Some(callee_id) = self.fn_index.get(&callee_name).cloned() {
789            let edge = Edge::call(caller_id, callee_id, line);
790            if !self.edges.contains(&edge) {
791                self.edges.push(edge);
792            }
793        } else if !self
794            .deferred_calls
795            .iter()
796            .any(|(c, n, _)| c == &caller_id && n == &callee_name)
797        {
798            self.deferred_calls.push((caller_id, callee_name, line));
799        }
800    }
801}
802
803/// Returns true for Go built-in types that don't correspond to user-defined symbols.
804fn is_builtin_go_type(name: &str) -> bool {
805    matches!(
806        name,
807        "bool"
808            | "byte"
809            | "complex64"
810            | "complex128"
811            | "error"
812            | "float32"
813            | "float64"
814            | "int"
815            | "int8"
816            | "int16"
817            | "int32"
818            | "int64"
819            | "rune"
820            | "string"
821            | "uint"
822            | "uint8"
823            | "uint16"
824            | "uint32"
825            | "uint64"
826            | "uintptr"
827            | "any"
828            | "comparable"
829    )
830}
831
832// ── Tests ─────────────────────────────────────────────────────────────────────
833
834#[cfg(test)]
835mod tests {
836    use super::GoParser;
837    use crate::parser::LanguageParser;
838    use gitcortex_core::schema::{EdgeKind, NodeKind};
839    use std::path::Path;
840
841    fn parse(
842        src: &str,
843    ) -> (
844        Vec<gitcortex_core::graph::Node>,
845        Vec<gitcortex_core::graph::Edge>,
846    ) {
847        let r = GoParser::new().parse(Path::new("test.go"), src).unwrap();
848        (r.nodes, r.edges)
849    }
850
851    #[allow(clippy::type_complexity)]
852    fn parse_full(
853        src: &str,
854    ) -> (
855        Vec<gitcortex_core::graph::Node>,
856        Vec<gitcortex_core::graph::Edge>,
857        Vec<(gitcortex_core::graph::NodeId, String, u32)>,
858        Vec<(gitcortex_core::graph::NodeId, String)>,
859        Vec<(gitcortex_core::graph::NodeId, String)>,
860        Vec<(gitcortex_core::graph::NodeId, String)>,
861    ) {
862        let r = GoParser::new().parse(Path::new("test.go"), src).unwrap();
863        (
864            r.nodes,
865            r.edges,
866            r.deferred_calls,
867            r.deferred_uses,
868            r.deferred_implements,
869            r.deferred_imports,
870        )
871    }
872
873    #[test]
874    fn parses_function() {
875        let src = "package main\nfunc Greet(name string) string { return name }";
876        let (nodes, _) = parse(src);
877        let fns: Vec<_> = nodes
878            .iter()
879            .filter(|n| n.kind == NodeKind::Function)
880            .collect();
881        assert_eq!(fns.len(), 1);
882        assert_eq!(fns[0].name, "Greet");
883    }
884
885    #[test]
886    fn parses_struct_and_method() {
887        let src = "package main\ntype Person struct { Name string }\nfunc (p *Person) Greet() string { return p.Name }";
888        let (nodes, edges) = parse(src);
889        let structs: Vec<_> = nodes
890            .iter()
891            .filter(|n| n.kind == NodeKind::Struct)
892            .collect();
893        let methods: Vec<_> = nodes
894            .iter()
895            .filter(|n| n.kind == NodeKind::Method)
896            .collect();
897        assert_eq!(structs.len(), 1);
898        assert_eq!(methods.len(), 1);
899        let contains: Vec<_> = edges
900            .iter()
901            .filter(|e| e.kind == EdgeKind::Contains)
902            .collect();
903        assert!(!contains.is_empty());
904    }
905
906    #[test]
907    fn parses_interface() {
908        let src = "package main\ntype Greeter interface { Greet() string }";
909        let (nodes, _) = parse(src);
910        let ifaces: Vec<_> = nodes
911            .iter()
912            .filter(|n| n.kind == NodeKind::Interface)
913            .collect();
914        assert_eq!(ifaces.len(), 1);
915        assert_eq!(ifaces[0].name, "Greeter");
916    }
917
918    #[test]
919    fn go_visibility_is_uppercase() {
920        let src = "package main\nfunc Exported() {}\nfunc unexported() {}";
921        let (nodes, _) = parse(src);
922        use gitcortex_core::schema::Visibility;
923        let exp = nodes.iter().find(|n| n.name == "Exported").unwrap();
924        let unexp = nodes.iter().find(|n| n.name == "unexported").unwrap();
925        assert_eq!(exp.metadata.visibility, Visibility::Pub);
926        assert_eq!(unexp.metadata.visibility, Visibility::Private);
927    }
928
929    #[test]
930    fn detects_call_edges() {
931        let src = "package main\nfunc Caller() { Callee() }\nfunc Callee() {}";
932        let (_, edges) = parse(src);
933        let calls: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Calls).collect();
934        assert_eq!(calls.len(), 1);
935    }
936
937    #[test]
938    fn package_node_is_emitted() {
939        let src = "package mypackage\nfunc Foo() {}";
940        let (nodes, _) = parse(src);
941        let modules: Vec<_> = nodes
942            .iter()
943            .filter(|n| n.kind == NodeKind::Module)
944            .collect();
945        assert_eq!(modules.len(), 1);
946        assert_eq!(modules[0].name, "mypackage");
947    }
948
949    #[test]
950    fn detects_import_declaration() {
951        let src = "package main\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n)\nfunc main() {}";
952        let (_, _, _, _, _, imports) = parse_full(src);
953        assert!(
954            imports.iter().any(|(_, n)| n == "fmt"),
955            "expected import 'fmt', got: {imports:?}"
956        );
957        assert!(
958            imports.iter().any(|(_, n)| n == "exec"),
959            "expected import 'exec' (last segment of os/exec), got: {imports:?}"
960        );
961    }
962
963    #[test]
964    fn detects_fn_type_uses() {
965        let src = "package main\ntype Request struct{}\ntype Response struct{}\nfunc Handle(req *Request) *Response { return nil }";
966        let (_, _, _, uses, _, _) = parse_full(src);
967        assert!(
968            uses.iter().any(|(_, n)| n == "Request"),
969            "expected Uses edge to Request, got: {uses:?}"
970        );
971        assert!(
972            uses.iter().any(|(_, n)| n == "Response"),
973            "expected Uses edge to Response, got: {uses:?}"
974        );
975    }
976
977    #[test]
978    fn detects_interface_assertion() {
979        let src = "package main\ntype Greeter interface { Greet() string }\ntype Person struct{}\nvar _ Greeter = (*Person)(nil)";
980        let (_, _, _, _, implements, _) = parse_full(src);
981        assert!(
982            implements.iter().any(|(_, n)| n == "Greeter"),
983            "expected Implements edge to Greeter, got: {implements:?}"
984        );
985    }
986
987    #[test]
988    fn captures_interface_methods() {
989        let src = "package main\ntype Greeter interface { Greet() string\nGetName() string }";
990        let (nodes, edges) = parse(src);
991        let methods: Vec<_> = nodes
992            .iter()
993            .filter(|n| n.kind == NodeKind::Method)
994            .collect();
995        assert_eq!(methods.len(), 2, "expected 2 interface method specs");
996        let contains: Vec<_> = edges
997            .iter()
998            .filter(|e| e.kind == EdgeKind::Contains)
999            .collect();
1000        assert_eq!(
1001            contains.len(),
1002            2,
1003            "expected 2 Contains edges from interface to methods"
1004        );
1005    }
1006}