Skip to main content

cgx_engine/parsers/
go.rs

1use tree_sitter::{Parser, Query, QueryCursor, Node};
2
3use crate::parser::{EdgeDef, EdgeKind, LanguageParser, NodeDef, NodeKind, ParseResult};
4use crate::walker::SourceFile;
5
6pub struct GoParser {
7    language: tree_sitter::Language,
8}
9
10impl GoParser {
11    pub fn new() -> Self {
12        Self {
13            language: tree_sitter_go::language(),
14        }
15    }
16}
17
18impl Default for GoParser {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl LanguageParser for GoParser {
25    fn extensions(&self) -> &[&str] {
26        &["go"]
27    }
28
29    fn extract(&self, file: &SourceFile) -> anyhow::Result<ParseResult> {
30        let mut parser = Parser::new();
31        parser.set_language(&self.language)?;
32
33        let tree = parser.parse(&file.content, None).ok_or_else(|| {
34            anyhow::anyhow!("failed to parse {}", file.relative_path)
35        })?;
36
37        let source_bytes = file.content.as_bytes();
38        let root = tree.root_node();
39        let mut nodes = Vec::new();
40        let mut edges = Vec::new();
41
42        let fp = file_node_id(&file.relative_path);
43
44        // Parse function declarations: func name(...) { ... }
45        if let Ok(query) =
46            Query::new(&self.language, "(function_declaration name: (identifier) @name) @fn")
47        {
48            extract_nodes(
49                &mut nodes,
50                &mut edges,
51                file,
52                &query,
53                root,
54                source_bytes,
55                NodeKind::Function,
56                "fn",
57                &fp,
58            );
59        }
60
61        // Parse method declarations: func (r *Receiver) Name(...) { ... }
62        if let Ok(query) = Query::new(
63            &self.language,
64            "(method_declaration name: (field_identifier) @name) @fn",
65        ) {
66            extract_nodes(
67                &mut nodes,
68                &mut edges,
69                file,
70                &query,
71                root,
72                source_bytes,
73                NodeKind::Function,
74                "fn",
75                &fp,
76            );
77        }
78
79        // Parse type declarations (struct / interface) as Class nodes
80        if let Ok(query) = Query::new(
81            &self.language,
82            "(type_declaration (type_spec name: (type_identifier) @name)) @cls",
83        ) {
84            extract_nodes(
85                &mut nodes,
86                &mut edges,
87                file,
88                &query,
89                root,
90                source_bytes,
91                NodeKind::Class,
92                "cls",
93                &fp,
94            );
95        }
96
97        // Parse imports
98        extract_imports(&mut edges, root, source_bytes, &fp, file);
99
100        // Extract calls
101        extract_calls(&mut edges, root, source_bytes, file);
102
103        Ok(ParseResult { nodes, edges })
104    }
105}
106
107fn file_node_id(rel_path: &str) -> String {
108    format!("file:{}", rel_path)
109}
110
111#[allow(clippy::too_many_arguments)]
112fn extract_nodes(
113    nodes: &mut Vec<NodeDef>,
114    edges: &mut Vec<EdgeDef>,
115    file: &SourceFile,
116    query: &Query,
117    root: tree_sitter::Node,
118    source_bytes: &[u8],
119    kind: NodeKind,
120    prefix: &str,
121    file_id: &str,
122) {
123    let mut cursor = QueryCursor::new();
124    for m in cursor.matches(query, root, source_bytes) {
125        let Some(name_capture) = m
126            .captures
127            .iter()
128            .find(|c| query.capture_names()[c.index as usize] == "name")
129        else {
130            continue;
131        };
132
133        let name = node_text(name_capture.node, source_bytes);
134        let node_start = name_capture.node.start_position();
135
136        let body_end = m
137            .captures
138            .iter()
139            .find(|c| {
140                let cap_name = &query.capture_names()[c.index as usize];
141                *cap_name == "fn" || *cap_name == "cls"
142            })
143            .map(|c| c.node.end_position())
144            .unwrap_or_else(|| name_capture.node.end_position());
145
146        let id = format!("{}:{}:{}", prefix, file.relative_path, name);
147
148        nodes.push(NodeDef {
149            id: id.clone(),
150            kind: kind.clone(),
151            name: name.clone(),
152            path: file.relative_path.clone(),
153            line_start: node_start.row as u32 + 1,
154            line_end: body_end.row as u32 + 1,
155            ..Default::default()
156        });
157
158        edges.push(EdgeDef {
159            src: file_id.to_string(),
160            dst: id,
161            kind: EdgeKind::Exports,
162            ..Default::default()
163        });
164    }
165}
166
167fn node_text(node: tree_sitter::Node, source: &[u8]) -> String {
168    node.utf8_text(source).unwrap_or("").to_string()
169}
170
171fn extract_imports(
172    edges: &mut Vec<EdgeDef>,
173    root: tree_sitter::Node,
174    source_bytes: &[u8],
175    file_id: &str,
176    file: &SourceFile,
177) {
178    let mut cursor = root.walk();
179    traverse_imports(edges, root, source_bytes, file_id, file, &mut cursor);
180}
181
182fn traverse_imports(
183    edges: &mut Vec<EdgeDef>,
184    node: tree_sitter::Node,
185    source_bytes: &[u8],
186    file_id: &str,
187    file: &SourceFile,
188    cursor: &mut tree_sitter::TreeCursor,
189) {
190    if node.kind() == "import_declaration" {
191        // Go imports: import "path" or import ( "path1" "path2" )
192        for j in 0..node.child_count() {
193            let Some(import_child) = node.child(j) else { continue };
194            if import_child.kind() == "import_spec" {
195                // import_spec has a path child
196                for k in 0..import_child.child_count() {
197                    let Some(spec_child) = import_child.child(k) else { continue };
198                    if spec_child.kind() == "interpreted_string_literal" || spec_child.kind() == "raw_string_literal" {
199                        let import_path = unquote_str(&source_bytes[spec_child.byte_range()]);
200                        // Only resolve relative imports (same module)
201                        // Go module imports are usually remote; we skip them for local graph
202                        if import_path.starts_with('.') {
203                            let resolved = resolve_import_path(&file.relative_path, &import_path);
204                            if !resolved.is_empty() {
205                                edges.push(EdgeDef {
206                                    src: file_id.to_string(),
207                                    dst: file_node_id(&resolved),
208                                    kind: EdgeKind::Imports,
209                                    ..Default::default()
210                                });
211                            }
212                        }
213                    }
214                }
215            } else if import_child.kind() == "interpreted_string_literal" || import_child.kind() == "raw_string_literal" {
216                // Single import: import "path"
217                let import_path = unquote_str(&source_bytes[import_child.byte_range()]);
218                if import_path.starts_with('.') {
219                    let resolved = resolve_import_path(&file.relative_path, &import_path);
220                    if !resolved.is_empty() {
221                        edges.push(EdgeDef {
222                            src: file_id.to_string(),
223                            dst: file_node_id(&resolved),
224                            kind: EdgeKind::Imports,
225                            ..Default::default()
226                        });
227                    }
228                }
229            }
230        }
231    }
232
233    if cursor.goto_first_child() {
234        loop {
235            let child = cursor.node();
236            traverse_imports(edges, child, source_bytes, file_id, file, cursor);
237            if !cursor.goto_next_sibling() {
238                break;
239            }
240        }
241        cursor.goto_parent();
242    }
243}
244
245fn unquote_str(s: &[u8]) -> String {
246    let s = std::str::from_utf8(s).unwrap_or("");
247    s.trim().trim_matches('\'').trim_matches('"').trim_matches('`').to_string()
248}
249
250fn resolve_import_path(current: &str, import: &str) -> String {
251    let mut parts: Vec<&str> = current.split('/').collect();
252    parts.pop(); // remove filename
253
254    for segment in import.split('/') {
255        match segment {
256            "." => {}
257            ".." => {
258                parts.pop();
259            }
260            _ => parts.push(segment),
261        }
262    }
263
264    parts.join("/")
265}
266
267fn extract_calls(edges: &mut Vec<EdgeDef>, root: Node, source: &[u8], file: &SourceFile) {
268    let mut fn_stack: Vec<String> = Vec::new();
269    walk_for_calls(edges, root, source, file, &mut fn_stack);
270}
271
272fn is_fn_node(kind: &str) -> bool {
273    matches!(kind, "function_declaration" | "method_declaration" | "func_literal")
274}
275
276fn fn_name_from_node(node: Node, source: &[u8], file: &SourceFile) -> Option<String> {
277    // function_declaration has `name` field (identifier)
278    // method_declaration has `name` field (field_identifier)
279    if let Some(name_node) = node.child_by_field_name("name") {
280        let name = name_node.utf8_text(source).unwrap_or("").to_string();
281        if !name.is_empty() {
282            return Some(format!("fn:{}:{}", file.relative_path, name));
283        }
284    }
285    None
286}
287
288fn walk_for_calls(
289    edges: &mut Vec<EdgeDef>,
290    node: Node,
291    source: &[u8],
292    file: &SourceFile,
293    fn_stack: &mut Vec<String>,
294) {
295    let kind = node.kind();
296    let pushed = is_fn_node(kind);
297
298    if pushed {
299        if let Some(id) = fn_name_from_node(node, source, file) {
300            fn_stack.push(id);
301        } else {
302            fn_stack.push(String::new());
303        }
304    }
305
306    if kind == "call_expression" {
307        if let Some(caller_id) = fn_stack.last().filter(|s| !s.is_empty()) {
308            let callee_name = node
309                .child_by_field_name("function")
310                .and_then(|func| match func.kind() {
311                    "identifier" => Some(func.utf8_text(source).unwrap_or("").to_string()),
312                    "selector_expression" => func
313                        .child_by_field_name("field")
314                        .map(|p| p.utf8_text(source).unwrap_or("").to_string()),
315                    _ => None,
316                })
317                .unwrap_or_default();
318
319            if !callee_name.is_empty() {
320                edges.push(EdgeDef {
321                    src: caller_id.clone(),
322                    dst: callee_name,
323                    kind: EdgeKind::Calls,
324                    confidence: 0.7,
325                    ..Default::default()
326                });
327            }
328        }
329    }
330
331    let mut cursor = node.walk();
332    if cursor.goto_first_child() {
333        loop {
334            walk_for_calls(edges, cursor.node(), source, file, fn_stack);
335            if !cursor.goto_next_sibling() {
336                break;
337            }
338        }
339    }
340
341    if pushed {
342        fn_stack.pop();
343    }
344}