Skip to main content

mlua_check/
vm.rs

1//! mlua VM integration — automatic symbol table construction from live VM state.
2//!
3//! Walks `lua.globals()` to populate a [`SymbolTable`] with the actual globals
4//! and their first-level fields, so that the linter knows exactly what is
5//! available at runtime without manual registration.
6
7use mlua::prelude::*;
8
9use crate::config::LintConfig;
10use crate::engine::LintEngine;
11use crate::symbols::SymbolTable;
12use crate::types::LintResult;
13
14/// Build a [`SymbolTable`] by introspecting the live Lua VM globals.
15///
16/// Walks `lua.globals()` and registers:
17/// - Every top-level key as a global name
18/// - For globals that are tables, every first-level string key as a field
19///
20/// The Lua 5.4 stdlib names are *not* added separately — they are already
21/// present in `lua.globals()` for a standard `Lua::new()` VM.
22pub 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 the value is a table, register first-level fields
31        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
42/// Register the linter on an existing Lua VM.
43///
44/// Introspects `lua.globals()` to build the symbol table automatically.
45/// Returns a configured [`LintEngine`] ready to lint code against this VM's
46/// environment.
47///
48/// # Example
49///
50/// ```rust
51/// use mlua::prelude::*;
52/// use mlua_check::register;
53///
54/// let lua = Lua::new();
55/// // Register custom globals
56/// lua.globals().set("my_api", lua.create_table().unwrap()).unwrap();
57///
58/// let engine = register(&lua).unwrap();
59/// let result = engine.lint("my_api.something()", "@test.lua");
60/// // my_api is known, but "something" field was not registered on the table
61/// ```
62pub fn register(lua: &Lua) -> LuaResult<LintEngine> {
63    register_with_config(lua, LintConfig::default())
64}
65
66/// Register with a custom [`LintConfig`].
67pub 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
74/// Prepend entries to Lua's `package.path` so that `require` can find
75/// modules in project-specific directories.
76///
77/// For each directory in `search_paths`, two patterns are added:
78/// `<dir>/?.lua` and `<dir>/?/init.lua`.
79fn 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(&current);
100
101    package
102        .set("path", prefix)
103        .map_err(|e| format!("Failed to set package.path: {e}"))?;
104    Ok(())
105}
106
107/// One-shot lint: creates a fresh Lua VM, collects stdlib symbols, and lints.
108///
109/// `search_paths` is prepended to `package.path` so that the VM can
110/// resolve project-specific modules when building the symbol table.
111/// Pass `&[]` when no extra paths are needed.
112///
113/// ```rust
114/// let result = mlua_check::run_lint("print('hello')", "@test.lua", &[]).unwrap();
115/// assert_eq!(result.diagnostics.len(), 0);
116/// ```
117pub 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
124/// Lint code against an existing VM's environment.
125///
126/// Convenience wrapper that calls [`register`] and then [`LintEngine::lint`].
127///
128/// ```rust
129/// use mlua::prelude::*;
130///
131/// let lua = Lua::new();
132/// lua.globals().set("custom_fn",
133///     lua.create_function(|_, ()| Ok(42)).unwrap()
134/// ).unwrap();
135///
136/// let result = mlua_check::lint(&lua, "custom_fn()", "@test.lua").unwrap();
137/// assert_eq!(result.diagnostics.len(), 0);
138/// ```
139pub 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        // Standard Lua globals should be present
153        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        // table.insert, string.format, etc.
164        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        // print is known
191        let result = engine.lint("print('hello')", "@test.lua");
192        assert_eq!(result.diagnostics.len(), 0);
193
194        // unknown_func is not
195        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        // alc.llm is known
221        let result = lint(&lua, "alc.llm('hello')", "@test.lua").unwrap();
222        assert_eq!(result.diagnostics.len(), 0);
223
224        // alc.unknown is not
225        let result = lint(&lua, "alc.unknown('hello')", "@test.lua").unwrap();
226        assert!(result.diagnostics.len() > 0);
227    }
228}