1use crate::config::LintConfig;
5use crate::symbols::SymbolTable;
6use crate::types::LintResult;
7use crate::walker;
8
9#[derive(Debug, Clone)]
24pub struct LintEngine {
25 symbols: SymbolTable,
26 config: LintConfig,
27}
28
29impl LintEngine {
30 pub fn new() -> Self {
33 Self {
34 symbols: SymbolTable::new().with_lua54_stdlib(),
35 config: LintConfig::default(),
36 }
37 }
38
39 pub fn with_config(config: LintConfig) -> Self {
41 Self {
42 symbols: SymbolTable::new().with_lua54_stdlib(),
43 config,
44 }
45 }
46
47 pub fn symbols_mut(&mut self) -> &mut SymbolTable {
49 &mut self.symbols
50 }
51
52 pub fn symbols(&self) -> &SymbolTable {
54 &self.symbols
55 }
56
57 pub fn config_mut(&mut self) -> &mut LintConfig {
59 &mut self.config
60 }
61
62 pub fn lint(&self, source: &str, _chunk_name: &str) -> LintResult {
66 let mut all_diagnostics = walker::walk(source, &self.symbols, &self.config);
69
70 all_diagnostics.sort_by(|a, b| a.line.cmp(&b.line).then(a.column.cmp(&b.column)));
72
73 LintResult::new(all_diagnostics)
74 }
75}
76
77impl Default for LintEngine {
78 fn default() -> Self {
79 Self::new()
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn engine_detects_undefined_global() {
89 let engine = LintEngine::new();
90 let result = engine.lint("unknown_func()", "@test.lua");
91 assert_eq!(result.warning_count, 1);
92 assert!(result.diagnostics[0].message.contains("unknown_func"));
93 }
94
95 #[test]
96 fn engine_with_custom_globals() {
97 let mut engine = LintEngine::new();
98 engine.symbols_mut().add_global("alc");
99 engine.symbols_mut().add_global_field("alc", "llm");
100
101 let result = engine.lint("alc.llm('hello')", "@test.lua");
102 assert_eq!(result.diagnostics.len(), 0);
103
104 let result = engine.lint("alc.llm_call('hello')", "@test.lua");
105 assert_eq!(result.diagnostics.len(), 1);
106 assert!(result.diagnostics[0].message.contains("llm_call"));
107 }
108
109 #[test]
110 fn engine_empty_code_no_errors() {
111 let engine = LintEngine::new();
112 let result = engine.lint("", "@test.lua");
113 assert_eq!(result.diagnostics.len(), 0);
114 }
115
116 #[test]
117 fn engine_detects_unused_variable() {
118 let engine = LintEngine::new();
119 let result = engine.lint("local unused = 42\nprint('hi')", "@test.lua");
120 let unused: Vec<_> = result
121 .diagnostics
122 .iter()
123 .filter(|d| d.rule == crate::types::RuleId::UnusedVariable)
124 .collect();
125 assert_eq!(unused.len(), 1);
126 assert!(unused[0].message.contains("unused"));
127 }
128
129 #[test]
130 fn engine_scoped_local_out_of_scope() {
131 let engine = LintEngine::new();
132 let result = engine.lint("do\n local x = 1\nend\nprint(x)", "@test.lua");
133 let globals: Vec<_> = result
134 .diagnostics
135 .iter()
136 .filter(|d| d.rule == crate::types::RuleId::UndefinedGlobal)
137 .collect();
138 assert_eq!(globals.len(), 1, "diagnostics: {globals:?}");
140 }
141}