Skip to main content

cgx_engine/parsers/
go.rs

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