1use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8pub struct Clojure;
10
11impl Language for Clojure {
12 fn name(&self) -> &'static str {
13 "Clojure"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["clj", "cljs", "cljc", "edn"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "clojure"
20 }
21
22 fn has_symbols(&self) -> bool {
23 true
24 }
25
26 fn container_kinds(&self) -> &'static [&'static str] {
27 &["list_lit"] }
29
30 fn function_kinds(&self) -> &'static [&'static str] {
31 &["list_lit"] }
33
34 fn type_kinds(&self) -> &'static [&'static str] {
35 &["list_lit"] }
37
38 fn import_kinds(&self) -> &'static [&'static str] {
39 &["list_lit"] }
41
42 fn public_symbol_kinds(&self) -> &'static [&'static str] {
43 &["list_lit"]
44 }
45
46 fn visibility_mechanism(&self) -> VisibilityMechanism {
47 VisibilityMechanism::NamingConvention }
49
50 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
51 if node.kind() != "list_lit" {
52 return Vec::new();
53 }
54
55 let (form, name) = match self.extract_def_form(node, content) {
56 Some(info) => info,
57 None => return Vec::new(),
58 };
59
60 if form == "defn-" || form == "def-" {
62 return Vec::new();
63 }
64
65 let kind = match form.as_str() {
66 "defn" | "defmacro" | "defmethod" => SymbolKind::Function,
67 "defrecord" | "deftype" | "defprotocol" => SymbolKind::Struct,
68 "def" | "defonce" => SymbolKind::Variable,
69 _ => return Vec::new(),
70 };
71
72 vec![Export {
73 name,
74 kind,
75 line: node.start_position().row + 1,
76 }]
77 }
78
79 fn scope_creating_kinds(&self) -> &'static [&'static str] {
80 &["list_lit"] }
82
83 fn control_flow_kinds(&self) -> &'static [&'static str] {
84 &["list_lit"] }
86
87 fn complexity_nodes(&self) -> &'static [&'static str] {
88 &["list_lit"]
89 }
90
91 fn nesting_nodes(&self) -> &'static [&'static str] {
92 &["list_lit", "vec_lit", "map_lit"]
93 }
94
95 fn signature_suffix(&self) -> &'static str {
96 ""
97 }
98
99 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
100 if node.kind() != "list_lit" {
101 return None;
102 }
103
104 let (form, name) = self.extract_def_form(node, content)?;
105
106 if !matches!(form.as_str(), "defn" | "defn-" | "defmacro" | "defmethod") {
107 return None;
108 }
109
110 let text = &content[node.byte_range()];
111 let first_line = text.lines().next().unwrap_or(text);
112
113 Some(Symbol {
114 name,
115 kind: SymbolKind::Function,
116 signature: first_line.trim().to_string(),
117 docstring: self.extract_docstring(node, content),
118 attributes: Vec::new(),
119 start_line: node.start_position().row + 1,
120 end_line: node.end_position().row + 1,
121 visibility: if form == "defn-" {
122 Visibility::Private
123 } else {
124 Visibility::Public
125 },
126 children: Vec::new(),
127 is_interface_impl: false,
128 implements: Vec::new(),
129 })
130 }
131
132 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
133 if node.kind() != "list_lit" {
134 return None;
135 }
136
137 let (form, name) = self.extract_def_form(node, content)?;
138
139 let kind = match form.as_str() {
140 "ns" => SymbolKind::Module,
141 "defrecord" | "deftype" => SymbolKind::Struct,
142 "defprotocol" => SymbolKind::Interface,
143 _ => return None,
144 };
145
146 Some(Symbol {
147 name: name.clone(),
148 kind,
149 signature: format!("({} {})", form, name),
150 docstring: self.extract_docstring(node, content),
151 attributes: Vec::new(),
152 start_line: node.start_position().row + 1,
153 end_line: node.end_position().row + 1,
154 visibility: Visibility::Public,
155 children: Vec::new(),
156 is_interface_impl: false,
157 implements: Vec::new(),
158 })
159 }
160
161 fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
162 self.extract_container(node, content)
163 }
164
165 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
166 let mut cursor = node.walk();
169 let children: Vec<_> = node.children(&mut cursor).collect();
170
171 if children.len() > 2 {
173 let third = &children[2];
174 if third.kind() == "str_lit" {
175 let text = &content[third.byte_range()];
176 return Some(text.trim_matches('"').to_string());
177 }
178 }
179 None
180 }
181
182 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
183 Vec::new()
184 }
185
186 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
187 if node.kind() != "list_lit" {
188 return Vec::new();
189 }
190
191 let (form, _) = match self.extract_def_form(node, content) {
192 Some(info) => info,
193 None => return Vec::new(),
194 };
195
196 if form != "require" && form != "use" && form != "import" {
197 return Vec::new();
198 }
199
200 vec![Import {
202 module: form,
203 names: Vec::new(),
204 alias: None,
205 is_wildcard: false,
206 is_relative: false,
207 line: node.start_position().row + 1,
208 }]
209 }
210
211 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
212 let names_to_use: Vec<&str> = names
214 .map(|n| n.to_vec())
215 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
216 if names_to_use.is_empty() {
217 format!("(require '[{}])", import.module)
218 } else {
219 format!(
220 "(require '[{} :refer [{}]])",
221 import.module,
222 names_to_use.join(" ")
223 )
224 }
225 }
226
227 fn is_public(&self, node: &Node, content: &str) -> bool {
228 if let Some((form, _)) = self.extract_def_form(node, content) {
229 !form.ends_with('-')
230 } else {
231 true
232 }
233 }
234
235 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
236 if self.is_public(node, content) {
237 Visibility::Public
238 } else {
239 Visibility::Private
240 }
241 }
242
243 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
244 let name = symbol.name.as_str();
245 match symbol.kind {
246 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
247 crate::SymbolKind::Module => name == "tests" || name == "test",
248 _ => false,
249 }
250 }
251
252 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
253 None
254 }
255
256 fn container_body<'a>(&self, _node: &'a Node<'a>) -> Option<Node<'a>> {
257 None
258 }
259 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
260 false
261 }
262 fn node_name<'a>(&self, _node: &Node, _content: &'a str) -> Option<&'a str> {
263 None
264 }
265
266 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
267 let ext = path.extension()?.to_str()?;
268 if !["clj", "cljs", "cljc"].contains(&ext) {
269 return None;
270 }
271 let stem = path.file_stem()?.to_str()?;
272 Some(stem.replace('_', "-"))
273 }
274
275 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
276 let path = module.replace('-', "_").replace('.', "/");
277 vec![
278 format!("{}.clj", path),
279 format!("{}.cljs", path),
280 format!("{}.cljc", path),
281 ]
282 }
283
284 fn lang_key(&self) -> &'static str {
285 "clojure"
286 }
287
288 fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
289 import_name.starts_with("clojure.") || import_name.starts_with("cljs.")
290 }
291
292 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
293 None
294 }
295
296 fn resolve_local_import(
297 &self,
298 import: &str,
299 current_file: &Path,
300 _project_root: &Path,
301 ) -> Option<PathBuf> {
302 let dir = current_file.parent()?;
303 let path = import.replace('-', "_").replace('.', "/");
304 for ext in &["clj", "cljs", "cljc"] {
305 let full = dir.join(format!("{}.{}", path, ext));
306 if full.is_file() {
307 return Some(full);
308 }
309 }
310 None
311 }
312
313 fn resolve_external_import(
314 &self,
315 _import_name: &str,
316 _project_root: &Path,
317 ) -> Option<ResolvedPackage> {
318 None
319 }
320
321 fn get_version(&self, project_root: &Path) -> Option<String> {
322 let project_clj = project_root.join("project.clj");
324 if project_clj.is_file() {
325 return Some("leiningen".to_string());
326 }
327 let deps_edn = project_root.join("deps.edn");
328 if deps_edn.is_file() {
329 return Some("deps.edn".to_string());
330 }
331 None
332 }
333
334 fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
335 if let Some(home) = std::env::var_os("HOME") {
336 let m2 = PathBuf::from(home).join(".m2/repository");
337 if m2.is_dir() {
338 return Some(m2);
339 }
340 }
341 None
342 }
343
344 fn indexable_extensions(&self) -> &'static [&'static str] {
345 &["clj", "cljs", "cljc"]
346 }
347 fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
348 Vec::new()
349 }
350
351 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
352 use crate::traits::{has_extension, skip_dotfiles};
353 if skip_dotfiles(name) {
354 return true;
355 }
356 if is_dir && name == "target" {
357 return true;
358 }
359 !is_dir && !has_extension(name, self.indexable_extensions())
360 }
361
362 fn discover_packages(&self, _source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
363 Vec::new()
364 }
365
366 fn package_module_name(&self, entry_name: &str) -> String {
367 entry_name
368 .strip_suffix(".clj")
369 .or_else(|| entry_name.strip_suffix(".cljs"))
370 .or_else(|| entry_name.strip_suffix(".cljc"))
371 .unwrap_or(entry_name)
372 .replace('_', "-")
373 }
374
375 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
376 if path.is_file() {
377 Some(path.to_path_buf())
378 } else {
379 None
380 }
381 }
382}
383
384impl Clojure {
385 fn extract_def_form(&self, node: &Node, content: &str) -> Option<(String, String)> {
387 let mut cursor = node.walk();
388 let mut form = None;
389 let mut name = None;
390
391 for child in node.children(&mut cursor) {
392 match child.kind() {
393 "sym_lit" if form.is_none() => {
394 form = Some(content[child.byte_range()].to_string());
395 }
396 "sym_lit" if form.is_some() && name.is_none() => {
397 name = Some(content[child.byte_range()].to_string());
398 break;
399 }
400 _ => {}
401 }
402 }
403
404 Some((form?, name?))
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411 use crate::validate_unused_kinds_audit;
412
413 #[test]
414 fn unused_node_kinds_audit() {
415 #[rustfmt::skip]
416 let documented_unused: &[&str] = &[];
417 validate_unused_kinds_audit(&Clojure, documented_unused)
418 .expect("Clojure unused node kinds audit failed");
419 }
420}