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