Skip to main content

normalize_languages/
agda.rs

1//! Agda 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/// Agda language support.
9pub struct Agda;
10
11impl Language for Agda {
12    fn name(&self) -> &'static str {
13        "Agda"
14    }
15    fn extensions(&self) -> &'static [&'static str] {
16        &["agda"]
17    }
18    fn grammar_name(&self) -> &'static str {
19        "agda"
20    }
21
22    fn has_symbols(&self) -> bool {
23        true
24    }
25
26    fn container_kinds(&self) -> &'static [&'static str] {
27        &["module", "record"]
28    }
29
30    fn function_kinds(&self) -> &'static [&'static str] {
31        &["function", "signature"]
32    }
33
34    fn type_kinds(&self) -> &'static [&'static str] {
35        &["data", "record"]
36    }
37
38    fn import_kinds(&self) -> &'static [&'static str] {
39        &["import", "open"]
40    }
41
42    fn public_symbol_kinds(&self) -> &'static [&'static str] {
43        &["module", "data", "record", "function"]
44    }
45
46    fn visibility_mechanism(&self) -> VisibilityMechanism {
47        VisibilityMechanism::AllPublic
48    }
49
50    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
51        match node.kind() {
52            "module" => {
53                if let Some(name) = self.node_name(node, content) {
54                    return vec![Export {
55                        name: name.to_string(),
56                        kind: SymbolKind::Module,
57                        line: node.start_position().row + 1,
58                    }];
59                }
60            }
61            "data" | "record" => {
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            "function" | "signature" => {
71                if let Some(name) = self.node_name(node, content) {
72                    return vec![Export {
73                        name: name.to_string(),
74                        kind: SymbolKind::Function,
75                        line: node.start_position().row + 1,
76                    }];
77                }
78            }
79            _ => {}
80        }
81        Vec::new()
82    }
83
84    fn scope_creating_kinds(&self) -> &'static [&'static str] {
85        &["module", "where"]
86    }
87
88    fn control_flow_kinds(&self) -> &'static [&'static str] {
89        &[] // Agda is purely functional
90    }
91
92    fn complexity_nodes(&self) -> &'static [&'static str] {
93        &["function", "lambda_clause"] // Pattern matching
94    }
95
96    fn nesting_nodes(&self) -> &'static [&'static str] {
97        &["module", "where", "do"]
98    }
99
100    fn signature_suffix(&self) -> &'static str {
101        ""
102    }
103
104    fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
105        if node.kind() != "function" && node.kind() != "signature" {
106            return None;
107        }
108
109        let name = self.node_name(node, content)?;
110        let text = &content[node.byte_range()];
111        let first_line = text.lines().next().unwrap_or(text);
112
113        Some(Symbol {
114            name: name.to_string(),
115            kind: SymbolKind::Function,
116            signature: first_line.trim().to_string(),
117            docstring: None,
118            attributes: Vec::new(),
119            start_line: node.start_position().row + 1,
120            end_line: node.end_position().row + 1,
121            visibility: Visibility::Public,
122            children: Vec::new(),
123            is_interface_impl: false,
124            implements: Vec::new(),
125        })
126    }
127
128    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
129        if node.kind() != "module" {
130            return None;
131        }
132
133        let name = self.node_name(node, content)?;
134        let text = &content[node.byte_range()];
135        let first_line = text.lines().next().unwrap_or(text);
136
137        Some(Symbol {
138            name: name.to_string(),
139            kind: SymbolKind::Module,
140            signature: first_line.trim().to_string(),
141            docstring: None,
142            attributes: Vec::new(),
143            start_line: node.start_position().row + 1,
144            end_line: node.end_position().row + 1,
145            visibility: Visibility::Public,
146            children: Vec::new(),
147            is_interface_impl: false,
148            implements: Vec::new(),
149        })
150    }
151
152    fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
153        match node.kind() {
154            "data" | "record" => {
155                let name = self.node_name(node, content)?;
156                let text = &content[node.byte_range()];
157                let first_line = text.lines().next().unwrap_or(text);
158
159                Some(Symbol {
160                    name: name.to_string(),
161                    kind: SymbolKind::Type,
162                    signature: first_line.trim().to_string(),
163                    docstring: None,
164                    attributes: Vec::new(),
165                    start_line: node.start_position().row + 1,
166                    end_line: node.end_position().row + 1,
167                    visibility: Visibility::Public,
168                    children: Vec::new(),
169                    is_interface_impl: false,
170                    implements: Vec::new(),
171                })
172            }
173            _ => None,
174        }
175    }
176
177    fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
178        None
179    }
180
181    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
182        Vec::new()
183    }
184
185    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
186        match node.kind() {
187            "import" | "open" => {
188                let text = &content[node.byte_range()];
189                vec![Import {
190                    module: text.trim().to_string(),
191                    names: Vec::new(),
192                    alias: None,
193                    is_wildcard: node.kind() == "open",
194                    is_relative: false,
195                    line: node.start_position().row + 1,
196                }]
197            }
198            _ => Vec::new(),
199        }
200    }
201
202    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
203        // Agda: open import Module or import Module using (a; b; c)
204        let names_to_use: Vec<&str> = names
205            .map(|n| n.to_vec())
206            .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
207        if names_to_use.is_empty() {
208            format!("open import {}", import.module)
209        } else {
210            format!(
211                "import {} using ({})",
212                import.module,
213                names_to_use.join("; ")
214            )
215        }
216    }
217
218    fn is_public(&self, _node: &Node, _content: &str) -> bool {
219        true
220    }
221    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
222        Visibility::Public
223    }
224
225    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
226        let name = symbol.name.as_str();
227        match symbol.kind {
228            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
229            crate::SymbolKind::Module => name == "tests" || name == "test",
230            _ => false,
231        }
232    }
233
234    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
235        None
236    }
237
238    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
239        node.child_by_field_name("declarations")
240    }
241
242    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
243        false
244    }
245
246    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
247        if let Some(name_node) = node.child_by_field_name("name") {
248            return Some(&content[name_node.byte_range()]);
249        }
250        let mut cursor = node.walk();
251        for child in node.children(&mut cursor) {
252            if child.kind() == "id" || child.kind() == "qid" {
253                return Some(&content[child.byte_range()]);
254            }
255        }
256        None
257    }
258
259    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
260        let ext = path.extension()?.to_str()?;
261        if ext != "agda" {
262            return None;
263        }
264        let stem = path.file_stem()?.to_str()?;
265        Some(stem.to_string())
266    }
267
268    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
269        vec![format!("{}.agda", module.replace('.', "/"))]
270    }
271
272    fn lang_key(&self) -> &'static str {
273        "agda"
274    }
275
276    fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
277        import_name.starts_with("Agda.")
278    }
279
280    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
281        None
282    }
283    fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
284        None
285    }
286    fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
287        None
288    }
289    fn get_version(&self, _: &Path) -> Option<String> {
290        None
291    }
292    fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
293        None
294    }
295    fn indexable_extensions(&self) -> &'static [&'static str] {
296        &["agda"]
297    }
298    fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
299        Vec::new()
300    }
301
302    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
303        use crate::traits::{has_extension, skip_dotfiles};
304        if skip_dotfiles(name) {
305            return true;
306        }
307        !is_dir && !has_extension(name, self.indexable_extensions())
308    }
309
310    fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
311        Vec::new()
312    }
313
314    fn package_module_name(&self, entry_name: &str) -> String {
315        entry_name
316            .strip_suffix(".agda")
317            .unwrap_or(entry_name)
318            .to_string()
319    }
320
321    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
322        if path.is_file() {
323            Some(path.to_path_buf())
324        } else {
325            None
326        }
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use crate::validate_unused_kinds_audit;
334
335    #[test]
336    fn unused_node_kinds_audit() {
337        #[rustfmt::skip]
338        let documented_unused: &[&str] = &[
339            // Function and lambda definitions
340            "catchall_pragma", "forall", "function_name", "lambda",
341            "lambda_clause_absurd", "type_signature",
342            // Module-related
343            "import_directive", "module_application", "module_assignment", "module_macro",
344            "module_name",
345            // Record definitions
346            "record_constructor", "record_constructor_instance", "record_declarations_block",
347            // Bindings
348            "typed_binding", "untyped_binding", "with_expressions",
349        ];
350        validate_unused_kinds_audit(&Agda, documented_unused)
351            .expect("Agda unused node kinds audit failed");
352    }
353}