1use std::path::{Path, PathBuf};
4
5use crate::{
6 ContainerBody, Import, ImportSpec, Language, LanguageSymbols, ModuleId, ModuleResolver,
7 Resolution, ResolverConfig,
8};
9use tree_sitter::Node;
10
11pub 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 as_symbols(&self) -> Option<&dyn LanguageSymbols> {
26 Some(self)
27 }
28
29 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
30 if let Some(name_node) = node.child_by_field_name("name") {
32 return Some(&content[name_node.byte_range()]);
33 }
34 let mut cursor = node.walk();
37 for child in node.children(&mut cursor) {
38 if child.kind() == "signature" || child.kind() == "type_head" {
39 let text = &content[child.byte_range()];
40 let end = text
42 .find(|c: char| c == '(' || c == '<' || c == '{' || c.is_whitespace())
43 .unwrap_or(text.len());
44 if end > 0 {
45 return Some(&content[child.start_byte()..child.start_byte() + end]);
46 }
47 }
48 if child.kind() == "identifier" {
49 return Some(&content[child.byte_range()]);
50 }
51 }
52 None
53 }
54
55 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
56 let prev = node.prev_sibling()?;
57 if prev.kind() != "string_literal" {
58 return None;
59 }
60
61 let text = &content[prev.byte_range()];
62 if !text.starts_with("\"\"\"") {
63 return None;
64 }
65
66 let inner = text
68 .strip_prefix("\"\"\"")
69 .unwrap_or(text)
70 .strip_suffix("\"\"\"")
71 .unwrap_or(text);
72
73 let lines: Vec<&str> = inner
74 .lines()
75 .map(|l| l.trim())
76 .filter(|l| !l.is_empty())
77 .collect();
78
79 if lines.is_empty() {
80 return None;
81 }
82
83 Some(lines.join(" "))
84 }
85
86 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
87 let text = &content[node.byte_range()];
88 let line = node.start_position().row + 1;
89
90 let (keyword, is_wildcard) = if text.starts_with("using ") {
91 ("using ", true)
92 } else if text.starts_with("import ") {
93 ("import ", false)
94 } else {
95 return Vec::new();
96 };
97
98 let rest = text.strip_prefix(keyword).unwrap_or("");
99 let module = rest
100 .split([':', ','])
101 .next()
102 .map(|s| s.trim().to_string())
103 .unwrap_or_default();
104
105 if module.is_empty() {
106 return Vec::new();
107 }
108
109 vec![Import {
110 module,
111 names: Vec::new(),
112 alias: None,
113 is_wildcard,
114 is_relative: false,
115 line,
116 }]
117 }
118
119 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
120 let names_to_use: Vec<&str> = names
122 .map(|n| n.to_vec())
123 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
124 if names_to_use.is_empty() {
125 format!("using {}", import.module)
126 } else {
127 format!("import {}: {}", import.module, names_to_use.join(", "))
128 }
129 }
130
131 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
132 let name = symbol.name.as_str();
133 match symbol.kind {
134 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
135 crate::SymbolKind::Module => name == "tests" || name == "test",
136 _ => false,
137 }
138 }
139
140 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
141 node.child_by_field_name("body")
142 }
143
144 fn analyze_container_body(
145 &self,
146 body_node: &Node,
147 content: &str,
148 inner_indent: &str,
149 ) -> Option<ContainerBody> {
150 crate::body::analyze_end_body(body_node, content, inner_indent)
151 }
152
153 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
154 static RESOLVER: JuliaModuleResolver = JuliaModuleResolver;
155 Some(&RESOLVER)
156 }
157}
158
159impl LanguageSymbols for Julia {}
160
161pub struct JuliaModuleResolver;
172
173impl ModuleResolver for JuliaModuleResolver {
174 fn workspace_config(&self, root: &Path) -> ResolverConfig {
175 let mut path_mappings: Vec<(String, PathBuf)> = Vec::new();
176
177 let project_toml = root.join("Project.toml");
178 if let Ok(content) = std::fs::read_to_string(&project_toml)
179 && let Ok(parsed) = content.parse::<toml::Value>()
180 && let Some(name) = parsed.get("name").and_then(|v| v.as_str())
181 {
182 let src_dir = root.join("src");
183 path_mappings.push((name.to_string(), src_dir));
184 }
185
186 ResolverConfig {
187 workspace_root: root.to_path_buf(),
188 path_mappings,
189 search_roots: Vec::new(),
190 }
191 }
192
193 fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
194 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
195 if ext != "jl" {
196 return Vec::new();
197 }
198
199 let src_dir = cfg.workspace_root.join("src");
201 let base = if let Ok(rel) = file.strip_prefix(&src_dir) {
202 rel.to_string_lossy().into_owned()
203 } else if let Ok(rel) = file.strip_prefix(&cfg.workspace_root) {
204 rel.to_string_lossy().into_owned()
205 } else {
206 return Vec::new();
207 };
208
209 let canonical = base.strip_suffix(".jl").unwrap_or(&base).to_string();
210
211 if canonical.is_empty() {
212 return Vec::new();
213 }
214 vec![ModuleId {
215 canonical_path: canonical,
216 }]
217 }
218
219 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
220 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
221 if ext != "jl" {
222 return Resolution::NotApplicable;
223 }
224
225 let raw = &spec.raw;
226
227 if spec.is_relative || raw.ends_with(".jl") {
229 let base_dir = from_file.parent().unwrap_or(&cfg.workspace_root);
230 let candidate = base_dir.join(raw);
231 if candidate.exists() {
232 return Resolution::Resolved(candidate, String::new());
233 }
234 if !raw.ends_with(".jl") {
236 let with_ext = base_dir.join(format!("{}.jl", raw));
237 if with_ext.exists() {
238 return Resolution::Resolved(with_ext, String::new());
239 }
240 }
241 return Resolution::NotFound;
242 }
243
244 for (pkg_name, pkg_src) in &cfg.path_mappings {
246 if raw == pkg_name {
247 let main_file = pkg_src.join(format!("{}.jl", pkg_name));
249 if main_file.exists() {
250 return Resolution::Resolved(main_file, String::new());
251 }
252 if pkg_src.exists() {
254 return Resolution::Resolved(pkg_src.clone(), String::new());
255 }
256 }
257 }
258
259 Resolution::NotFound
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use crate::validate_unused_kinds_audit;
268
269 #[test]
270 fn unused_node_kinds_audit() {
271 #[rustfmt::skip]
272 let documented_unused: &[&str] = &[
273 "adjoint_expression", "binary_expression", "block",
274 "block_comment", "break_statement", "broadcast_call_expression", "call_expression",
275 "catch_clause", "compound_assignment_expression", "compound_statement",
276 "comprehension_expression", "continue_statement", "curly_expression", "else_clause",
277 "export_statement", "field_expression", "finally_clause", "for_binding", "for_clause",
278 "generator", "global_statement", "identifier", "if_clause", "import_alias",
279 "import_path", "index_expression", "interpolation_expression",
280 "juxtaposition_expression", "local_statement", "macro_identifier",
281 "macrocall_expression", "matrix_expression", "operator", "parametrized_type_expression",
282 "parenthesized_expression", "public_statement", "quote_expression", "quote_statement",
283 "range_expression", "return_statement", "selected_import", "splat_expression",
284 "tuple_expression", "typed_expression", "unary_expression",
285 "unary_typed_expression", "vector_expression", "where_expression",
286 "const_statement",
288 "arrow_function_expression",
289 "if_statement",
290 "using_statement",
291 "primitive_definition",
292 "for_statement",
293 "let_statement",
294 "ternary_expression",
295 "do_clause",
296 "while_statement",
297 "try_statement",
298 "elseif_clause",
299 "import_statement",
300 ];
301 validate_unused_kinds_audit(&Julia, documented_unused)
302 .expect("Julia unused node kinds audit failed");
303 }
304}