1use valua_ast::{Block, LuaTarget};
10use valua_diagnostics::Diagnostic;
11
12pub mod passes;
13
14use passes::const_mutation::ConstMutation;
15use passes::unsupported::UnsupportedFeatureGuard;
16
17pub trait Lint {
19 fn check(&self, block: &Block) -> Vec<Diagnostic>;
20}
21
22pub struct LintPipeline {
24 passes: Vec<Box<dyn Lint>>,
25}
26
27impl LintPipeline {
28 #[must_use]
34 pub fn default_for(target: LuaTarget) -> Self {
35 let mut passes: Vec<Box<dyn Lint>> = vec![Box::new(ConstMutation)];
36 match target {
37 LuaTarget::Lua51 | LuaTarget::LuaJIT => {
38 passes.push(Box::new(UnsupportedFeatureGuard));
39 }
40 }
41 Self { passes }
42 }
43
44 #[must_use]
46 pub fn run(&self, block: &Block) -> Vec<Diagnostic> {
47 let mut out = Vec::new();
48 for pass in &self.passes {
49 out.extend(pass.check(block));
50 }
51 out
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use valua_ast::{Block, LuaTarget};
58 use valua_diagnostics::{CollectingReporter, Diagnostic, Reporter, Span};
59
60 use super::{Lint, LintPipeline};
61
62 fn parse(src: &str) -> Block {
63 valua_parser::parse(src).expect("parse failed")
64 }
65
66 fn dummy_span() -> Span {
67 Span::new(0, 1, 1, 1)
68 }
69
70 struct AlwaysErrors;
73 impl Lint for AlwaysErrors {
74 fn check(&self, _: &Block) -> Vec<Diagnostic> {
75 vec![Diagnostic::error("always fires", dummy_span()).with_code("E9999")]
76 }
77 }
78
79 struct NeverErrors;
80 impl Lint for NeverErrors {
81 fn check(&self, _: &Block) -> Vec<Diagnostic> {
82 vec![]
83 }
84 }
85
86 #[test]
87 fn lint_trait_mock_fires() {
88 let block = parse("local x = 1");
89 let diags = AlwaysErrors.check(&block);
90 assert_eq!(diags.len(), 1);
91 assert_eq!(diags[0].code, Some("E9999"));
92 }
93
94 #[test]
95 fn lint_trait_mock_silent() {
96 let block = parse("local x = 1");
97 assert!(NeverErrors.check(&block).is_empty());
98 }
99
100 #[test]
103 fn pipeline_lua51_runs_unsupported_guard() {
104 let block = parse("math.type(1)");
105 let diags = LintPipeline::default_for(LuaTarget::Lua51).run(&block);
106 assert!(diags.iter().any(|d| d.code == Some("E0101")));
107 }
108
109 #[test]
110 fn pipeline_luajit_runs_unsupported_guard() {
111 let block = parse("math.type(1)");
112 let diags = LintPipeline::default_for(LuaTarget::LuaJIT).run(&block);
113 assert!(diags.iter().any(|d| d.code == Some("E0101")));
114 }
115
116 #[test]
117 fn pipeline_all_targets_run_const_mutation() {
118 let block = parse("local x <const> = 1\nx = 2");
119 for target in [LuaTarget::Lua51, LuaTarget::LuaJIT] {
120 let diags = LintPipeline::default_for(target).run(&block);
121 assert!(
122 diags.iter().any(|d| d.code == Some("E0301")),
123 "{target:?} should detect E0301"
124 );
125 }
126 }
127
128 #[test]
129 fn pipeline_run_returns_diags_in_pass_order() {
130 let src = format!(
131 "local x <const> = 1\nx = 2\nmath.type(1)\nlocal y = {}",
132 i64::MAX
133 );
134 let diags = LintPipeline::default_for(LuaTarget::Lua51).run(&parse(&src));
135 let codes: Vec<_> = diags.iter().filter_map(|d| d.code).collect();
136 let e0301_pos = codes.iter().position(|&c| c == "E0301").unwrap();
138 let e0101_pos = codes.iter().position(|&c| c == "E0101").unwrap();
139 assert!(
140 e0301_pos < e0101_pos,
141 "E0301 should appear before E0101 (pass order)"
142 );
143 assert!(codes.contains(&"E0102"));
144 }
145
146 #[test]
147 fn pipeline_clean_code_no_diags() {
148 let block = parse("local x = 1\nlocal y = x + 2\nreturn y");
149 let diags = LintPipeline::default_for(LuaTarget::Lua51).run(&block);
150 assert!(diags.is_empty());
151 }
152
153 #[test]
156 fn pipeline_with_collecting_reporter() {
157 let src = "local x <const> = 1\nx = 2";
158 let diags = LintPipeline::default_for(LuaTarget::Lua51).run(&parse(src));
159 let mut reporter = CollectingReporter::default();
160 for d in &diags {
161 reporter.report(d, src, "test.lua");
162 }
163 assert!(reporter.has_errors());
164 assert!(reporter.diagnostics.iter().any(|d| d.code == Some("E0301")));
165 }
166
167 #[test]
168 fn pipeline_no_errors_reporter_clean() {
169 let src = "local x = 1";
170 let diags = LintPipeline::default_for(LuaTarget::Lua51).run(&parse(src));
171 let mut reporter = CollectingReporter::default();
172 for d in &diags {
173 reporter.report(d, src, "test.lua");
174 }
175 assert!(!reporter.has_errors());
176 }
177}