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 Elisp;
10
11impl Language for Elisp {
12 fn name(&self) -> &'static str {
13 "Emacs Lisp"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["el", "elc"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "elisp"
20 }
21
22 fn has_symbols(&self) -> bool {
23 true
24 }
25
26 fn container_kinds(&self) -> &'static [&'static str] {
27 &["list"] }
29
30 fn function_kinds(&self) -> &'static [&'static str] {
31 &["list"] }
33
34 fn type_kinds(&self) -> &'static [&'static str] {
35 &["list"] }
37
38 fn import_kinds(&self) -> &'static [&'static str] {
39 &["list"] }
41
42 fn public_symbol_kinds(&self) -> &'static [&'static str] {
43 &["list"]
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" {
52 return Vec::new();
53 }
54
55 let text = &content[node.byte_range()];
56 let line = node.start_position().row + 1;
57
58 for prefix in &["(defun ", "(defmacro ", "(defsubst ", "(cl-defun "] {
59 if text.starts_with(prefix) {
60 if let Some(name) = text[prefix.len()..].split_whitespace().next() {
61 if name.contains("--") {
63 return Vec::new();
64 }
65 return vec![Export {
66 name: name.to_string(),
67 kind: SymbolKind::Function,
68 line,
69 }];
70 }
71 }
72 }
73
74 if text.starts_with("(defvar ")
75 || text.starts_with("(defconst ")
76 || text.starts_with("(defcustom ")
77 {
78 let prefix_len = if text.starts_with("(defvar ") {
79 8
80 } else if text.starts_with("(defconst ") {
81 10
82 } else {
83 11
84 };
85 if let Some(name) = text[prefix_len..].split_whitespace().next() {
86 if !name.contains("--") {
87 return vec![Export {
88 name: name.to_string(),
89 kind: SymbolKind::Variable,
90 line,
91 }];
92 }
93 }
94 }
95
96 Vec::new()
97 }
98
99 fn scope_creating_kinds(&self) -> &'static [&'static str] {
100 &["list"] }
102
103 fn control_flow_kinds(&self) -> &'static [&'static str] {
104 &["list"] }
106
107 fn complexity_nodes(&self) -> &'static [&'static str] {
108 &["list"]
109 }
110
111 fn nesting_nodes(&self) -> &'static [&'static str] {
112 &["list"]
113 }
114
115 fn signature_suffix(&self) -> &'static str {
116 ""
117 }
118
119 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
120 if node.kind() != "list" {
121 return None;
122 }
123
124 let text = &content[node.byte_range()];
125 let first_line = text.lines().next().unwrap_or(text);
126
127 for prefix in &["(defun ", "(defmacro ", "(defsubst ", "(cl-defun "] {
128 if text.starts_with(prefix) {
129 if let Some(name) = text[prefix.len()..].split_whitespace().next() {
130 let is_private = name.contains("--");
131 return Some(Symbol {
132 name: name.to_string(),
133 kind: SymbolKind::Function,
134 signature: first_line.trim().to_string(),
135 docstring: self.extract_docstring(node, content),
136 attributes: Vec::new(),
137 start_line: node.start_position().row + 1,
138 end_line: node.end_position().row + 1,
139 visibility: if is_private {
140 Visibility::Private
141 } else {
142 Visibility::Public
143 },
144 children: Vec::new(),
145 is_interface_impl: false,
146 implements: Vec::new(),
147 });
148 }
149 }
150 }
151
152 None
153 }
154
155 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
156 if node.kind() != "list" {
157 return None;
158 }
159
160 let text = &content[node.byte_range()];
161
162 if text.starts_with("(defgroup ") {
163 let name = text["(defgroup ".len()..].split_whitespace().next()?;
164 return Some(Symbol {
165 name: name.to_string(),
166 kind: SymbolKind::Module,
167 signature: format!("(defgroup {})", name),
168 docstring: self.extract_docstring(node, content),
169 attributes: Vec::new(),
170 start_line: node.start_position().row + 1,
171 end_line: node.end_position().row + 1,
172 visibility: Visibility::Public,
173 children: Vec::new(),
174 is_interface_impl: false,
175 implements: Vec::new(),
176 });
177 }
178
179 None
180 }
181
182 fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
183 None
184 }
185
186 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
187 let text = &content[node.byte_range()];
189 if let Some(paren_end) = text.find(')') {
191 let after_args = &text[paren_end + 1..];
192 if let Some(start) = after_args.find('"')
193 && let Some(end) = after_args[start + 1..].find('"')
194 {
195 return Some(after_args[start + 1..start + 1 + end].to_string());
196 }
197 }
198 None
199 }
200
201 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
202 Vec::new()
203 }
204
205 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
206 if node.kind() != "list" {
207 return Vec::new();
208 }
209
210 let text = &content[node.byte_range()];
211 let line = node.start_position().row + 1;
212
213 if text.starts_with("(require ") {
214 let module = text["(require ".len()..]
215 .split(|c: char| c.is_whitespace() || c == ')')
216 .next()
217 .map(|s| s.trim_matches('\''))
218 .unwrap_or("")
219 .to_string();
220
221 if !module.is_empty() {
222 return vec![Import {
223 module,
224 names: Vec::new(),
225 alias: None,
226 is_wildcard: false,
227 is_relative: false,
228 line,
229 }];
230 }
231 }
232
233 Vec::new()
234 }
235
236 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
237 format!("(require '{})", import.module)
239 }
240
241 fn is_public(&self, node: &Node, content: &str) -> bool {
242 let text = &content[node.byte_range()];
243 !text.contains("--")
244 }
245
246 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
247 if self.is_public(node, content) {
248 Visibility::Public
249 } else {
250 Visibility::Private
251 }
252 }
253
254 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
255 let name = symbol.name.as_str();
256 match symbol.kind {
257 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
258 crate::SymbolKind::Module => name == "tests" || name == "test",
259 _ => false,
260 }
261 }
262
263 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
264 None
265 }
266
267 fn container_body<'a>(&self, _node: &'a Node<'a>) -> Option<Node<'a>> {
268 None
269 }
270 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
271 false
272 }
273 fn node_name<'a>(&self, _node: &Node, _content: &'a str) -> Option<&'a str> {
274 None
275 }
276
277 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
278 let ext = path.extension()?.to_str()?;
279 if ext != "el" {
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!("{}.el", module)]
288 }
289
290 fn lang_key(&self) -> &'static str {
291 "elisp"
292 }
293
294 fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
295 matches!(
296 import_name,
297 "cl-lib" | "seq" | "subr-x" | "map" | "pcase" | "rx"
298 )
299 }
300
301 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
302 None
303 }
304
305 fn resolve_local_import(
306 &self,
307 import: &str,
308 current_file: &Path,
309 _project_root: &Path,
310 ) -> Option<PathBuf> {
311 let dir = current_file.parent()?;
312 let full = dir.join(format!("{}.el", import));
313 if full.is_file() { Some(full) } else { None }
314 }
315
316 fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
317 None
318 }
319
320 fn get_version(&self, project_root: &Path) -> Option<String> {
321 if project_root.join("Cask").is_file() {
322 return Some("Cask".to_string());
323 }
324 None
325 }
326
327 fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
328 if let Some(home) = std::env::var_os("HOME") {
329 let elpa = PathBuf::from(home).join(".emacs.d/elpa");
330 if elpa.is_dir() {
331 return Some(elpa);
332 }
333 }
334 None
335 }
336
337 fn indexable_extensions(&self) -> &'static [&'static str] {
338 &["el"]
339 }
340 fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
341 Vec::new()
342 }
343
344 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
345 use crate::traits::{has_extension, skip_dotfiles};
346 if skip_dotfiles(name) {
347 return true;
348 }
349 !is_dir && !has_extension(name, self.indexable_extensions())
350 }
351
352 fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
353 Vec::new()
354 }
355
356 fn package_module_name(&self, entry_name: &str) -> String {
357 entry_name
358 .strip_suffix(".el")
359 .unwrap_or(entry_name)
360 .to_string()
361 }
362
363 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
364 if path.is_file() {
365 Some(path.to_path_buf())
366 } else {
367 None
368 }
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use crate::validate_unused_kinds_audit;
376
377 #[test]
378 fn unused_node_kinds_audit() {
379 #[rustfmt::skip]
380 let documented_unused: &[&str] = &[
381 "function_definition", "macro_definition", "special_form",
383 ];
384 validate_unused_kinds_audit(&Elisp, documented_unused)
385 .expect("Emacs Lisp unused node kinds audit failed");
386 }
387}