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 Lua;
10
11impl Language for Lua {
12 fn name(&self) -> &'static str {
13 "Lua"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["lua"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "lua"
20 }
21
22 fn has_symbols(&self) -> bool {
23 true
24 }
25
26 fn container_kinds(&self) -> &'static [&'static str] {
27 &[] }
29
30 fn function_kinds(&self) -> &'static [&'static str] {
31 &["function_declaration", "function_definition"]
32 }
33
34 fn type_kinds(&self) -> &'static [&'static str] {
35 &[]
36 }
37
38 fn import_kinds(&self) -> &'static [&'static str] {
39 &["function_call"] }
41
42 fn public_symbol_kinds(&self) -> &'static [&'static str] {
43 &["function_declaration", "function_definition"]
44 }
45
46 fn visibility_mechanism(&self) -> VisibilityMechanism {
47 VisibilityMechanism::NamingConvention }
49
50 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
51 if node.kind() == "function_declaration" {
53 if let Some(name) = self.node_name(node, content) {
54 return vec![Export {
55 name: name.to_string(),
56 kind: SymbolKind::Function,
57 line: node.start_position().row + 1,
58 }];
59 }
60 }
61 Vec::new()
62 }
63
64 fn scope_creating_kinds(&self) -> &'static [&'static str] {
65 &[
66 "do_statement",
67 "for_statement",
68 "while_statement",
69 "repeat_statement",
70 ]
71 }
72
73 fn control_flow_kinds(&self) -> &'static [&'static str] {
74 &[
75 "if_statement",
76 "for_statement",
77 "while_statement",
78 "repeat_statement",
79 "return_statement",
80 "break_statement",
81 "goto_statement",
82 ]
83 }
84
85 fn complexity_nodes(&self) -> &'static [&'static str] {
86 &[
87 "if_statement",
88 "elseif_statement",
89 "for_statement",
90 "while_statement",
91 "repeat_statement",
92 "and",
93 "or",
94 ]
95 }
96
97 fn nesting_nodes(&self) -> &'static [&'static str] {
98 &[
99 "if_statement",
100 "for_statement",
101 "while_statement",
102 "repeat_statement",
103 "function_declaration",
104 "function_definition",
105 ]
106 }
107
108 fn signature_suffix(&self) -> &'static str {
109 " end"
110 }
111
112 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
113 let name = self.node_name(node, content)?;
114
115 let params = node
116 .child_by_field_name("parameters")
117 .map(|p| content[p.byte_range()].to_string())
118 .unwrap_or_else(|| "()".to_string());
119
120 let text = &content[node.byte_range()];
122 let is_local = text.trim_start().starts_with("local ");
123 let keyword = if is_local {
124 "local function"
125 } else {
126 "function"
127 };
128 let signature = format!("{} {}{}", keyword, name, params);
129
130 Some(Symbol {
131 name: name.to_string(),
132 kind: SymbolKind::Function,
133 signature,
134 docstring: self.extract_docstring(node, content),
135 attributes: Vec::new(),
136 start_line: node.start_position().row + 1,
137 end_line: node.end_position().row + 1,
138 visibility: if is_local {
139 Visibility::Private
140 } else {
141 Visibility::Public
142 },
143 children: Vec::new(),
144 is_interface_impl: false,
145 implements: Vec::new(),
146 })
147 }
148
149 fn extract_container(&self, _node: &Node, _content: &str) -> Option<Symbol> {
150 None
151 }
152 fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
153 None
154 }
155
156 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
157 let mut prev = node.prev_sibling();
159 while let Some(sibling) = prev {
160 let text = &content[sibling.byte_range()];
161 if sibling.kind() == "comment" {
162 if text.starts_with("---") {
164 let doc = text.strip_prefix("---").unwrap_or(text).trim();
165 if !doc.starts_with('@') {
166 return Some(doc.to_string());
167 }
168 }
169 if text.starts_with("--[[") {
171 let inner = text
172 .strip_prefix("--[[")
173 .unwrap_or(text)
174 .strip_suffix("]]")
175 .unwrap_or(text)
176 .trim();
177 if !inner.is_empty() {
178 return Some(inner.to_string());
179 }
180 }
181 break;
182 }
183 prev = sibling.prev_sibling();
184 }
185 None
186 }
187
188 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
189 Vec::new()
190 }
191
192 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
193 if node.kind() != "function_call" {
195 return Vec::new();
196 }
197
198 let func_name = node
199 .child_by_field_name("name")
200 .map(|n| &content[n.byte_range()]);
201
202 if func_name != Some("require") {
203 return Vec::new();
204 }
205
206 if let Some(args) = node.child_by_field_name("arguments") {
207 let mut cursor = args.walk();
208 for child in args.children(&mut cursor) {
209 if child.kind() == "string" {
210 let module = content[child.byte_range()]
211 .trim_matches(|c| c == '"' || c == '\'' || c == '[' || c == ']')
212 .to_string();
213 return vec![Import {
214 module,
215 names: Vec::new(),
216 alias: None,
217 is_wildcard: false,
218 is_relative: false,
219 line: node.start_position().row + 1,
220 }];
221 }
222 }
223 }
224
225 Vec::new()
226 }
227
228 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
229 format!("require(\"{}\")", import.module)
231 }
232
233 fn is_public(&self, node: &Node, content: &str) -> bool {
234 let text = &content[node.byte_range()];
235 !text.trim_start().starts_with("local ")
236 }
237
238 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
239 let text = &content[node.byte_range()];
240 if text.trim_start().starts_with("local ") {
241 Visibility::Private
242 } else {
243 Visibility::Public
244 }
245 }
246
247 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
248 let name = symbol.name.as_str();
249 match symbol.kind {
250 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
251 crate::SymbolKind::Module => name == "tests" || name == "test",
252 _ => false,
253 }
254 }
255
256 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
257 None
258 }
259
260 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
261 node.child_by_field_name("body")
262 }
263
264 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
265 false
266 }
267
268 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
269 node.child_by_field_name("name")
270 .map(|n| &content[n.byte_range()])
271 }
272
273 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
274 let ext = path.extension()?.to_str()?;
275 if ext != "lua" {
276 return None;
277 }
278 let stem = path.file_stem()?.to_str()?;
279 Some(stem.to_string())
280 }
281
282 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
283 let path = module.replace('.', "/");
284 vec![format!("{}.lua", path), format!("{}/init.lua", path)]
285 }
286
287 fn lang_key(&self) -> &'static str {
288 "lua"
289 }
290
291 fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
292 matches!(
294 import_name,
295 "string"
296 | "table"
297 | "math"
298 | "io"
299 | "os"
300 | "debug"
301 | "coroutine"
302 | "package"
303 | "utf8"
304 | "bit32"
305 )
306 }
307
308 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
309 None
310 }
311
312 fn resolve_local_import(
313 &self,
314 import: &str,
315 current_file: &Path,
316 project_root: &Path,
317 ) -> Option<PathBuf> {
318 let path_part = import.replace('.', "/");
319 let paths = [
320 format!("{}.lua", path_part),
321 format!("{}/init.lua", path_part),
322 ];
323
324 if let Some(dir) = current_file.parent() {
326 for p in &paths {
327 let full = dir.join(p);
328 if full.is_file() {
329 return Some(full);
330 }
331 }
332 }
333
334 for p in &paths {
336 let full = project_root.join(p);
337 if full.is_file() {
338 return Some(full);
339 }
340 }
341
342 None
343 }
344
345 fn resolve_external_import(
346 &self,
347 _import_name: &str,
348 _project_root: &Path,
349 ) -> Option<ResolvedPackage> {
350 None
352 }
353
354 fn get_version(&self, _project_root: &Path) -> Option<String> {
355 None
356 }
357 fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
358 None
359 }
360 fn indexable_extensions(&self) -> &'static [&'static str] {
361 &["lua"]
362 }
363 fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
364 Vec::new()
365 }
366
367 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
368 use crate::traits::{has_extension, skip_dotfiles};
369 if skip_dotfiles(name) {
370 return true;
371 }
372 !is_dir && !has_extension(name, self.indexable_extensions())
373 }
374
375 fn discover_packages(&self, _source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
376 Vec::new()
377 }
378
379 fn package_module_name(&self, entry_name: &str) -> String {
380 entry_name
381 .strip_suffix(".lua")
382 .unwrap_or(entry_name)
383 .to_string()
384 }
385
386 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
387 if path.is_file() {
388 return Some(path.to_path_buf());
389 }
390 let init = path.join("init.lua");
391 if init.is_file() {
392 return Some(init);
393 }
394 None
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use crate::validate_unused_kinds_audit;
402
403 #[test]
404 fn unused_node_kinds_audit() {
405 #[rustfmt::skip]
406 let documented_unused: &[&str] = &[
407 "assignment_statement", "binary_expression", "block",
408 "bracket_index_expression", "dot_index_expression", "else_statement",
409 "empty_statement", "expression_list", "for_generic_clause",
410 "for_numeric_clause", "identifier", "label_statement",
411 "method_index_expression", "parenthesized_expression", "table_constructor",
412 "unary_expression", "vararg_expression", "variable_declaration",
413 ];
414 validate_unused_kinds_audit(&Lua, documented_unused)
415 .expect("Lua unused node kinds audit failed");
416 }
417}