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