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/// One-shot lint: creates a fresh Lua VM, collects stdlib symbols, and lints.
75///
76/// This is the simplest API — equivalent to `mlua_lspec::run_tests`.
77///
78/// ```rust
79/// let result = mlua_check::run_lint("print('hello')", "@test.lua").unwrap();
80/// assert_eq!(result.diagnostics.len(), 0);
81/// ```
82pub fn run_lint(code: &str, chunk_name: &str) -> Result<LintResult, String> {
83    let lua = Lua::new();
84    let engine = register(&lua).map_err(|e| format!("Failed to collect VM symbols: {e}"))?;
85    Ok(engine.lint(code, chunk_name))
86}
87
88/// Lint code against an existing VM's environment.
89///
90/// Convenience wrapper that calls [`register`] and then [`LintEngine::lint`].
91///
92/// ```rust
93/// use mlua::prelude::*;
94///
95/// let lua = Lua::new();
96/// lua.globals().set("custom_fn",
97///     lua.create_function(|_, ()| Ok(42)).unwrap()
98/// ).unwrap();
99///
100/// let result = mlua_check::lint(&lua, "custom_fn()", "@test.lua").unwrap();
101/// assert_eq!(result.diagnostics.len(), 0);
102/// ```
103pub fn lint(lua: &Lua, code: &str, chunk_name: &str) -> LuaResult<LintResult> {
104    let engine = register(lua)?;
105    Ok(engine.lint(code, chunk_name))
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn collect_symbols_includes_stdlib() {
114        let lua = Lua::new();
115        let symbols = collect_symbols(&lua).unwrap();
116        // Standard Lua globals should be present
117        assert!(symbols.has_global("print"));
118        assert!(symbols.has_global("table"));
119        assert!(symbols.has_global("string"));
120        assert!(symbols.has_global("math"));
121    }
122
123    #[test]
124    fn collect_symbols_includes_table_fields() {
125        let lua = Lua::new();
126        let symbols = collect_symbols(&lua).unwrap();
127        // table.insert, string.format, etc.
128        assert!(symbols.has_global_field("table", "insert"));
129        assert!(symbols.has_global_field("string", "format"));
130        assert!(symbols.has_global_field("math", "floor"));
131    }
132
133    #[test]
134    fn collect_symbols_includes_custom_globals() {
135        let lua = Lua::new();
136        let tbl = lua.create_table().unwrap();
137        tbl.set("llm", lua.create_function(|_, ()| Ok(())).unwrap())
138            .unwrap();
139        tbl.set("state", lua.create_function(|_, ()| Ok(())).unwrap())
140            .unwrap();
141        lua.globals().set("alc", tbl).unwrap();
142
143        let symbols = collect_symbols(&lua).unwrap();
144        assert!(symbols.has_global("alc"));
145        assert!(symbols.has_global_field("alc", "llm"));
146        assert!(symbols.has_global_field("alc", "state"));
147    }
148
149    #[test]
150    fn register_creates_working_engine() {
151        let lua = Lua::new();
152        let engine = register(&lua).unwrap();
153
154        // print is known
155        let result = engine.lint("print('hello')", "@test.lua");
156        assert_eq!(result.diagnostics.len(), 0);
157
158        // unknown_func is not
159        let result = engine.lint("unknown_func()", "@test.lua");
160        assert!(result.warning_count > 0);
161    }
162
163    #[test]
164    fn run_lint_detects_undefined() {
165        let result = run_lint("unknown_func()", "@test.lua").unwrap();
166        assert!(result.warning_count > 0);
167        assert!(result.diagnostics[0].message.contains("unknown_func"));
168    }
169
170    #[test]
171    fn run_lint_allows_stdlib() {
172        let result = run_lint("print(table.insert)", "@test.lua").unwrap();
173        assert_eq!(result.diagnostics.len(), 0);
174    }
175
176    #[test]
177    fn lint_with_custom_globals() {
178        let lua = Lua::new();
179        let tbl = lua.create_table().unwrap();
180        tbl.set("llm", lua.create_function(|_, ()| Ok(())).unwrap())
181            .unwrap();
182        lua.globals().set("alc", tbl).unwrap();
183
184        // alc.llm is known
185        let result = lint(&lua, "alc.llm('hello')", "@test.lua").unwrap();
186        assert_eq!(result.diagnostics.len(), 0);
187
188        // alc.unknown is not
189        let result = lint(&lua, "alc.unknown('hello')", "@test.lua").unwrap();
190        assert!(result.diagnostics.len() > 0);
191    }
192}