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