normalize_languages/
elixir.rs1use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
4use tree_sitter::Node;
5
6pub struct Elixir;
8
9impl Language for Elixir {
10 fn name(&self) -> &'static str {
11 "Elixir"
12 }
13 fn extensions(&self) -> &'static [&'static str] {
14 &["ex", "exs"]
15 }
16 fn grammar_name(&self) -> &'static str {
17 "elixir"
18 }
19
20 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
21 Some(self)
22 }
23
24 fn signature_suffix(&self) -> &'static str {
25 " end"
26 }
27
28 fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
29 let mut attrs = Vec::new();
30 let mut prev = node.prev_sibling();
31 while let Some(sibling) = prev {
32 if sibling.kind() == "unary_operator" {
33 let text = content[sibling.byte_range()].trim();
34 if text.starts_with('@')
35 && !text.starts_with("@doc")
36 && !text.starts_with("@moduledoc")
37 {
38 attrs.insert(0, text.to_string());
39 }
40 prev = sibling.prev_sibling();
41 } else {
42 break;
43 }
44 }
45 attrs
46 }
47
48 fn build_signature(&self, node: &Node, content: &str) -> String {
49 if node.kind() != "call" {
50 let text = &content[node.byte_range()];
51 return text.lines().next().unwrap_or(text).trim().to_string();
52 }
53 let text = &content[node.byte_range()];
54 if text.starts_with("defmodule ")
55 && let Some(name) = self.extract_module_name(node, content)
56 {
57 return format!("defmodule {}", name);
58 }
59 let first_line = text.lines().next().unwrap_or(text).trim();
61 first_line.trim_end_matches(" do").to_string()
62 }
63
64 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
65 if node.kind() != "call" {
66 return Vec::new();
67 }
68
69 let text = &content[node.byte_range()];
70 let line = node.start_position().row + 1;
71
72 for keyword in &["import ", "alias ", "require ", "use "] {
74 if let Some(stripped) = text.strip_prefix(keyword) {
75 let rest = stripped.trim();
76 let module = rest
77 .split(|c: char| c.is_whitespace() || c == ',')
78 .next()
79 .unwrap_or(rest)
80 .to_string();
81
82 if !module.is_empty() {
83 return vec![Import {
84 module,
85 names: Vec::new(),
86 alias: None,
87 is_wildcard: false,
88 is_relative: false,
89 line,
90 }];
91 }
92 }
93 }
94
95 Vec::new()
96 }
97
98 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
99 let names_to_use: Vec<&str> = names
101 .map(|n| n.to_vec())
102 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
103 if names_to_use.is_empty() {
104 format!("import {}", import.module)
105 } else {
106 format!(
107 "import {}, only: [{}]",
108 import.module,
109 names_to_use.join(", ")
110 )
111 }
112 }
113
114 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
115 if node.kind() != "call" {
116 return Visibility::Private;
117 }
118 let text = &content[node.byte_range()];
119 let is_public = (text.starts_with("def ") && !text.starts_with("defp"))
120 || (text.starts_with("defmacro ") && !text.starts_with("defmacrop"))
121 || text.starts_with("defmodule ");
122 if is_public {
123 Visibility::Public
124 } else {
125 Visibility::Private
126 }
127 }
128
129 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
130 let name = symbol.name.as_str();
131 match symbol.kind {
132 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
133 crate::SymbolKind::Module => name == "tests" || name == "test",
134 _ => false,
135 }
136 }
137
138 fn test_file_globs(&self) -> &'static [&'static str] {
139 &["**/test/**/*.exs", "**/*_test.exs"]
140 }
141
142 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
143 let mut cursor = node.walk();
145 node.children(&mut cursor)
146 .find(|&child| child.kind() == "do_block")
147 }
148
149 fn analyze_container_body(
150 &self,
151 body_node: &Node,
152 content: &str,
153 inner_indent: &str,
154 ) -> Option<ContainerBody> {
155 crate::body::analyze_do_end_body(body_node, content, inner_indent)
156 }
157
158 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
159 if node.kind() != "call" {
160 return node
162 .child_by_field_name("name")
163 .map(|n| &content[n.byte_range()]);
164 }
165 let mut cursor = node.walk();
169 for child in node.children(&mut cursor) {
170 if child.kind() == "arguments" {
171 let mut arg_cursor = child.walk();
172 for arg in child.children(&mut arg_cursor) {
173 match arg.kind() {
174 "alias" => return Some(&content[arg.byte_range()]),
176 "call" => {
178 if let Some(target) = arg.child_by_field_name("target") {
179 return Some(&content[target.byte_range()]);
180 }
181 }
182 "identifier" => return Some(&content[arg.byte_range()]),
184 _ => {}
185 }
186 }
187 }
188 }
189 None
190 }
191}
192
193impl LanguageSymbols for Elixir {}
194
195impl Elixir {
196 fn extract_module_name(&self, node: &Node, content: &str) -> Option<String> {
197 let mut cursor = node.walk();
199 for child in node.children(&mut cursor) {
200 if child.kind() == "alias" || child.kind() == "atom" {
201 let text = &content[child.byte_range()];
202 if !text.is_empty() && text != "defmodule" {
203 return Some(text.to_string());
204 }
205 }
206 }
207 None
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use crate::validate_unused_kinds_audit;
215
216 #[test]
217 fn unused_node_kinds_audit() {
218 #[rustfmt::skip]
219 let documented_unused: &[&str] = &[
220 "after_block", "block", "body", "catch_block", "charlist",
221 "else_block", "interpolation", "operator_identifier",
222 "rescue_block", "sigil_modifiers", "stab_clause", "struct",
223 "unary_operator",
224 "binary_operator",
226 "do_block",
227 "anonymous_function",
228 ];
229 validate_unused_kinds_audit(&Elixir, documented_unused)
230 .expect("Elixir unused node kinds audit failed");
231 }
232}