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 CommonLisp;
10
11impl Language for CommonLisp {
12 fn name(&self) -> &'static str {
13 "Common Lisp"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["lisp", "lsp", "cl", "asd"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "commonlisp"
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::ExplicitExport }
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 text = &content[node.byte_range()];
56 let line = node.start_position().row + 1;
57
58 for prefix in &["(defun ", "(defmacro ", "(defgeneric ", "(defmethod "] {
60 if text.starts_with(prefix) {
61 if let Some(name) = text[prefix.len()..].split_whitespace().next() {
62 return vec![Export {
63 name: name.to_string(),
64 kind: SymbolKind::Function,
65 line,
66 }];
67 }
68 }
69 }
70
71 for prefix in &["(defclass ", "(defstruct "] {
72 if text.starts_with(prefix) {
73 if let Some(name) = text[prefix.len()..].split_whitespace().next() {
74 return vec![Export {
75 name: name.to_string(),
76 kind: SymbolKind::Class,
77 line,
78 }];
79 }
80 }
81 }
82
83 Vec::new()
84 }
85
86 fn scope_creating_kinds(&self) -> &'static [&'static str] {
87 &["list_lit"] }
89
90 fn control_flow_kinds(&self) -> &'static [&'static str] {
91 &["list_lit"] }
93
94 fn complexity_nodes(&self) -> &'static [&'static str] {
95 &["list_lit"]
96 }
97
98 fn nesting_nodes(&self) -> &'static [&'static str] {
99 &["list_lit"]
100 }
101
102 fn signature_suffix(&self) -> &'static str {
103 ""
104 }
105
106 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
107 if node.kind() != "list_lit" {
108 return None;
109 }
110
111 let text = &content[node.byte_range()];
112 let first_line = text.lines().next().unwrap_or(text);
113
114 for prefix in &["(defun ", "(defmacro ", "(defgeneric ", "(defmethod "] {
115 if text.starts_with(prefix) {
116 if let Some(name) = text[prefix.len()..].split_whitespace().next() {
117 return Some(Symbol {
118 name: name.to_string(),
119 kind: SymbolKind::Function,
120 signature: first_line.trim().to_string(),
121 docstring: self.extract_docstring(node, content),
122 attributes: Vec::new(),
123 start_line: node.start_position().row + 1,
124 end_line: node.end_position().row + 1,
125 visibility: Visibility::Public,
126 children: Vec::new(),
127 is_interface_impl: false,
128 implements: Vec::new(),
129 });
130 }
131 }
132 }
133
134 None
135 }
136
137 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
138 if node.kind() != "list_lit" {
139 return None;
140 }
141
142 let text = &content[node.byte_range()];
143
144 if text.starts_with("(defpackage ") {
145 let name = text["(defpackage ".len()..].split_whitespace().next()?;
146 return Some(Symbol {
147 name: name.to_string(),
148 kind: SymbolKind::Module,
149 signature: format!("(defpackage {})", name),
150 docstring: None,
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 for prefix in &["(defclass ", "(defstruct "] {
162 if text.starts_with(prefix) {
163 let name = text[prefix.len()..].split_whitespace().next()?;
164 return Some(Symbol {
165 name: name.to_string(),
166 kind: SymbolKind::Class,
167 signature: format!("{}{}", prefix.trim_start_matches('('), 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
180 None
181 }
182
183 fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
184 self.extract_container(node, content)
185 }
186
187 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
188 let text = &content[node.byte_range()];
190 if let Some(start) = text.find('"')
192 && let Some(end) = text[start + 1..].find('"')
193 {
194 return Some(text[start + 1..start + 1 + end].to_string());
195 }
196 None
197 }
198
199 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
200 Vec::new()
201 }
202
203 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
204 if node.kind() != "list_lit" {
205 return Vec::new();
206 }
207
208 let text = &content[node.byte_range()];
209 let line = node.start_position().row + 1;
210
211 for prefix in &["(require ", "(use-package ", "(ql:quickload "] {
212 if text.starts_with(prefix) {
213 let module = text[prefix.len()..]
214 .split(|c: char| c.is_whitespace() || c == ')')
215 .next()
216 .map(|s| s.trim_matches(|c| c == '\'' || c == ':' || c == '"'))
217 .unwrap_or("")
218 .to_string();
219
220 if !module.is_empty() {
221 return vec![Import {
222 module,
223 names: Vec::new(),
224 alias: None,
225 is_wildcard: false,
226 is_relative: false,
227 line,
228 }];
229 }
230 }
231 }
232
233 Vec::new()
234 }
235
236 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
237 let names_to_use: Vec<&str> = names
239 .map(|n| n.to_vec())
240 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
241 if names_to_use.is_empty() {
242 format!("(use-package :{})", import.module)
243 } else {
244 let symbols: Vec<String> = names_to_use.iter().map(|n| format!("#:{}", n)).collect();
245 format!(
246 "(use-package :{} (:import-from {}))",
247 import.module,
248 symbols.join(" ")
249 )
250 }
251 }
252
253 fn is_public(&self, _node: &Node, _content: &str) -> bool {
254 true
255 }
256 fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
257 Visibility::Public
258 }
259
260 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
261 let name = symbol.name.as_str();
262 match symbol.kind {
263 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
264 crate::SymbolKind::Module => name == "tests" || name == "test",
265 _ => false,
266 }
267 }
268
269 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
270 None
271 }
272
273 fn container_body<'a>(&self, _node: &'a Node<'a>) -> Option<Node<'a>> {
274 None
275 }
276 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
277 false
278 }
279 fn node_name<'a>(&self, _node: &Node, _content: &'a str) -> Option<&'a str> {
280 None
281 }
282
283 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
284 let ext = path.extension()?.to_str()?;
285 if !["lisp", "lsp", "cl"].contains(&ext) {
286 return None;
287 }
288 let stem = path.file_stem()?.to_str()?;
289 Some(stem.to_string())
290 }
291
292 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
293 vec![
294 format!("{}.lisp", module),
295 format!("{}.lsp", module),
296 format!("{}.cl", module),
297 ]
298 }
299
300 fn lang_key(&self) -> &'static str {
301 "commonlisp"
302 }
303
304 fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
305 matches!(
306 import_name.to_lowercase().as_str(),
307 "cl" | "common-lisp" | "asdf" | "uiop" | "alexandria" | "cl-ppcre"
308 )
309 }
310
311 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
312 None
313 }
314
315 fn resolve_local_import(
316 &self,
317 import: &str,
318 current_file: &Path,
319 _project_root: &Path,
320 ) -> Option<PathBuf> {
321 let dir = current_file.parent()?;
322 for ext in &["lisp", "lsp", "cl"] {
323 let full = dir.join(format!("{}.{}", import, ext));
324 if full.is_file() {
325 return Some(full);
326 }
327 }
328 None
329 }
330
331 fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
332 None
333 }
334
335 fn get_version(&self, project_root: &Path) -> Option<String> {
336 for entry in std::fs::read_dir(project_root).ok()? {
337 let entry = entry.ok()?;
338 if entry.path().extension().map_or(false, |e| e == "asd") {
339 return Some("ASDF".to_string());
340 }
341 }
342 None
343 }
344
345 fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
346 if let Some(home) = std::env::var_os("HOME") {
347 let quicklisp = PathBuf::from(home).join("quicklisp/dists");
348 if quicklisp.is_dir() {
349 return Some(quicklisp);
350 }
351 }
352 None
353 }
354
355 fn indexable_extensions(&self) -> &'static [&'static str] {
356 &["lisp", "lsp", "cl"]
357 }
358 fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
359 Vec::new()
360 }
361
362 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
363 use crate::traits::{has_extension, skip_dotfiles};
364 if skip_dotfiles(name) {
365 return true;
366 }
367 !is_dir && !has_extension(name, self.indexable_extensions())
368 }
369
370 fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
371 Vec::new()
372 }
373
374 fn package_module_name(&self, entry_name: &str) -> String {
375 entry_name
376 .strip_suffix(".lisp")
377 .or_else(|| entry_name.strip_suffix(".lsp"))
378 .or_else(|| entry_name.strip_suffix(".cl"))
379 .unwrap_or(entry_name)
380 .to_string()
381 }
382
383 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
384 if path.is_file() {
385 Some(path.to_path_buf())
386 } else {
387 None
388 }
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395 use crate::validate_unused_kinds_audit;
396
397 #[test]
398 fn unused_node_kinds_audit() {
399 #[rustfmt::skip]
400 let documented_unused: &[&str] = &[
401 "accumulation_clause", "condition_clause", "do_clause", "for_clause",
403 "for_clause_word", "loop_clause", "loop_macro", "repeat_clause",
404 "termination_clause", "while_clause", "with_clause",
405 "format_directive_type", "format_modifiers", "format_prefix_parameters",
407 "format_specifier",
408 "block_comment",
410 ];
411 validate_unused_kinds_audit(&CommonLisp, documented_unused)
412 .expect("Common Lisp unused node kinds audit failed");
413 }
414}