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 { 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 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 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 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 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(); 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 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}