normalize_languages/
lua.rs1use crate::docstring::extract_preceding_prefix_comments;
4use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
5use crate::{Import, Language, LanguageSymbols, Visibility};
6use std::path::Path;
7use tree_sitter::Node;
8
9pub struct Lua;
11
12impl Language for Lua {
13 fn name(&self) -> &'static str {
14 "Lua"
15 }
16 fn extensions(&self) -> &'static [&'static str] {
17 &["lua"]
18 }
19 fn grammar_name(&self) -> &'static str {
20 "lua"
21 }
22
23 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
24 Some(self)
25 }
26
27 fn signature_suffix(&self) -> &'static str {
28 " end"
29 }
30
31 fn build_signature(&self, node: &Node, content: &str) -> String {
32 let name = match self.node_name(node, content) {
33 Some(n) => n,
34 None => {
35 let text = &content[node.byte_range()];
36 return text.lines().next().unwrap_or(text).trim().to_string();
37 }
38 };
39 let params = node
40 .child_by_field_name("parameters")
41 .map(|p| content[p.byte_range()].to_string())
42 .unwrap_or_else(|| "()".to_string());
43 let text = &content[node.byte_range()];
44 let is_local = text.trim_start().starts_with("local ");
45 let keyword = if is_local {
46 "local function"
47 } else {
48 "function"
49 };
50 format!("{} {}{}", keyword, name, params)
51 }
52
53 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
54 if node.kind() != "function_call" {
56 return Vec::new();
57 }
58
59 let func_name = node
60 .child_by_field_name("name")
61 .map(|n| &content[n.byte_range()]);
62
63 if func_name != Some("require") {
64 return Vec::new();
65 }
66
67 if let Some(args) = node.child_by_field_name("arguments") {
68 let mut cursor = args.walk();
69 for child in args.children(&mut cursor) {
70 if child.kind() == "string" {
71 let module = content[child.byte_range()]
72 .trim_matches(|c| c == '"' || c == '\'' || c == '[' || c == ']')
73 .to_string();
74 return vec![Import {
75 module,
76 names: Vec::new(),
77 alias: None,
78 is_wildcard: false,
79 is_relative: false,
80 line: node.start_position().row + 1,
81 }];
82 }
83 }
84 }
85
86 Vec::new()
87 }
88
89 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
90 format!("require(\"{}\")", import.module)
92 }
93
94 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
95 let text = &content[node.byte_range()];
96 if text.trim_start().starts_with("local ") {
97 Visibility::Private
98 } else {
99 Visibility::Public
100 }
101 }
102
103 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
104 let name = symbol.name.as_str();
105 match symbol.kind {
106 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
107 crate::SymbolKind::Module => name == "tests" || name == "test",
108 _ => false,
109 }
110 }
111
112 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
113 extract_preceding_prefix_comments(node, content, "---")
115 }
116
117 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
118 node.child_by_field_name("body")
119 }
120
121 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
122 static RESOLVER: LuaModuleResolver = LuaModuleResolver;
123 Some(&RESOLVER)
124 }
125}
126
127impl LanguageSymbols for Lua {}
128
129pub struct LuaModuleResolver;
137
138impl ModuleResolver for LuaModuleResolver {
139 fn workspace_config(&self, root: &Path) -> ResolverConfig {
140 ResolverConfig {
141 workspace_root: root.to_path_buf(),
142 path_mappings: Vec::new(),
143 search_roots: vec![root.to_path_buf()],
144 }
145 }
146
147 fn module_of_file(&self, root: &Path, file: &Path, _cfg: &ResolverConfig) -> Vec<ModuleId> {
148 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
149 if ext != "lua" {
150 return Vec::new();
151 }
152 if let Ok(rel) = file.strip_prefix(root) {
153 let rel_str = rel.to_str().unwrap_or("");
154 let module = if rel_str.ends_with("/init.lua") || rel_str == "init.lua" {
156 rel_str
157 .trim_end_matches("/init.lua")
158 .trim_end_matches("init.lua")
159 .trim_end_matches('/')
160 .replace('/', ".")
161 } else {
162 rel_str.trim_end_matches(".lua").replace('/', ".")
163 };
164 if !module.is_empty() {
165 return vec![ModuleId {
166 canonical_path: module,
167 }];
168 }
169 }
170 Vec::new()
171 }
172
173 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
174 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
175 if ext != "lua" {
176 return Resolution::NotApplicable;
177 }
178 let raw = &spec.raw;
179 let path_part = raw.replace('.', "/");
180 let exported_name = raw.rsplit('.').next().unwrap_or(raw).to_string();
181
182 let direct = cfg.workspace_root.join(format!("{}.lua", path_part));
183 if direct.exists() {
184 return Resolution::Resolved(direct, exported_name);
185 }
186 let init = cfg.workspace_root.join(format!("{}/init.lua", path_part));
187 if init.exists() {
188 return Resolution::Resolved(init, exported_name);
189 }
190 Resolution::NotFound
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::validate_unused_kinds_audit;
198
199 #[test]
200 fn unused_node_kinds_audit() {
201 #[rustfmt::skip]
202 let documented_unused: &[&str] = &[ "binary_expression", "block",
203 "bracket_index_expression", "else_statement",
204 "empty_statement", "for_generic_clause",
205 "for_numeric_clause", "identifier", "label_statement", "parenthesized_expression", "table_constructor",
206 "unary_expression", "vararg_expression", "variable_declaration",
207 "return_statement",
209 "while_statement",
210 "elseif_statement",
211 "for_statement",
212 "goto_statement",
213 "do_statement",
214 "if_statement",
215 "break_statement",
216 "repeat_statement",
217 "function_call",
218 ];
219 validate_unused_kinds_audit(&Lua, documented_unused)
220 .expect("Lua unused node kinds audit failed");
221 }
222}