Skip to main content

normalize_languages/
idris.rs

1//! Idris language support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8/// Idris language support.
9pub struct Idris;
10
11impl Language for Idris {
12    fn name(&self) -> &'static str {
13        "Idris"
14    }
15    fn extensions(&self) -> &'static [&'static str] {
16        &["idr", "lidr"]
17    }
18    fn grammar_name(&self) -> &'static str {
19        "idris"
20    }
21
22    fn has_symbols(&self) -> bool {
23        true
24    }
25
26    fn container_kinds(&self) -> &'static [&'static str] {
27        &["data", "record", "interface"]
28    }
29
30    fn function_kinds(&self) -> &'static [&'static str] {
31        &["function", "signature"]
32    }
33
34    fn type_kinds(&self) -> &'static [&'static str] {
35        &[] // Idris grammar doesn't have type alias node
36    }
37
38    fn import_kinds(&self) -> &'static [&'static str] {
39        &["import"]
40    }
41
42    fn public_symbol_kinds(&self) -> &'static [&'static str] {
43        &["function", "data", "record"]
44    }
45
46    fn visibility_mechanism(&self) -> VisibilityMechanism {
47        VisibilityMechanism::ExplicitExport
48    }
49
50    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
51        match node.kind() {
52            "function" | "signature" => {
53                if let Some(name) = self.node_name(node, content) {
54                    return vec![Export {
55                        name: name.to_string(),
56                        kind: SymbolKind::Function,
57                        line: node.start_position().row + 1,
58                    }];
59                }
60            }
61            "data" | "record" | "interface" => {
62                if let Some(name) = self.node_name(node, content) {
63                    return vec![Export {
64                        name: name.to_string(),
65                        kind: SymbolKind::Type,
66                        line: node.start_position().row + 1,
67                    }];
68                }
69            }
70            _ => {}
71        }
72        Vec::new()
73    }
74
75    fn scope_creating_kinds(&self) -> &'static [&'static str] {
76        &["function", "where", "exp_let_in", "exp_do"]
77    }
78
79    fn control_flow_kinds(&self) -> &'static [&'static str] {
80        &["exp_if", "exp_case"]
81    }
82
83    fn complexity_nodes(&self) -> &'static [&'static str] {
84        &["exp_if", "exp_case", "alt"]
85    }
86
87    fn nesting_nodes(&self) -> &'static [&'static str] {
88        &["exp_if", "exp_case", "exp_do", "exp_let_in"]
89    }
90
91    fn signature_suffix(&self) -> &'static str {
92        ""
93    }
94
95    fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
96        match node.kind() {
97            "function" | "signature" => {
98                let name = self.node_name(node, content)?;
99                let text = &content[node.byte_range()];
100                let first_line = text.lines().next().unwrap_or(text);
101
102                Some(Symbol {
103                    name: name.to_string(),
104                    kind: SymbolKind::Function,
105                    signature: first_line.trim().to_string(),
106                    docstring: None,
107                    attributes: Vec::new(),
108                    start_line: node.start_position().row + 1,
109                    end_line: node.end_position().row + 1,
110                    visibility: Visibility::Public,
111                    children: Vec::new(),
112                    is_interface_impl: false,
113                    implements: Vec::new(),
114                })
115            }
116            _ => None,
117        }
118    }
119
120    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
121        match node.kind() {
122            "data" | "record" | "interface" => {
123                let name = self.node_name(node, content)?;
124                let text = &content[node.byte_range()];
125                let first_line = text.lines().next().unwrap_or(text);
126
127                Some(Symbol {
128                    name: name.to_string(),
129                    kind: SymbolKind::Type,
130                    signature: first_line.trim().to_string(),
131                    docstring: None,
132                    attributes: Vec::new(),
133                    start_line: node.start_position().row + 1,
134                    end_line: node.end_position().row + 1,
135                    visibility: Visibility::Public,
136                    children: Vec::new(),
137                    is_interface_impl: false,
138                    implements: Vec::new(),
139                })
140            }
141            _ => None,
142        }
143    }
144
145    fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
146        None // Type extraction handled by container for data/record
147    }
148
149    fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
150        None
151    }
152
153    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
154        Vec::new()
155    }
156
157    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
158        if node.kind() != "import" {
159            return Vec::new();
160        }
161
162        let text = &content[node.byte_range()];
163        vec![Import {
164            module: text.trim().to_string(),
165            names: Vec::new(),
166            alias: None,
167            is_wildcard: false,
168            is_relative: false,
169            line: node.start_position().row + 1,
170        }]
171    }
172
173    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
174        // Idris: import Module
175        format!("import {}", import.module)
176    }
177
178    fn is_public(&self, _node: &Node, _content: &str) -> bool {
179        true
180    }
181    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
182        Visibility::Public
183    }
184
185    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
186        let name = symbol.name.as_str();
187        match symbol.kind {
188            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
189            crate::SymbolKind::Module => name == "tests" || name == "test",
190            _ => false,
191        }
192    }
193
194    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
195        None
196    }
197
198    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
199        node.child_by_field_name("body")
200    }
201
202    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
203        false
204    }
205
206    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
207        node.child_by_field_name("name")
208            .map(|n| &content[n.byte_range()])
209    }
210
211    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
212        let ext = path.extension()?.to_str()?;
213        if !["idr", "lidr"].contains(&ext) {
214            return None;
215        }
216        let stem = path.file_stem()?.to_str()?;
217        Some(stem.to_string())
218    }
219
220    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
221        vec![format!("{}.idr", module), format!("{}.lidr", module)]
222    }
223
224    fn lang_key(&self) -> &'static str {
225        "idris"
226    }
227
228    fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
229        import_name.starts_with("Prelude")
230            || import_name.starts_with("Data.")
231            || import_name.starts_with("Control.")
232    }
233
234    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
235        None
236    }
237    fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
238        None
239    }
240    fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
241        None
242    }
243    fn get_version(&self, _: &Path) -> Option<String> {
244        None
245    }
246    fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
247        None
248    }
249    fn indexable_extensions(&self) -> &'static [&'static str] {
250        &["idr", "lidr"]
251    }
252    fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
253        Vec::new()
254    }
255
256    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
257        use crate::traits::{has_extension, skip_dotfiles};
258        if skip_dotfiles(name) {
259            return true;
260        }
261        !is_dir && !has_extension(name, self.indexable_extensions())
262    }
263
264    fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
265        Vec::new()
266    }
267
268    fn package_module_name(&self, entry_name: &str) -> String {
269        entry_name
270            .strip_suffix(".idr")
271            .or_else(|| entry_name.strip_suffix(".lidr"))
272            .unwrap_or(entry_name)
273            .to_string()
274    }
275
276    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
277        if path.is_file() {
278            Some(path.to_path_buf())
279        } else {
280            None
281        }
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use crate::validate_unused_kinds_audit;
289
290    #[test]
291    fn unused_node_kinds_audit() {
292        #[rustfmt::skip]
293        let documented_unused: &[&str] = &[
294            // Expression nodes
295            "exp_else", "exp_with", "exp_lambda", "exp_lambda_case",
296            "exp_list_comprehension", "lambda_exp", "lambda_args",
297            // Type-related
298            "type_signature", "type_parens", "type_braces", "type_var", "forall",
299            // Body nodes
300            "parameters_body", "namespace_body", "mutual_body", "data_body",
301            "record_body", "interface_body", "implementation_body",
302            // Interface and module
303            "interface_head", "interface_name", "module",
304            // Operators
305            "operator", "qualified_operator", "qualified_dot_operators", "dot_operator",
306            "ticked_operator", "tuple_operator",
307            // Qualified names
308            "qualified_loname", "qualified_caname",
309            // Other constructs
310            "constructor", "statement", "declarations",
311            "with", "with_pat", "with_arg",
312            // Pragmas
313            "pragma_export", "pragma_foreign", "pragma_foreign_impl", "pragma_transform",
314        ];
315        validate_unused_kinds_audit(&Idris, documented_unused)
316            .expect("Idris unused node kinds audit failed");
317    }
318}