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 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 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 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 extract_imports(&mut edges, root, source_bytes, &fp, file);
99
100 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 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 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 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 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(); 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 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}