1use super::{add_contains_edge, add_node_if_missing, make_file_node, run_query_named};
2use crate::error::Result;
3use crate::extract::{make_id, ImportNode, LanguageExtractor};
4use crate::types::{Edge, ExtractionFragment, Node};
5use std::collections::HashMap;
6use std::path::Path;
7use tree_sitter::{Node as TsNode, Parser};
8
9pub struct TsJuliaExtractor;
10
11fn jl_text(source: &[u8], node: &TsNode<'_>) -> String {
12 std::str::from_utf8(&source[node.start_byte()..node.end_byte()])
13 .unwrap_or("")
14 .trim()
15 .to_string()
16}
17
18impl TsJuliaExtractor {
19 pub fn extract(source: &[u8], path: &Path) -> Result<ExtractionFragment> {
20 let (file_id, _, file_node) = make_file_node(path);
21 let mut fragment = ExtractionFragment {
22 nodes: vec![file_node],
23 edges: vec![],
24 };
25
26 let lang = tree_sitter_julia::LANGUAGE.into();
27
28 let module_query = r#"
30 (module_definition
31 (identifier) @module.name)
32 "#;
33 if let Ok(mut captures) = run_query_named(source, &lang, module_query) {
34 for name in captures.remove("module.name").unwrap_or_default() {
35 let id = make_id(&[&file_id, &name]);
36 fragment.nodes.push(Node {
37 id: id.clone(),
38 label: name,
39 file_type: "module".to_string(),
40 source_file: path.to_string_lossy().to_string(),
41 source_location: None,
42 community: None,
43 rationale: None,
44 docstring: None,
45 metadata: HashMap::new(),
46 });
47 add_contains_edge(&mut fragment, &file_id, id, path);
48 }
49 }
50
51 let func_query = r#"
53 (function_definition
54 (signature
55 (call_expression
56 . (identifier) @func.name)))
57 "#;
58 if let Ok(mut captures) = run_query_named(source, &lang, func_query) {
59 for name in captures.remove("func.name").unwrap_or_default() {
60 let label = format!("{}()", name);
61 let id = make_id(&[&file_id, &name]);
62 fragment.nodes.push(Node {
63 id: id.clone(),
64 label,
65 file_type: "function".to_string(),
66 source_file: path.to_string_lossy().to_string(),
67 source_location: None,
68 community: None,
69 rationale: None,
70 docstring: None,
71 metadata: HashMap::new(),
72 });
73 add_contains_edge(&mut fragment, &file_id, id, path);
74 }
75 }
76
77 let short_fn_query = r#"
79 (assignment
80 (call_expression
81 . (identifier) @fn.name))
82 "#;
83 if let Ok(mut captures) = run_query_named(source, &lang, short_fn_query) {
84 for name in captures.remove("fn.name").unwrap_or_default() {
85 let id = make_id(&[&file_id, &name]);
86 if !fragment.nodes.iter().any(|n| n.id == id) {
87 let label = format!("{}()", name);
88 fragment.nodes.push(Node {
89 id: id.clone(),
90 label,
91 file_type: "function".to_string(),
92 source_file: path.to_string_lossy().to_string(),
93 source_location: None,
94 community: None,
95 rationale: None,
96 docstring: None,
97 metadata: HashMap::new(),
98 });
99 add_contains_edge(&mut fragment, &file_id, id, path);
100 }
101 }
102 }
103
104 Self::extract_structs_and_inherits(source, path, &file_id, &lang, &mut fragment);
107
108 let abstract_query = r#"
110 (abstract_definition
111 (type_head
112 (identifier) @abs.name))
113 "#;
114 if let Ok(mut captures) = run_query_named(source, &lang, abstract_query) {
115 for name in captures.remove("abs.name").unwrap_or_default() {
116 let id = make_id(&[&file_id, &name]);
117 add_node_if_missing(
118 &mut fragment,
119 Node {
120 id: id.clone(),
121 label: name,
122 file_type: "class".to_string(),
123 source_file: path.to_string_lossy().to_string(),
124 source_location: None,
125 community: None,
126 rationale: None,
127 docstring: None,
128 metadata: HashMap::new(),
129 },
130 );
131 add_contains_edge(&mut fragment, &file_id, id, path);
132 }
133 }
134
135 let import_query = r#"
137 [
138 (import_statement (identifier) @import.name)
139 (using_statement (identifier) @import.name)
140 ]
141 "#;
142 if let Ok(mut captures) = run_query_named(source, &lang, import_query) {
143 for name in captures.remove("import.name").unwrap_or_default() {
144 let mod_id = make_id(&[&file_id, &name]);
145 add_node_if_missing(
146 &mut fragment,
147 Node {
148 id: mod_id.clone(),
149 label: name,
150 file_type: "module".to_string(),
151 source_file: path.to_string_lossy().to_string(),
152 source_location: None,
153 community: None,
154 rationale: None,
155 docstring: None,
156 metadata: HashMap::new(),
157 },
158 );
159 fragment.edges.push(Edge {
160 source: file_id.clone(),
161 target: mod_id,
162 relation: "imports".to_string(),
163 confidence: "EXTRACTED".to_string(),
164 source_file: Some(path.to_string_lossy().to_string()),
165 weight: 1.0,
166 context: Some("import".to_string()),
167 });
168 }
169 }
170
171 Self::extract_calls(source, path, &file_id, &lang, &mut fragment);
173
174 Ok(fragment)
175 }
176
177 fn extract_structs_and_inherits(
178 source: &[u8],
179 path: &Path,
180 file_id: &str,
181 lang: &tree_sitter::Language,
182 fragment: &mut ExtractionFragment,
183 ) {
184 let mut parser = Parser::new();
185 if parser.set_language(lang).is_err() {
186 return;
187 }
188 let tree = match parser.parse(source, None) {
189 Some(t) => t,
190 None => return,
191 };
192 let root = tree.root_node();
193 Self::walk_structs(root, source, path, file_id, file_id, fragment);
194 }
195
196 fn walk_structs(
197 node: TsNode<'_>,
198 source: &[u8],
199 path: &Path,
200 file_id: &str,
201 stem: &str,
202 fragment: &mut ExtractionFragment,
203 ) {
204 if node.kind() == "struct_definition" {
205 for i in 0..node.child_count() {
207 if let Some(type_head) = node.child(i) {
208 if type_head.kind() == "type_head" {
209 let mut bin_expr = None;
211 for j in 0..type_head.child_count() {
212 if let Some(c) = type_head.child(j) {
213 if c.kind() == "binary_expression" {
214 bin_expr = Some(c);
215 }
216 }
217 }
218 if let Some(bin) = bin_expr {
219 let identifiers: Vec<TsNode<'_>> = (0..bin.child_count())
221 .filter_map(|k| bin.child(k))
222 .filter(|c| c.kind() == "identifier")
223 .collect();
224 if !identifiers.is_empty() {
225 let struct_name = jl_text(source, &identifiers[0]);
226 let struct_id = make_id(&[stem, &struct_name]);
227 add_node_if_missing(
228 fragment,
229 Node {
230 id: struct_id.clone(),
231 label: struct_name.clone(),
232 file_type: "struct".to_string(),
233 source_file: path.to_string_lossy().to_string(),
234 source_location: None,
235 community: None,
236 rationale: None,
237 docstring: None,
238 metadata: HashMap::new(),
239 },
240 );
241 fragment.edges.push(Edge {
242 source: file_id.to_string(),
243 target: struct_id.clone(),
244 relation: "contains".to_string(),
245 confidence: "EXTRACTED".to_string(),
246 source_file: Some(path.to_string_lossy().to_string()),
247 weight: 1.0,
248 context: None,
249 });
250 if identifiers.len() >= 2 {
251 let super_name =
252 jl_text(source, &identifiers[identifiers.len() - 1]);
253 let super_id = make_id(&[stem, &super_name]);
254 add_node_if_missing(
255 fragment,
256 Node {
257 id: super_id.clone(),
258 label: super_name,
259 file_type: "code".to_string(),
260 source_file: path.to_string_lossy().to_string(),
261 source_location: None,
262 community: None,
263 rationale: None,
264 docstring: None,
265 metadata: HashMap::new(),
266 },
267 );
268 fragment.edges.push(Edge {
269 source: struct_id,
270 target: super_id,
271 relation: "inherits".to_string(),
272 confidence: "EXTRACTED".to_string(),
273 source_file: Some(path.to_string_lossy().to_string()),
274 weight: 1.0,
275 context: None,
276 });
277 }
278 }
279 } else {
280 for j in 0..type_head.child_count() {
282 if let Some(id_node) = type_head.child(j) {
283 if id_node.kind() == "identifier" {
284 let name = jl_text(source, &id_node);
285 let nid = make_id(&[stem, &name]);
286 add_node_if_missing(
287 fragment,
288 Node {
289 id: nid.clone(),
290 label: name,
291 file_type: "struct".to_string(),
292 source_file: path.to_string_lossy().to_string(),
293 source_location: None,
294 community: None,
295 rationale: None,
296 docstring: None,
297 metadata: HashMap::new(),
298 },
299 );
300 fragment.edges.push(Edge {
301 source: file_id.to_string(),
302 target: nid,
303 relation: "contains".to_string(),
304 confidence: "EXTRACTED".to_string(),
305 source_file: Some(path.to_string_lossy().to_string()),
306 weight: 1.0,
307 context: None,
308 });
309 break;
310 }
311 }
312 }
313 }
314 }
315 }
316 }
317 return;
318 }
319 for i in 0..node.child_count() {
320 if let Some(child) = node.child(i) {
321 Self::walk_structs(child, source, path, file_id, stem, fragment);
322 }
323 }
324 }
325
326 fn extract_calls(
327 source: &[u8],
328 path: &Path,
329 file_id: &str,
330 lang: &tree_sitter::Language,
331 fragment: &mut ExtractionFragment,
332 ) {
333 let mut parser = Parser::new();
334 if parser.set_language(lang).is_err() {
335 return;
336 }
337 let tree = match parser.parse(source, None) {
338 Some(t) => t,
339 None => return,
340 };
341 let root = tree.root_node();
342
343 let label_to_id: HashMap<String, String> = fragment
345 .nodes
346 .iter()
347 .filter(|n| n.file_type == "function")
348 .map(|n| {
349 let label = n.label.trim_end_matches("()").to_string();
350 (label, n.id.clone())
351 })
352 .collect();
353
354 let mut seen_pairs: std::collections::HashSet<(String, String)> = Default::default();
355 Self::walk_calls_julia(
356 root,
357 source,
358 path,
359 file_id,
360 file_id,
361 fragment,
362 None,
363 &label_to_id,
364 &mut seen_pairs,
365 );
366 }
367
368 #[allow(clippy::only_used_in_recursion, clippy::too_many_arguments)]
369 fn walk_calls_julia(
370 node: TsNode<'_>,
371 source: &[u8],
372 path: &Path,
373 file_id: &str,
374 stem: &str,
375 fragment: &mut ExtractionFragment,
376 caller_id: Option<&str>,
377 label_to_id: &HashMap<String, String>,
378 seen: &mut std::collections::HashSet<(String, String)>,
379 ) {
380 match node.kind() {
381 "function_definition" | "assignment" => {
382 let func_id = Self::get_julia_func_id(node, source, stem);
384 let effective_caller = func_id.as_deref().or(caller_id);
385 for i in 0..node.child_count() {
386 if let Some(child) = node.child(i) {
387 Self::walk_calls_julia(
388 child,
389 source,
390 path,
391 file_id,
392 stem,
393 fragment,
394 effective_caller,
395 label_to_id,
396 seen,
397 );
398 }
399 }
400 }
401 "call_expression" => {
402 if let Some(caller) = caller_id {
403 if let Some(first_child) = node.child(0) {
404 if first_child.kind() == "identifier" {
405 let callee_name = jl_text(source, &first_child);
406 if let Some(callee_id) = label_to_id.get(&callee_name) {
407 let pair = (caller.to_string(), callee_id.clone());
408 if !seen.contains(&pair) && caller != callee_id.as_str() {
409 seen.insert(pair.clone());
410 fragment.edges.push(Edge {
411 source: caller.to_string(),
412 target: callee_id.clone(),
413 relation: "calls".to_string(),
414 confidence: "EXTRACTED".to_string(),
415 source_file: Some(path.to_string_lossy().to_string()),
416 weight: 1.0,
417 context: Some("call".to_string()),
418 });
419 }
420 }
421 }
422 }
423 }
424 for i in 0..node.child_count() {
425 if let Some(child) = node.child(i) {
426 Self::walk_calls_julia(
427 child,
428 source,
429 path,
430 file_id,
431 stem,
432 fragment,
433 caller_id,
434 label_to_id,
435 seen,
436 );
437 }
438 }
439 }
440 _ => {
441 for i in 0..node.child_count() {
442 if let Some(child) = node.child(i) {
443 Self::walk_calls_julia(
444 child,
445 source,
446 path,
447 file_id,
448 stem,
449 fragment,
450 caller_id,
451 label_to_id,
452 seen,
453 );
454 }
455 }
456 }
457 }
458 }
459
460 fn get_julia_func_id(node: TsNode<'_>, source: &[u8], stem: &str) -> Option<String> {
461 if node.kind() == "function_definition" {
462 for i in 0..node.child_count() {
463 if let Some(sig) = node.child(i) {
464 if sig.kind() == "signature" {
465 for j in 0..sig.child_count() {
466 if let Some(call_expr) = sig.child(j) {
467 if call_expr.kind() == "call_expression" {
468 if let Some(name_node) = call_expr.child(0) {
469 if name_node.kind() == "identifier" {
470 let name = jl_text(source, &name_node);
471 return Some(make_id(&[stem, &name]));
472 }
473 }
474 }
475 }
476 }
477 }
478 }
479 }
480 } else if node.kind() == "assignment" {
481 if let Some(lhs) = node.child(0) {
482 if lhs.kind() == "call_expression" {
483 if let Some(name_node) = lhs.child(0) {
484 if name_node.kind() == "identifier" {
485 let name = jl_text(source, &name_node);
486 return Some(make_id(&[stem, &name]));
487 }
488 }
489 }
490 }
491 }
492 None
493 }
494}
495
496impl LanguageExtractor for TsJuliaExtractor {
497 fn file_extensions(&self) -> Vec<&'static str> {
498 vec!["jl"]
499 }
500 fn extract(&self, source: &[u8], path: &Path) -> Result<ExtractionFragment> {
501 Self::extract(source, path)
502 }
503 fn resolve_imports(&self, _imports: &[ImportNode]) -> Vec<Edge> {
504 vec![]
505 }
506 fn collect_type_refs(&self, _fragment: &mut ExtractionFragment) {}
507}