1use mlua::prelude::*;
8
9use crate::config::LintConfig;
10use crate::engine::LintEngine;
11use crate::symbols::SymbolTable;
12use crate::types::LintResult;
13
14pub fn collect_symbols(lua: &Lua) -> LuaResult<SymbolTable> {
23 let mut symbols = SymbolTable::new();
24 let globals = lua.globals();
25
26 for pair in globals.pairs::<String, LuaValue>() {
27 let (key, value) = pair?;
28 symbols.add_global(&key);
29
30 if let LuaValue::Table(tbl) = value {
32 for field_pair in tbl.pairs::<String, LuaValue>() {
33 let (field_key, _) = field_pair?;
34 symbols.add_global_field(&key, &field_key);
35 }
36 }
37 }
38
39 Ok(symbols)
40}
41
42pub fn register(lua: &Lua) -> LuaResult<LintEngine> {
63 register_with_config(lua, LintConfig::default())
64}
65
66pub fn register_with_config(lua: &Lua, config: LintConfig) -> LuaResult<LintEngine> {
68 let symbols = collect_symbols(lua)?;
69 let mut engine = LintEngine::with_config(config);
70 *engine.symbols_mut() = symbols;
71 Ok(engine)
72}
73
74fn prepend_search_paths(lua: &Lua, search_paths: &[&str]) -> Result<(), String> {
80 if search_paths.is_empty() {
81 return Ok(());
82 }
83 let package: LuaTable = lua
84 .globals()
85 .get("package")
86 .map_err(|e| format!("Failed to get package table: {e}"))?;
87 let current: String = package
88 .get("path")
89 .map_err(|e| format!("Failed to get package.path: {e}"))?;
90
91 let mut prefix = String::new();
92 for dir in search_paths {
93 let dir = dir.trim_end_matches('/');
94 prefix.push_str(dir);
95 prefix.push_str("/?.lua;");
96 prefix.push_str(dir);
97 prefix.push_str("/?/init.lua;");
98 }
99 prefix.push_str(¤t);
100
101 package
102 .set("path", prefix)
103 .map_err(|e| format!("Failed to set package.path: {e}"))?;
104 Ok(())
105}
106
107pub fn run_lint(code: &str, chunk_name: &str, search_paths: &[&str]) -> Result<LintResult, String> {
118 let lua = Lua::new();
119 prepend_search_paths(&lua, search_paths)?;
120 let engine = register(&lua).map_err(|e| format!("Failed to collect VM symbols: {e}"))?;
121 Ok(engine.lint(code, chunk_name))
122}
123
124pub fn lint(lua: &Lua, code: &str, chunk_name: &str) -> LuaResult<LintResult> {
140 let engine = register(lua)?;
141 Ok(engine.lint(code, chunk_name))
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn collect_symbols_includes_stdlib() {
150 let lua = Lua::new();
151 let symbols = collect_symbols(&lua).unwrap();
152 assert!(symbols.has_global("print"));
154 assert!(symbols.has_global("table"));
155 assert!(symbols.has_global("string"));
156 assert!(symbols.has_global("math"));
157 }
158
159 #[test]
160 fn collect_symbols_includes_table_fields() {
161 let lua = Lua::new();
162 let symbols = collect_symbols(&lua).unwrap();
163 assert!(symbols.has_global_field("table", "insert"));
165 assert!(symbols.has_global_field("string", "format"));
166 assert!(symbols.has_global_field("math", "floor"));
167 }
168
169 #[test]
170 fn collect_symbols_includes_custom_globals() {
171 let lua = Lua::new();
172 let tbl = lua.create_table().unwrap();
173 tbl.set("llm", lua.create_function(|_, ()| Ok(())).unwrap())
174 .unwrap();
175 tbl.set("state", lua.create_function(|_, ()| Ok(())).unwrap())
176 .unwrap();
177 lua.globals().set("alc", tbl).unwrap();
178
179 let symbols = collect_symbols(&lua).unwrap();
180 assert!(symbols.has_global("alc"));
181 assert!(symbols.has_global_field("alc", "llm"));
182 assert!(symbols.has_global_field("alc", "state"));
183 }
184
185 #[test]
186 fn register_creates_working_engine() {
187 let lua = Lua::new();
188 let engine = register(&lua).unwrap();
189
190 let result = engine.lint("print('hello')", "@test.lua");
192 assert_eq!(result.diagnostics.len(), 0);
193
194 let result = engine.lint("unknown_func()", "@test.lua");
196 assert!(result.warning_count > 0);
197 }
198
199 #[test]
200 fn run_lint_detects_undefined() {
201 let result = run_lint("unknown_func()", "@test.lua", &[]).unwrap();
202 assert!(result.warning_count > 0);
203 assert!(result.diagnostics[0].message.contains("unknown_func"));
204 }
205
206 #[test]
207 fn run_lint_allows_stdlib() {
208 let result = run_lint("print(table.insert)", "@test.lua", &[]).unwrap();
209 assert_eq!(result.diagnostics.len(), 0);
210 }
211
212 #[test]
213 fn lint_with_custom_globals() {
214 let lua = Lua::new();
215 let tbl = lua.create_table().unwrap();
216 tbl.set("llm", lua.create_function(|_, ()| Ok(())).unwrap())
217 .unwrap();
218 lua.globals().set("alc", tbl).unwrap();
219
220 let result = lint(&lua, "alc.llm('hello')", "@test.lua").unwrap();
222 assert_eq!(result.diagnostics.len(), 0);
223
224 let result = lint(&lua, "alc.unknown('hello')", "@test.lua").unwrap();
226 assert!(result.diagnostics.len() > 0);
227 }
228}