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