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);
55        visitor.collect_names(tree.root_node());
56        visitor.visit_source_file(tree.root_node());
57
58        Ok(ParseResult {
59            nodes: visitor.nodes,
60            edges: visitor.edges,
61            deferred_calls: visitor.deferred_calls,
62            deferred_uses: Vec::new(),
63            deferred_implements: Vec::new(),
64            deferred_imports: Vec::new(),
65        })
66    }
67}
68
69// ── Internal visitor ──────────────────────────────────────────────────────────
70
71struct FileVisitor<'src> {
72    source: &'src [u8],
73    file: PathBuf,
74    nodes: Vec<Node>,
75    edges: Vec<Edge>,
76    /// type name → NodeId (struct/interface)
77    type_index: HashMap<String, NodeId>,
78    /// function/method name → NodeId
79    fn_index: HashMap<String, NodeId>,
80    deferred_calls: Vec<(NodeId, String)>,
81}
82
83impl<'src> FileVisitor<'src> {
84    fn new(file: &Path, source: &'src str) -> Self {
85        Self {
86            source: source.as_bytes(),
87            file: file.to_owned(),
88            nodes: Vec::new(),
89            edges: Vec::new(),
90            type_index: HashMap::new(),
91            fn_index: HashMap::new(),
92            deferred_calls: Vec::new(),
93        }
94    }
95
96    fn text<'t>(&self, node: TsNode<'t>) -> &'src str {
97        node.utf8_text(self.source).unwrap_or("")
98    }
99
100    fn span(node: TsNode<'_>) -> Span {
101        Span {
102            start_line: node.start_position().row as u32 + 1,
103            end_line: node.end_position().row as u32 + 1,
104        }
105    }
106
107    /// In Go, exported = first letter is uppercase.
108    fn visibility(name: &str) -> Visibility {
109        if name
110            .chars()
111            .next()
112            .map(|c| c.is_uppercase())
113            .unwrap_or(false)
114        {
115            Visibility::Pub
116        } else {
117            Visibility::Private
118        }
119    }
120
121    fn qualified(scope: &[String], name: &str) -> String {
122        if scope.is_empty() {
123            name.to_owned()
124        } else {
125            format!("{}.{name}", scope.join("."))
126        }
127    }
128
129    fn make_node(
130        &self,
131        id: NodeId,
132        kind: NodeKind,
133        name: String,
134        scope: &[String],
135        ts_node: TsNode<'_>,
136    ) -> Node {
137        Node {
138            id,
139            qualified_name: Self::qualified(scope, &name),
140            kind,
141            name: name.clone(),
142            file: self.file.clone(),
143            span: Self::span(ts_node),
144            metadata: NodeMetadata {
145                loc: (ts_node.end_position().row - ts_node.start_position().row + 1) as u32,
146                visibility: Self::visibility(&name),
147                is_async: false, // Go does not have async/await syntax
148                is_unsafe: false,
149                ..Default::default()
150            },
151        }
152    }
153
154    // ── Pass 1 ────────────────────────────────────────────────────────────────
155
156    fn collect_names(&mut self, node: TsNode<'_>) {
157        let mut cursor = node.walk();
158        let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
159        for child in children {
160            match child.kind() {
161                "function_declaration" => {
162                    if let Some(name_node) = child.child_by_field_name("name") {
163                        let name = self.text(name_node).to_owned();
164                        self.fn_index.entry(name).or_default();
165                    }
166                }
167                "method_declaration" => {
168                    if let Some(name_node) = child.child_by_field_name("name") {
169                        let name = self.text(name_node).to_owned();
170                        self.fn_index.entry(name).or_default();
171                    }
172                }
173                "type_declaration" => {
174                    self.collect_type_names(child);
175                }
176                _ => {}
177            }
178        }
179    }
180
181    fn collect_type_names(&mut self, decl: TsNode<'_>) {
182        let mut cursor = decl.walk();
183        for spec in decl.named_children(&mut cursor) {
184            if spec.kind() != "type_spec" {
185                continue;
186            }
187            if let Some(name_node) = spec.child_by_field_name("name") {
188                let name = self.text(name_node).to_owned();
189                if let Some(type_node) = spec.child_by_field_name("type") {
190                    if matches!(type_node.kind(), "struct_type" | "interface_type") {
191                        self.type_index.entry(name).or_default();
192                    }
193                }
194            }
195        }
196    }
197
198    // ── Pass 2 ────────────────────────────────────────────────────────────────
199
200    fn visit_source_file(&mut self, node: TsNode<'_>) {
201        let mut cursor = node.walk();
202        let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
203        for child in children {
204            self.visit_top_level(child);
205        }
206    }
207
208    fn visit_top_level(&mut self, node: TsNode<'_>) {
209        match node.kind() {
210            "function_declaration" => self.visit_function(node, &[]),
211            "method_declaration" => self.visit_method(node),
212            "type_declaration" => self.visit_type_decl(node),
213            "const_declaration" => self.visit_const_decl(node),
214            _ => {}
215        }
216    }
217
218    fn visit_function(&mut self, node: TsNode<'_>, scope: &[String]) {
219        let Some(name_node) = node.child_by_field_name("name") else {
220            return;
221        };
222        let name = self.text(name_node).to_owned();
223        let id = self
224            .fn_index
225            .get(&name)
226            .cloned()
227            .unwrap_or_else(NodeId::new);
228        let graph_node = self.make_node(id.clone(), NodeKind::Function, name, scope, node);
229        self.nodes.push(graph_node);
230
231        if let Some(body) = node.child_by_field_name("body") {
232            self.collect_calls(body, &id);
233        }
234    }
235
236    fn visit_method(&mut self, node: TsNode<'_>) {
237        let Some(name_node) = node.child_by_field_name("name") else {
238            return;
239        };
240        let name = self.text(name_node).to_owned();
241
242        // Determine the receiver type name for the scope.
243        let receiver_type = self.receiver_type(node);
244        let scope: Vec<String> = receiver_type.into_iter().collect();
245
246        let container_id = scope.first().and_then(|t| self.type_index.get(t).cloned());
247        let id = self
248            .fn_index
249            .get(&name)
250            .cloned()
251            .unwrap_or_else(NodeId::new);
252        let graph_node = self.make_node(id.clone(), NodeKind::Method, name, &scope, node);
253
254        if let Some(cid) = container_id {
255            self.edges.push(Edge {
256                src: cid,
257                dst: id.clone(),
258                kind: EdgeKind::Contains,
259            });
260        }
261        self.nodes.push(graph_node);
262
263        if let Some(body) = node.child_by_field_name("body") {
264            self.collect_calls(body, &id);
265        }
266    }
267
268    /// Extract the receiver type name from `func (r *ReceiverType) MethodName()`.
269    fn receiver_type(&self, method_node: TsNode<'_>) -> Option<String> {
270        let recv = method_node.child_by_field_name("receiver")?;
271        // The receiver is a parameter_list containing a parameter_declaration.
272        let mut cursor = recv.walk();
273        for param in recv.named_children(&mut cursor) {
274            if param.kind() != "parameter_declaration" {
275                continue;
276            }
277            if let Some(type_node) = param.child_by_field_name("type") {
278                return match type_node.kind() {
279                    "type_identifier" => Some(self.text(type_node).to_owned()),
280                    "pointer_type" => {
281                        // *Type → dereference to get the type identifier
282                        let mut c = type_node.walk();
283                        let result = type_node
284                            .named_children(&mut c)
285                            .find(|n| n.kind() == "type_identifier")
286                            .map(|n| self.text(n).to_owned());
287                        result
288                    }
289                    _ => None,
290                };
291            }
292        }
293        None
294    }
295
296    fn visit_type_decl(&mut self, decl: TsNode<'_>) {
297        let mut cursor = decl.walk();
298        let specs: Vec<TsNode<'_>> = decl.named_children(&mut cursor).collect();
299        for spec in specs {
300            if spec.kind() != "type_spec" {
301                continue;
302            }
303            let Some(name_node) = spec.child_by_field_name("name") else {
304                continue;
305            };
306            let name = self.text(name_node).to_owned();
307            let Some(type_node) = spec.child_by_field_name("type") else {
308                continue;
309            };
310
311            let kind = match type_node.kind() {
312                "struct_type" => NodeKind::Struct,
313                "interface_type" => NodeKind::Trait,
314                _ => {
315                    // Simple type alias.
316                    let id = NodeId::new();
317                    let graph_node = self.make_node(id, NodeKind::TypeAlias, name, &[], spec);
318                    self.nodes.push(graph_node);
319                    continue;
320                }
321            };
322
323            let id = self
324                .type_index
325                .get(&name)
326                .cloned()
327                .unwrap_or_else(NodeId::new);
328            let graph_node = self.make_node(id, kind, name, &[], spec);
329            self.nodes.push(graph_node);
330        }
331    }
332
333    fn visit_const_decl(&mut self, node: TsNode<'_>) {
334        let mut cursor = node.walk();
335        for spec in node.named_children(&mut cursor) {
336            if spec.kind() != "const_spec" {
337                continue;
338            }
339            let Some(name_node) = spec.child_by_field_name("name") else {
340                continue;
341            };
342            let name = self.text(name_node).to_owned();
343            let id = NodeId::new();
344            let graph_node = self.make_node(id, NodeKind::Constant, name, &[], spec);
345            self.nodes.push(graph_node);
346        }
347    }
348
349    fn collect_calls(&mut self, node: TsNode<'_>, caller_id: &NodeId) {
350        let mut cursor = node.walk();
351        let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
352        for child in children {
353            if child.kind() == "call_expression" {
354                if let Some(callee) = self.callee_name(child) {
355                    self.record_call(caller_id.clone(), callee);
356                }
357                if let Some(args) = child.child_by_field_name("arguments") {
358                    self.collect_calls(args, caller_id);
359                }
360            } else {
361                self.collect_calls(child, caller_id);
362            }
363        }
364    }
365
366    fn callee_name(&self, call_expr: TsNode<'_>) -> Option<String> {
367        let func = call_expr.child_by_field_name("function")?;
368        match func.kind() {
369            "identifier" => Some(self.text(func).to_owned()),
370            "selector_expression" => func
371                .child_by_field_name("field")
372                .map(|n| self.text(n).to_owned()),
373            _ => None,
374        }
375    }
376
377    fn record_call(&mut self, caller_id: NodeId, callee_name: String) {
378        if callee_name.is_empty() {
379            return;
380        }
381        if let Some(callee_id) = self.fn_index.get(&callee_name).cloned() {
382            let edge = Edge {
383                src: caller_id,
384                dst: callee_id,
385                kind: EdgeKind::Calls,
386            };
387            if !self.edges.contains(&edge) {
388                self.edges.push(edge);
389            }
390        } else if !self
391            .deferred_calls
392            .iter()
393            .any(|(c, n)| c == &caller_id && n == &callee_name)
394        {
395            self.deferred_calls.push((caller_id, callee_name));
396        }
397    }
398}
399
400// ── Tests ─────────────────────────────────────────────────────────────────────
401
402#[cfg(test)]
403mod tests {
404    use super::GoParser;
405    use crate::parser::LanguageParser;
406    use gitcortex_core::schema::{EdgeKind, NodeKind};
407    use std::path::Path;
408
409    fn parse(
410        src: &str,
411    ) -> (
412        Vec<gitcortex_core::graph::Node>,
413        Vec<gitcortex_core::graph::Edge>,
414    ) {
415        let r = GoParser::new().parse(Path::new("test.go"), src).unwrap();
416        (r.nodes, r.edges)
417    }
418
419    #[test]
420    fn parses_function() {
421        let src = "package main\nfunc Greet(name string) string { return name }";
422        let (nodes, _) = parse(src);
423        let fns: Vec<_> = nodes
424            .iter()
425            .filter(|n| n.kind == NodeKind::Function)
426            .collect();
427        assert_eq!(fns.len(), 1);
428        assert_eq!(fns[0].name, "Greet");
429    }
430
431    #[test]
432    fn parses_struct_and_method() {
433        let src = "package main\ntype Person struct { Name string }\nfunc (p *Person) Greet() string { return p.Name }";
434        let (nodes, edges) = parse(src);
435        let structs: Vec<_> = nodes
436            .iter()
437            .filter(|n| n.kind == NodeKind::Struct)
438            .collect();
439        let methods: Vec<_> = nodes
440            .iter()
441            .filter(|n| n.kind == NodeKind::Method)
442            .collect();
443        assert_eq!(structs.len(), 1);
444        assert_eq!(methods.len(), 1);
445        let contains: Vec<_> = edges
446            .iter()
447            .filter(|e| e.kind == EdgeKind::Contains)
448            .collect();
449        assert!(!contains.is_empty());
450    }
451
452    #[test]
453    fn parses_interface() {
454        let src = "package main\ntype Greeter interface { Greet() string }";
455        let (nodes, _) = parse(src);
456        let traits: Vec<_> = nodes.iter().filter(|n| n.kind == NodeKind::Trait).collect();
457        assert_eq!(traits.len(), 1);
458        assert_eq!(traits[0].name, "Greeter");
459    }
460
461    #[test]
462    fn go_visibility_is_uppercase() {
463        let src = "package main\nfunc Exported() {}\nfunc unexported() {}";
464        let (nodes, _) = parse(src);
465        use gitcortex_core::schema::Visibility;
466        let exp = nodes.iter().find(|n| n.name == "Exported").unwrap();
467        let unexp = nodes.iter().find(|n| n.name == "unexported").unwrap();
468        assert_eq!(exp.metadata.visibility, Visibility::Pub);
469        assert_eq!(unexp.metadata.visibility, Visibility::Private);
470    }
471
472    #[test]
473    fn detects_call_edges() {
474        let src = "package main\nfunc Caller() { Callee() }\nfunc Callee() {}";
475        let (_, edges) = parse(src);
476        let calls: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Calls).collect();
477        assert_eq!(calls.len(), 1);
478    }
479}