use crate::config::LintConfig;
use crate::symbols::SymbolTable;
use crate::types::LintResult;
use crate::walker;
#[derive(Debug, Clone)]
pub struct LintEngine {
symbols: SymbolTable,
config: LintConfig,
}
impl LintEngine {
pub fn new() -> Self {
Self {
symbols: SymbolTable::new().with_lua54_stdlib(),
config: LintConfig::default(),
}
}
pub fn with_config(config: LintConfig) -> Self {
Self {
symbols: SymbolTable::new().with_lua54_stdlib(),
config,
}
}
pub fn symbols_mut(&mut self) -> &mut SymbolTable {
&mut self.symbols
}
pub fn symbols(&self) -> &SymbolTable {
&self.symbols
}
pub fn config_mut(&mut self) -> &mut LintConfig {
&mut self.config
}
pub fn lint(&self, source: &str, _chunk_name: &str) -> LintResult {
let mut all_diagnostics = walker::walk(source, &self.symbols, &self.config);
all_diagnostics.sort_by(|a, b| a.line.cmp(&b.line).then(a.column.cmp(&b.column)));
LintResult::new(all_diagnostics)
}
}
impl Default for LintEngine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn engine_detects_undefined_global() {
let engine = LintEngine::new();
let result = engine.lint("unknown_func()", "@test.lua");
assert_eq!(result.warning_count, 1);
assert!(result.diagnostics[0].message.contains("unknown_func"));
}
#[test]
fn engine_with_custom_globals() {
let mut engine = LintEngine::new();
engine.symbols_mut().add_global("alc");
engine.symbols_mut().add_global_field("alc", "llm");
let result = engine.lint("alc.llm('hello')", "@test.lua");
assert_eq!(result.diagnostics.len(), 0);
let result = engine.lint("alc.llm_call('hello')", "@test.lua");
assert_eq!(result.diagnostics.len(), 1);
assert!(result.diagnostics[0].message.contains("llm_call"));
}
#[test]
fn engine_empty_code_no_errors() {
let engine = LintEngine::new();
let result = engine.lint("", "@test.lua");
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn engine_detects_unused_variable() {
let engine = LintEngine::new();
let result = engine.lint("local unused = 42\nprint('hi')", "@test.lua");
let unused: Vec<_> = result
.diagnostics
.iter()
.filter(|d| d.rule == crate::types::RuleId::UnusedVariable)
.collect();
assert_eq!(unused.len(), 1);
assert!(unused[0].message.contains("unused"));
}
#[test]
fn engine_scoped_local_out_of_scope() {
let engine = LintEngine::new();
let result = engine.lint("do\n local x = 1\nend\nprint(x)", "@test.lua");
let globals: Vec<_> = result
.diagnostics
.iter()
.filter(|d| d.rule == crate::types::RuleId::UndefinedGlobal)
.collect();
assert_eq!(globals.len(), 1, "diagnostics: {globals:?}");
}
}