normalize_languages/
clojure.rs1use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
4use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
5use std::path::Path;
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 as_symbols(&self) -> Option<&dyn LanguageSymbols> {
23 Some(self)
24 }
25
26 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
27 if node.kind() != "list_lit" {
28 return Vec::new();
29 }
30
31 let (form, _) = match self.extract_def_form(node, content) {
32 Some(info) => info,
33 None => return Vec::new(),
34 };
35
36 if form != "require" && form != "use" && form != "import" {
37 return Vec::new();
38 }
39
40 vec![Import {
42 module: form,
43 names: Vec::new(),
44 alias: None,
45 is_wildcard: false,
46 is_relative: false,
47 line: node.start_position().row + 1,
48 }]
49 }
50
51 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
52 let names_to_use: Vec<&str> = names
54 .map(|n| n.to_vec())
55 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
56 if names_to_use.is_empty() {
57 format!("(require '[{}])", import.module)
58 } else {
59 format!(
60 "(require '[{} :refer [{}]])",
61 import.module,
62 names_to_use.join(" ")
63 )
64 }
65 }
66
67 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
68 if let Some((form, _)) = self.extract_def_form(node, content) {
69 if form.ends_with('-') {
70 Visibility::Private
71 } else {
72 Visibility::Public
73 }
74 } else {
75 Visibility::Public
76 }
77 }
78
79 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
80 let name = symbol.name.as_str();
81 match symbol.kind {
82 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
83 crate::SymbolKind::Module => name == "tests" || name == "test",
84 _ => false,
85 }
86 }
87
88 fn test_file_globs(&self) -> &'static [&'static str] {
89 &["**/*_test.clj", "**/*_test.cljs", "**/*_test.cljc"]
90 }
91
92 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
93 Some(*node)
95 }
96 fn analyze_container_body(
97 &self,
98 body_node: &Node,
99 content: &str,
100 inner_indent: &str,
101 ) -> Option<ContainerBody> {
102 crate::body::analyze_paren_body(body_node, content, inner_indent)
103 }
104
105 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
106 if node.kind() != "list_lit" {
109 return node
110 .child_by_field_name("name")
111 .map(|n| &content[n.byte_range()]);
112 }
113 let mut cursor = node.walk();
114 let mut seen_form = false;
115 for child in node.children(&mut cursor) {
116 if child.kind() == "sym_lit" {
117 if !seen_form {
118 seen_form = true;
119 } else {
120 return Some(&content[child.byte_range()]);
121 }
122 }
123 }
124 None
125 }
126
127 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
128 static RESOLVER: ClojureModuleResolver = ClojureModuleResolver;
129 Some(&RESOLVER)
130 }
131}
132
133impl LanguageSymbols for Clojure {}
134
135pub struct ClojureModuleResolver;
144
145impl ModuleResolver for ClojureModuleResolver {
146 fn workspace_config(&self, root: &Path) -> ResolverConfig {
147 ResolverConfig {
148 workspace_root: root.to_path_buf(),
149 path_mappings: Vec::new(),
150 search_roots: vec![root.join("src"), root.join("test"), root.to_path_buf()],
151 }
152 }
153
154 fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
155 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
156 if ext != "clj" && ext != "cljs" && ext != "cljc" {
157 return Vec::new();
158 }
159 for search_root in &cfg.search_roots {
160 if let Ok(rel) = file.strip_prefix(search_root) {
161 let module = rel
162 .to_str()
163 .unwrap_or("")
164 .trim_end_matches(".cljc")
165 .trim_end_matches(".cljs")
166 .trim_end_matches(".clj")
167 .replace('/', ".")
168 .replace('_', "-"); if !module.is_empty() {
170 return vec![ModuleId {
171 canonical_path: module,
172 }];
173 }
174 }
175 }
176 Vec::new()
177 }
178
179 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
180 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
181 if ext != "clj" && ext != "cljs" && ext != "cljc" {
182 return Resolution::NotApplicable;
183 }
184 let raw = &spec.raw;
185 if raw == "require" || raw == "use" || raw == "import" || raw == "ns" {
187 return Resolution::NotFound;
188 }
189 let path_part = raw.replace('.', "/").replace('-', "_");
191 let exported_name = raw.rsplit('.').next().unwrap_or(raw).to_string();
192
193 for search_root in &cfg.search_roots {
194 for ext_try in &["clj", "cljs", "cljc"] {
195 let candidate = search_root.join(format!("{}.{}", path_part, ext_try));
196 if candidate.exists() {
197 return Resolution::Resolved(candidate, exported_name);
198 }
199 }
200 }
201 Resolution::NotFound
202 }
203}
204
205impl Clojure {
206 fn extract_def_form(&self, node: &Node, content: &str) -> Option<(String, String)> {
208 let mut cursor = node.walk();
209 let mut form = None;
210 let mut name = None;
211
212 for child in node.children(&mut cursor) {
213 match child.kind() {
214 "sym_lit" if form.is_none() => {
215 form = Some(content[child.byte_range()].to_string());
216 }
217 "sym_lit" if form.is_some() && name.is_none() => {
218 name = Some(content[child.byte_range()].to_string());
219 break;
220 }
221 _ => {}
222 }
223 }
224
225 Some((form?, name?))
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use crate::validate_unused_kinds_audit;
233
234 #[test]
235 fn unused_node_kinds_audit() {
236 #[rustfmt::skip]
237 let documented_unused: &[&str] = &[];
238 validate_unused_kinds_audit(&Clojure, documented_unused)
239 .expect("Clojure unused node kinds audit failed");
240 }
241}