1use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
4use crate::{Import, Language, LanguageSymbols};
5use std::path::Path;
6use tree_sitter::Node;
7
8pub struct Erlang;
10
11impl Language for Erlang {
12 fn name(&self) -> &'static str {
13 "Erlang"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["erl", "hrl"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "erlang"
20 }
21
22 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
23 Some(self)
24 }
25
26 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
27 if node.kind() != "module_attribute" {
28 return Vec::new();
29 }
30
31 let text = &content[node.byte_range()];
32 let line = node.start_position().row + 1;
33
34 if text.starts_with("-import(")
36 && let Some(start) = text.find('(')
37 {
38 let rest = &text[start + 1..];
39 if let Some(comma) = rest.find(',') {
40 let module = rest[..comma].trim().to_string();
41 return vec![Import {
42 module,
43 names: Vec::new(),
44 alias: None,
45 is_wildcard: false,
46 is_relative: false,
47 line,
48 }];
49 }
50 }
51
52 if text.starts_with("-include")
54 && let Some(start) = text.find('"')
55 {
56 let rest = &text[start + 1..];
57 if let Some(end) = rest.find('"') {
58 let module = rest[..end].to_string();
59 return vec![Import {
60 module,
61 names: Vec::new(),
62 alias: None,
63 is_wildcard: false,
64 is_relative: text.starts_with("-include("),
65 line,
66 }];
67 }
68 }
69
70 Vec::new()
71 }
72
73 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
74 let names_to_use: Vec<&str> = names
76 .map(|n| n.to_vec())
77 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
78 if names_to_use.is_empty() {
79 format!("-import({}, []).", import.module)
80 } else {
81 format!("-import({}, [{}]).", import.module, names_to_use.join(", "))
82 }
83 }
84
85 fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
86 let mut attrs = Vec::new();
87 let mut prev = node.prev_sibling();
88 while let Some(sibling) = prev {
89 if sibling.kind() == "module_attribute" {
90 let text = content[sibling.byte_range()].trim();
91 if text.starts_with("-spec(")
92 || text.starts_with("-spec ")
93 || text.starts_with("-callback(")
94 || text.starts_with("-deprecated(")
95 || text.starts_with("-deprecated.")
96 {
97 attrs.insert(0, text.to_string());
98 }
99 prev = sibling.prev_sibling();
100 } else if sibling.kind() == "comment" {
101 prev = sibling.prev_sibling();
102 } else {
103 break;
104 }
105 }
106 attrs
107 }
108
109 fn build_signature(&self, node: &Node, content: &str) -> String {
110 if node.kind() == "function_clause"
111 && let Some(name_node) = node.child_by_field_name("name")
112 {
113 let name = &content[name_node.byte_range()];
114 let arity = node
115 .child_by_field_name("arguments")
116 .map(|args| {
117 let mut cursor = args.walk();
118 args.children(&mut cursor).count()
119 })
120 .unwrap_or(0);
121 return format!("{}/{}", name, arity);
122 }
123 let text = &content[node.byte_range()];
124 text.lines().next().unwrap_or(text).trim().to_string()
125 }
126
127 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
128 let name = symbol.name.as_str();
129 match symbol.kind {
130 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
131 crate::SymbolKind::Module => name == "tests" || name == "test",
132 _ => false,
133 }
134 }
135
136 fn test_file_globs(&self) -> &'static [&'static str] {
137 &["**/*_SUITE.erl", "**/*_test.erl", "**/*_tests.erl"]
138 }
139
140 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
141 static RESOLVER: ErlangModuleResolver = ErlangModuleResolver;
142 Some(&RESOLVER)
143 }
144}
145
146impl LanguageSymbols for Erlang {}
147
148pub struct ErlangModuleResolver;
157
158impl ModuleResolver for ErlangModuleResolver {
159 fn workspace_config(&self, root: &Path) -> ResolverConfig {
160 ResolverConfig {
161 workspace_root: root.to_path_buf(),
162 path_mappings: Vec::new(),
163 search_roots: vec![root.join("src"), root.join("lib"), root.to_path_buf()],
164 }
165 }
166
167 fn module_of_file(&self, _root: &Path, file: &Path, _cfg: &ResolverConfig) -> Vec<ModuleId> {
168 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
169 if ext != "erl" && ext != "hrl" {
170 return Vec::new();
171 }
172 if let Some(stem) = file.file_stem().and_then(|s| s.to_str()) {
173 return vec![ModuleId {
174 canonical_path: stem.to_string(),
175 }];
176 }
177 Vec::new()
178 }
179
180 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
181 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
182 if ext != "erl" && ext != "hrl" {
183 return Resolution::NotApplicable;
184 }
185 let raw = &spec.raw;
186
187 if (spec.is_relative || raw.ends_with(".hrl") || raw.ends_with(".erl"))
189 && let Some(parent) = from_file.parent()
190 {
191 let candidate = parent.join(raw);
192 if candidate.exists() {
193 let name = candidate
194 .file_stem()
195 .and_then(|s| s.to_str())
196 .unwrap_or("")
197 .to_string();
198 return Resolution::Resolved(candidate, name);
199 }
200 }
201
202 for search_root in &cfg.search_roots {
203 for ext_try in &["erl", "hrl"] {
204 let candidate = search_root.join(format!("{}.{}", raw, ext_try));
205 if candidate.exists() {
206 return Resolution::Resolved(candidate, raw.to_string());
207 }
208 }
209 }
210 Resolution::NotFound
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::validate_unused_kinds_audit;
218
219 #[test]
220 fn unused_node_kinds_audit() {
221 #[rustfmt::skip]
222 let documented_unused: &[&str] = &[
223 "ann_type", "b_generator", "binary_comprehension", "bit_type_list",
224 "bit_type_unit", "block_expr", "catch_expr", "clause_body",
225 "cond_match_expr", "deprecated_module", "export_attribute",
226 "export_type_attribute", "field_type", "fun_type", "fun_type_sig",
227 "generator", "guard_clause", "import_attribute", "list_comprehension",
228 "map_comprehension", "map_generator", "match_expr", "module",
229 "pp_elif", "pp_else", "pp_endif", "pp_if", "pp_ifdef", "pp_ifndef",
230 "range_type", "remote_module", "replacement_cr_clauses",
231 "replacement_function_clauses", "ssr_definition", "try_after",
232 "try_class", "try_stack", "type_guards", "type_name", "type_sig",
233 "cr_clause",
235 "try_expr",
236 "fun_clause",
237 "if_expr",
238 "if_clause",
239 "case_expr",
240 "catch_clause",
241 ];
242 validate_unused_kinds_audit(&Erlang, documented_unused)
243 .expect("Erlang unused node kinds audit failed");
244 }
245}