Skip to main content

normalize_languages/
julia.rs

1//! Julia language support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{
5    Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism,
6    simple_function_symbol,
7};
8use std::path::{Path, PathBuf};
9use tree_sitter::Node;
10
11/// Julia language support.
12pub struct Julia;
13
14impl Language for Julia {
15    fn name(&self) -> &'static str {
16        "Julia"
17    }
18    fn extensions(&self) -> &'static [&'static str] {
19        &["jl"]
20    }
21    fn grammar_name(&self) -> &'static str {
22        "julia"
23    }
24
25    fn has_symbols(&self) -> bool {
26        true
27    }
28
29    fn container_kinds(&self) -> &'static [&'static str] {
30        &[
31            "module_definition",
32            "struct_definition",
33            "abstract_definition",
34        ]
35    }
36
37    fn function_kinds(&self) -> &'static [&'static str] {
38        &[
39            "function_definition",
40            "arrow_function_expression",
41            "macro_definition",
42        ]
43    }
44
45    fn type_kinds(&self) -> &'static [&'static str] {
46        &[
47            "struct_definition",
48            "abstract_definition",
49            "primitive_definition",
50        ]
51    }
52
53    fn import_kinds(&self) -> &'static [&'static str] {
54        &["import_statement", "using_statement"]
55    }
56
57    fn public_symbol_kinds(&self) -> &'static [&'static str] {
58        &[
59            "function_definition",
60            "struct_definition",
61            "const_statement",
62        ]
63    }
64
65    fn visibility_mechanism(&self) -> VisibilityMechanism {
66        VisibilityMechanism::ExplicitExport
67    }
68
69    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
70        let name = match self.node_name(node, content) {
71            Some(n) => n.to_string(),
72            None => return Vec::new(),
73        };
74
75        let kind = match node.kind() {
76            "function_definition" | "arrow_function_expression" => SymbolKind::Function,
77            "macro_definition" => SymbolKind::Function,
78            "struct_definition" => SymbolKind::Struct,
79            "abstract_definition" => SymbolKind::Interface,
80            "module_definition" => SymbolKind::Module,
81            _ => return Vec::new(),
82        };
83
84        vec![Export {
85            name,
86            kind,
87            line: node.start_position().row + 1,
88        }]
89    }
90
91    fn scope_creating_kinds(&self) -> &'static [&'static str] {
92        &[
93            "function_definition",
94            "let_statement",
95            "do_clause",
96            "module_definition",
97        ]
98    }
99
100    fn control_flow_kinds(&self) -> &'static [&'static str] {
101        &[
102            "if_statement",
103            "for_statement",
104            "while_statement",
105            "try_statement",
106        ]
107    }
108
109    fn complexity_nodes(&self) -> &'static [&'static str] {
110        &[
111            "if_statement",
112            "for_statement",
113            "while_statement",
114            "elseif_clause",
115            "ternary_expression",
116        ]
117    }
118
119    fn nesting_nodes(&self) -> &'static [&'static str] {
120        &[
121            "function_definition",
122            "module_definition",
123            "struct_definition",
124            "if_statement",
125            "for_statement",
126        ]
127    }
128
129    fn signature_suffix(&self) -> &'static str {
130        ""
131    }
132
133    fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
134        let name = self.node_name(node, content)?;
135        Some(simple_function_symbol(
136            node,
137            content,
138            name,
139            self.extract_docstring(node, content),
140        ))
141    }
142
143    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
144        let name = self.node_name(node, content)?;
145
146        let (kind, keyword) = match node.kind() {
147            "module_definition" => (SymbolKind::Module, "module"),
148            "struct_definition" => (SymbolKind::Struct, "struct"),
149            "abstract_definition" => (SymbolKind::Interface, "abstract type"),
150            _ => return None,
151        };
152
153        Some(Symbol {
154            name: name.to_string(),
155            kind,
156            signature: format!("{} {}", keyword, name),
157            docstring: self.extract_docstring(node, content),
158            attributes: Vec::new(),
159            start_line: node.start_position().row + 1,
160            end_line: node.end_position().row + 1,
161            visibility: Visibility::Public,
162            children: Vec::new(),
163            is_interface_impl: false,
164            implements: Vec::new(),
165        })
166    }
167
168    fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
169        self.extract_container(node, content)
170    }
171
172    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
173        // Julia uses """ docstrings before definitions
174        let mut prev = node.prev_sibling();
175        while let Some(sibling) = prev {
176            let text = &content[sibling.byte_range()];
177            if sibling.kind() == "string_literal" && text.starts_with("\"\"\"") {
178                let inner = text
179                    .trim_start_matches("\"\"\"")
180                    .trim_end_matches("\"\"\"")
181                    .trim();
182                if !inner.is_empty() {
183                    return Some(inner.lines().next().unwrap_or(inner).to_string());
184                }
185            }
186            if sibling.kind() == "comment" {
187                prev = sibling.prev_sibling();
188            } else {
189                break;
190            }
191        }
192        None
193    }
194
195    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
196        Vec::new()
197    }
198
199    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
200        let text = &content[node.byte_range()];
201        let line = node.start_position().row + 1;
202
203        let (keyword, is_wildcard) = if text.starts_with("using ") {
204            ("using ", true)
205        } else if text.starts_with("import ") {
206            ("import ", false)
207        } else {
208            return Vec::new();
209        };
210
211        let rest = text.strip_prefix(keyword).unwrap_or("");
212        let module = rest
213            .split(|c| c == ':' || c == ',')
214            .next()
215            .map(|s| s.trim().to_string())
216            .unwrap_or_default();
217
218        if module.is_empty() {
219            return Vec::new();
220        }
221
222        vec![Import {
223            module,
224            names: Vec::new(),
225            alias: None,
226            is_wildcard,
227            is_relative: false,
228            line,
229        }]
230    }
231
232    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
233        // Julia: using Module or import Module: a, b, c
234        let names_to_use: Vec<&str> = names
235            .map(|n| n.to_vec())
236            .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
237        if names_to_use.is_empty() {
238            format!("using {}", import.module)
239        } else {
240            format!("import {}: {}", import.module, names_to_use.join(", "))
241        }
242    }
243
244    fn is_public(&self, _node: &Node, _content: &str) -> bool {
245        true
246    }
247    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
248        Visibility::Public
249    }
250
251    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
252        let name = symbol.name.as_str();
253        match symbol.kind {
254            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
255            crate::SymbolKind::Module => name == "tests" || name == "test",
256            _ => false,
257        }
258    }
259
260    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
261        None
262    }
263
264    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
265        node.child_by_field_name("body")
266    }
267
268    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
269        false
270    }
271
272    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
273        node.child_by_field_name("name")
274            .map(|n| &content[n.byte_range()])
275    }
276
277    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
278        let ext = path.extension()?.to_str()?;
279        if ext != "jl" {
280            return None;
281        }
282        let stem = path.file_stem()?.to_str()?;
283        Some(stem.to_string())
284    }
285
286    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
287        vec![format!("{}.jl", module), format!("src/{}.jl", module)]
288    }
289
290    fn lang_key(&self) -> &'static str {
291        "julia"
292    }
293
294    fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
295        matches!(
296            import_name,
297            "Base"
298                | "Core"
299                | "Main"
300                | "LinearAlgebra"
301                | "Statistics"
302                | "Random"
303                | "Dates"
304                | "Printf"
305                | "Test"
306                | "Pkg"
307        )
308    }
309
310    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
311        None
312    }
313
314    fn resolve_local_import(
315        &self,
316        import: &str,
317        _current_file: &Path,
318        project_root: &Path,
319    ) -> Option<PathBuf> {
320        let candidates = [
321            project_root.join("src").join(format!("{}.jl", import)),
322            project_root.join(format!("{}.jl", import)),
323        ];
324        for c in &candidates {
325            if c.is_file() {
326                return Some(c.clone());
327            }
328        }
329        None
330    }
331
332    fn resolve_external_import(
333        &self,
334        _import_name: &str,
335        _project_root: &Path,
336    ) -> Option<ResolvedPackage> {
337        None
338    }
339
340    fn get_version(&self, project_root: &Path) -> Option<String> {
341        if project_root.join("Project.toml").is_file() {
342            return Some("Project.toml".to_string());
343        }
344        None
345    }
346
347    fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
348        if let Some(home) = std::env::var_os("HOME") {
349            let depot = PathBuf::from(home).join(".julia/packages");
350            if depot.is_dir() {
351                return Some(depot);
352            }
353        }
354        None
355    }
356
357    fn indexable_extensions(&self) -> &'static [&'static str] {
358        &["jl"]
359    }
360    fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
361        Vec::new()
362    }
363
364    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
365        use crate::traits::{has_extension, skip_dotfiles};
366        if skip_dotfiles(name) {
367            return true;
368        }
369        if is_dir && (name == "test" || name == "docs" || name == "benchmark") {
370            return true;
371        }
372        !is_dir && !has_extension(name, self.indexable_extensions())
373    }
374
375    fn discover_packages(&self, _source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
376        Vec::new()
377    }
378
379    fn package_module_name(&self, entry_name: &str) -> String {
380        entry_name
381            .strip_suffix(".jl")
382            .unwrap_or(entry_name)
383            .to_string()
384    }
385
386    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
387        if path.is_file() {
388            return Some(path.to_path_buf());
389        }
390        let src = path
391            .join("src")
392            .join(format!("{}.jl", path.file_name()?.to_str()?));
393        if src.is_file() {
394            return Some(src);
395        }
396        None
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use crate::validate_unused_kinds_audit;
404
405    #[test]
406    fn unused_node_kinds_audit() {
407        #[rustfmt::skip]
408        let documented_unused: &[&str] = &[
409            "adjoint_expression", "binary_expression", "block",
410            "block_comment", "break_statement", "broadcast_call_expression", "call_expression",
411            "catch_clause", "compound_assignment_expression", "compound_statement",
412            "comprehension_expression", "continue_statement", "curly_expression", "else_clause",
413            "export_statement", "field_expression", "finally_clause", "for_binding", "for_clause",
414            "generator", "global_statement", "identifier", "if_clause", "import_alias",
415            "import_path", "index_expression", "interpolation_expression",
416            "juxtaposition_expression", "local_statement", "macro_identifier",
417            "macrocall_expression", "matrix_expression", "operator", "parametrized_type_expression",
418            "parenthesized_expression", "public_statement", "quote_expression", "quote_statement",
419            "range_expression", "return_statement", "selected_import", "splat_expression",
420            "tuple_expression", "type_head", "typed_expression", "unary_expression",
421            "unary_typed_expression", "vector_expression", "where_expression",
422        ];
423        validate_unused_kinds_audit(&Julia, documented_unused)
424            .expect("Julia unused node kinds audit failed");
425    }
426}