Skip to main content

normalize_languages/
clojure.rs

1//! Clojure language support.
2
3use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
4use tree_sitter::Node;
5
6/// Clojure language support.
7pub struct Clojure;
8
9impl Language for Clojure {
10    fn name(&self) -> &'static str {
11        "Clojure"
12    }
13    fn extensions(&self) -> &'static [&'static str] {
14        &["clj", "cljs", "cljc", "edn"]
15    }
16    fn grammar_name(&self) -> &'static str {
17        "clojure"
18    }
19
20    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
21        Some(self)
22    }
23
24    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
25        if node.kind() != "list_lit" {
26            return Vec::new();
27        }
28
29        let (form, _) = match self.extract_def_form(node, content) {
30            Some(info) => info,
31            None => return Vec::new(),
32        };
33
34        if form != "require" && form != "use" && form != "import" {
35            return Vec::new();
36        }
37
38        // Basic extraction - just note the require/import exists
39        vec![Import {
40            module: form,
41            names: Vec::new(),
42            alias: None,
43            is_wildcard: false,
44            is_relative: false,
45            line: node.start_position().row + 1,
46        }]
47    }
48
49    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
50        // Clojure: (require '[namespace]) or (require '[namespace :refer [a b c]])
51        let names_to_use: Vec<&str> = names
52            .map(|n| n.to_vec())
53            .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
54        if names_to_use.is_empty() {
55            format!("(require '[{}])", import.module)
56        } else {
57            format!(
58                "(require '[{} :refer [{}]])",
59                import.module,
60                names_to_use.join(" ")
61            )
62        }
63    }
64
65    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
66        if let Some((form, _)) = self.extract_def_form(node, content) {
67            if form.ends_with('-') {
68                Visibility::Private
69            } else {
70                Visibility::Public
71            }
72        } else {
73            Visibility::Public
74        }
75    }
76
77    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
78        let name = symbol.name.as_str();
79        match symbol.kind {
80            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
81            crate::SymbolKind::Module => name == "tests" || name == "test",
82            _ => false,
83        }
84    }
85
86    fn test_file_globs(&self) -> &'static [&'static str] {
87        &["**/*_test.clj", "**/*_test.cljs", "**/*_test.cljc"]
88    }
89
90    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
91        // list_lit is itself "( ... )" — use node directly for paren analysis
92        Some(*node)
93    }
94    fn analyze_container_body(
95        &self,
96        body_node: &Node,
97        content: &str,
98        inner_indent: &str,
99    ) -> Option<ContainerBody> {
100        crate::body::analyze_paren_body(body_node, content, inner_indent)
101    }
102
103    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
104        // list_lit captured by @definition.* — the name is in the second sym_lit child
105        // (first sym_lit is the form: defn, defrecord, ns, etc.)
106        if node.kind() != "list_lit" {
107            return node
108                .child_by_field_name("name")
109                .map(|n| &content[n.byte_range()]);
110        }
111        let mut cursor = node.walk();
112        let mut seen_form = false;
113        for child in node.children(&mut cursor) {
114            if child.kind() == "sym_lit" {
115                if !seen_form {
116                    seen_form = true;
117                } else {
118                    return Some(&content[child.byte_range()]);
119                }
120            }
121        }
122        None
123    }
124}
125
126impl LanguageSymbols for Clojure {}
127
128impl Clojure {
129    /// Extract the form name and symbol name from a list like (defn foo ...)
130    fn extract_def_form(&self, node: &Node, content: &str) -> Option<(String, String)> {
131        let mut cursor = node.walk();
132        let mut form = None;
133        let mut name = None;
134
135        for child in node.children(&mut cursor) {
136            match child.kind() {
137                "sym_lit" if form.is_none() => {
138                    form = Some(content[child.byte_range()].to_string());
139                }
140                "sym_lit" if form.is_some() && name.is_none() => {
141                    name = Some(content[child.byte_range()].to_string());
142                    break;
143                }
144                _ => {}
145            }
146        }
147
148        Some((form?, name?))
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::validate_unused_kinds_audit;
156
157    #[test]
158    fn unused_node_kinds_audit() {
159        #[rustfmt::skip]
160        let documented_unused: &[&str] = &[];
161        validate_unused_kinds_audit(&Clojure, documented_unused)
162            .expect("Clojure unused node kinds audit failed");
163    }
164}