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 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 &[] }
91
92 fn complexity_nodes(&self) -> &'static [&'static str] {
93 &["function", "lambda_clause"] }
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 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 "catchall_pragma", "forall", "function_name", "lambda",
341 "lambda_clause_absurd", "type_signature",
342 "import_directive", "module_application", "module_assignment", "module_macro",
344 "module_name",
345 "record_constructor", "record_constructor_instance", "record_declarations_block",
347 "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}