Skip to main content

normalize_languages/
lua.rs

1//! Lua language support.
2
3use crate::docstring::extract_preceding_prefix_comments;
4use crate::{Import, Language, LanguageSymbols, Visibility};
5use tree_sitter::Node;
6
7/// Lua language support.
8pub struct Lua;
9
10impl Language for Lua {
11    fn name(&self) -> &'static str {
12        "Lua"
13    }
14    fn extensions(&self) -> &'static [&'static str] {
15        &["lua"]
16    }
17    fn grammar_name(&self) -> &'static str {
18        "lua"
19    }
20
21    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
22        Some(self)
23    }
24
25    fn signature_suffix(&self) -> &'static str {
26        " end"
27    }
28
29    fn build_signature(&self, node: &Node, content: &str) -> String {
30        let name = match self.node_name(node, content) {
31            Some(n) => n,
32            None => {
33                let text = &content[node.byte_range()];
34                return text.lines().next().unwrap_or(text).trim().to_string();
35            }
36        };
37        let params = node
38            .child_by_field_name("parameters")
39            .map(|p| content[p.byte_range()].to_string())
40            .unwrap_or_else(|| "()".to_string());
41        let text = &content[node.byte_range()];
42        let is_local = text.trim_start().starts_with("local ");
43        let keyword = if is_local {
44            "local function"
45        } else {
46            "function"
47        };
48        format!("{} {}{}", keyword, name, params)
49    }
50
51    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
52        // Look for require("module") calls
53        if node.kind() != "function_call" {
54            return Vec::new();
55        }
56
57        let func_name = node
58            .child_by_field_name("name")
59            .map(|n| &content[n.byte_range()]);
60
61        if func_name != Some("require") {
62            return Vec::new();
63        }
64
65        if let Some(args) = node.child_by_field_name("arguments") {
66            let mut cursor = args.walk();
67            for child in args.children(&mut cursor) {
68                if child.kind() == "string" {
69                    let module = content[child.byte_range()]
70                        .trim_matches(|c| c == '"' || c == '\'' || c == '[' || c == ']')
71                        .to_string();
72                    return vec![Import {
73                        module,
74                        names: Vec::new(),
75                        alias: None,
76                        is_wildcard: false,
77                        is_relative: false,
78                        line: node.start_position().row + 1,
79                    }];
80                }
81            }
82        }
83
84        Vec::new()
85    }
86
87    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
88        // Lua: require("module")
89        format!("require(\"{}\")", import.module)
90    }
91
92    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
93        let text = &content[node.byte_range()];
94        if text.trim_start().starts_with("local ") {
95            Visibility::Private
96        } else {
97            Visibility::Public
98        }
99    }
100
101    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
102        let name = symbol.name.as_str();
103        match symbol.kind {
104            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
105            crate::SymbolKind::Module => name == "tests" || name == "test",
106            _ => false,
107        }
108    }
109
110    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
111        // LuaDoc comments start with ---
112        extract_preceding_prefix_comments(node, content, "---")
113    }
114
115    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
116        node.child_by_field_name("body")
117    }
118}
119
120impl LanguageSymbols for Lua {}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::validate_unused_kinds_audit;
126
127    #[test]
128    fn unused_node_kinds_audit() {
129        #[rustfmt::skip]
130        let documented_unused: &[&str] = &[ "binary_expression", "block",
131            "bracket_index_expression", "else_statement",
132            "empty_statement", "for_generic_clause",
133            "for_numeric_clause", "identifier", "label_statement", "parenthesized_expression", "table_constructor",
134            "unary_expression", "vararg_expression", "variable_declaration",
135            // control flow — not extracted as symbols
136            "return_statement",
137            "while_statement",
138            "elseif_statement",
139            "for_statement",
140            "goto_statement",
141            "do_statement",
142            "if_statement",
143            "break_statement",
144            "repeat_statement",
145            "function_call",
146        ];
147        validate_unused_kinds_audit(&Lua, documented_unused)
148            .expect("Lua unused node kinds audit failed");
149    }
150}