Skip to main content

normalize_languages/
julia.rs

1//! Julia language support.
2
3use crate::{ContainerBody, Import, Language, LanguageSymbols};
4use tree_sitter::Node;
5
6/// Julia language support.
7pub struct Julia;
8
9impl Language for Julia {
10    fn name(&self) -> &'static str {
11        "Julia"
12    }
13    fn extensions(&self) -> &'static [&'static str] {
14        &["jl"]
15    }
16    fn grammar_name(&self) -> &'static str {
17        "julia"
18    }
19
20    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
21        Some(self)
22    }
23
24    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
25        // module_definition has a "name" field
26        if let Some(name_node) = node.child_by_field_name("name") {
27            return Some(&content[name_node.byte_range()]);
28        }
29        // function_definition/macro_definition: name in signature (no named children)
30        // struct_definition/abstract_definition: name in type_head
31        let mut cursor = node.walk();
32        for child in node.children(&mut cursor) {
33            if child.kind() == "signature" || child.kind() == "type_head" {
34                let text = &content[child.byte_range()];
35                // "add(a, b)" → "add", "Foo <: Bar" → "Foo"
36                let end = text
37                    .find(|c: char| c == '(' || c == '<' || c == '{' || c.is_whitespace())
38                    .unwrap_or(text.len());
39                if end > 0 {
40                    return Some(&content[child.start_byte()..child.start_byte() + end]);
41                }
42            }
43            if child.kind() == "identifier" {
44                return Some(&content[child.byte_range()]);
45            }
46        }
47        None
48    }
49
50    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
51        let prev = node.prev_sibling()?;
52        if prev.kind() != "string_literal" {
53            return None;
54        }
55
56        let text = &content[prev.byte_range()];
57        if !text.starts_with("\"\"\"") {
58            return None;
59        }
60
61        // Strip the triple quotes and clean up
62        let inner = text
63            .strip_prefix("\"\"\"")
64            .unwrap_or(text)
65            .strip_suffix("\"\"\"")
66            .unwrap_or(text);
67
68        let lines: Vec<&str> = inner
69            .lines()
70            .map(|l| l.trim())
71            .filter(|l| !l.is_empty())
72            .collect();
73
74        if lines.is_empty() {
75            return None;
76        }
77
78        Some(lines.join(" "))
79    }
80
81    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
82        let text = &content[node.byte_range()];
83        let line = node.start_position().row + 1;
84
85        let (keyword, is_wildcard) = if text.starts_with("using ") {
86            ("using ", true)
87        } else if text.starts_with("import ") {
88            ("import ", false)
89        } else {
90            return Vec::new();
91        };
92
93        let rest = text.strip_prefix(keyword).unwrap_or("");
94        let module = rest
95            .split([':', ','])
96            .next()
97            .map(|s| s.trim().to_string())
98            .unwrap_or_default();
99
100        if module.is_empty() {
101            return Vec::new();
102        }
103
104        vec![Import {
105            module,
106            names: Vec::new(),
107            alias: None,
108            is_wildcard,
109            is_relative: false,
110            line,
111        }]
112    }
113
114    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
115        // Julia: using Module or import Module: a, b, c
116        let names_to_use: Vec<&str> = names
117            .map(|n| n.to_vec())
118            .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
119        if names_to_use.is_empty() {
120            format!("using {}", import.module)
121        } else {
122            format!("import {}: {}", import.module, names_to_use.join(", "))
123        }
124    }
125
126    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
127        let name = symbol.name.as_str();
128        match symbol.kind {
129            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
130            crate::SymbolKind::Module => name == "tests" || name == "test",
131            _ => false,
132        }
133    }
134
135    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
136        node.child_by_field_name("body")
137    }
138
139    fn analyze_container_body(
140        &self,
141        body_node: &Node,
142        content: &str,
143        inner_indent: &str,
144    ) -> Option<ContainerBody> {
145        crate::body::analyze_end_body(body_node, content, inner_indent)
146    }
147}
148
149impl LanguageSymbols for Julia {}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::validate_unused_kinds_audit;
155
156    #[test]
157    fn unused_node_kinds_audit() {
158        #[rustfmt::skip]
159        let documented_unused: &[&str] = &[
160            "adjoint_expression", "binary_expression", "block",
161            "block_comment", "break_statement", "broadcast_call_expression", "call_expression",
162            "catch_clause", "compound_assignment_expression", "compound_statement",
163            "comprehension_expression", "continue_statement", "curly_expression", "else_clause",
164            "export_statement", "field_expression", "finally_clause", "for_binding", "for_clause",
165            "generator", "global_statement", "identifier", "if_clause", "import_alias",
166            "import_path", "index_expression", "interpolation_expression",
167            "juxtaposition_expression", "local_statement", "macro_identifier",
168            "macrocall_expression", "matrix_expression", "operator", "parametrized_type_expression",
169            "parenthesized_expression", "public_statement", "quote_expression", "quote_statement",
170            "range_expression", "return_statement", "selected_import", "splat_expression",
171            "tuple_expression", "typed_expression", "unary_expression",
172            "unary_typed_expression", "vector_expression", "where_expression",
173            // covered by tags.scm
174            "const_statement",
175            "arrow_function_expression",
176            "if_statement",
177            "using_statement",
178            "primitive_definition",
179            "for_statement",
180            "let_statement",
181            "ternary_expression",
182            "do_clause",
183            "while_statement",
184            "try_statement",
185            "elseif_clause",
186            "import_statement",
187        ];
188        validate_unused_kinds_audit(&Julia, documented_unused)
189            .expect("Julia unused node kinds audit failed");
190    }
191}