Skip to main content

codesynapse_core/ts_extract/
julia.rs

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        // Module declarations
29        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        // Function definitions
52        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        // Short function: perimeter(c::Circle) = expr
78        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        // Struct definitions (with optional <: supertype)
105        // Walk AST manually to handle binary_expression in type_head
106        Self::extract_structs_and_inherits(source, path, &file_id, &lang, &mut fragment);
107
108        // Abstract type definitions
109        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        // using / import statements with context="import"
136        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        // Call extraction: find call_expression nodes inside function bodies
172        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            // type_head may contain: identifier (simple) or binary_expression (Foo <: Bar)
206            for i in 0..node.child_count() {
207                if let Some(type_head) = node.child(i) {
208                    if type_head.kind() == "type_head" {
209                        // Check for binary_expression (Foo <: Bar)
210                        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                            // First identifier = struct name, last = supertype
220                            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                            // Simple struct, no supertype
281                            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        // Build label→id map for known functions
344        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                // Find the function name
383                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}