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