1#![recursion_limit = "1000"]
2#![cfg_attr(
3 feature = "force_exhaustive_checks",
4 feature(non_exhaustive_omitted_patterns_lint)
5)]
6use std::{collections::HashMap, error::Error, fmt};
7
8use full_moon::ast::Ast;
9use serde::{
10 de::{DeserializeOwned, Deserializer},
11 Deserialize,
12};
13
14mod ast_util;
15mod lint_filtering;
16pub mod lints;
17mod possible_std;
18pub mod standard_library;
19mod text;
20
21#[cfg(test)]
22mod test_util;
23
24#[cfg(test)]
25mod test_full_runs;
26
27use lints::{AstContext, Context, Diagnostic, Lint, Severity};
28use standard_library::StandardLibrary;
29
30#[derive(Debug)]
31pub struct CheckerError {
32 pub name: &'static str,
33 pub problem: CheckerErrorProblem,
34}
35
36#[derive(Debug)]
37pub enum CheckerErrorProblem {
38 ConfigDeserializeError(Box<dyn Error>),
39 LintNewError(Box<dyn Error>),
40}
41
42impl fmt::Display for CheckerError {
43 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
44 use CheckerErrorProblem::*;
45
46 write!(formatter, "[{}] ", self.name)?;
47
48 match &self.problem {
49 ConfigDeserializeError(error) => write!(
50 formatter,
51 "Configuration was incorrectly formatted: {error}"
52 ),
53 LintNewError(error) => write!(formatter, "{error}"),
54 }
55 }
56}
57
58impl Error for CheckerError {}
59
60#[derive(Deserialize)]
61#[serde(default)]
62#[serde(rename_all = "kebab-case")]
63#[serde(deny_unknown_fields)]
64pub struct CheckerConfig<V> {
65 pub config: HashMap<String, V>,
66 #[serde(alias = "rules")]
67 pub lints: HashMap<String, LintVariation>,
68 pub std: Option<String>,
69 pub exclude: Vec<String>,
70
71 pub roblox_std_source: RobloxStdSource,
74}
75
76impl<V> CheckerConfig<V> {
77 pub fn std(&self) -> &str {
78 self.std.as_deref().unwrap_or("lua51")
79 }
80}
81
82impl<V> Default for CheckerConfig<V> {
83 fn default() -> Self {
84 CheckerConfig {
85 config: HashMap::new(),
86 lints: HashMap::new(),
87 std: None,
88 exclude: Vec::new(),
89
90 roblox_std_source: RobloxStdSource::default(),
91 }
92 }
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
96#[serde(rename_all = "kebab-case")]
97pub enum LintVariation {
98 Allow,
99 Deny,
100 Warn,
101}
102
103impl LintVariation {
104 pub fn to_severity(self) -> Severity {
105 match self {
106 LintVariation::Allow => Severity::Allow,
107 LintVariation::Deny => Severity::Error,
108 LintVariation::Warn => Severity::Warning,
109 }
110 }
111}
112
113#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
114#[serde(rename_all = "kebab-case")]
115pub enum RobloxStdSource {
116 #[default]
117 Floating,
118 Pinned,
119}
120
121macro_rules! use_lints {
122 {
123 $(
124 $lint_name:ident: $lint_path:ty,
125 )+
126
127 $(
128 #[$meta:meta]
129 {
130 $($meta_lint_name:ident: $meta_lint_path:ty,)+
131 },
132 )+
133 } => {
134 lazy_static::lazy_static! {
135 static ref ALL_LINTS: Vec<&'static str> = vec![
136 $(
137 stringify!($lint_name),
138 )+
139
140 $(
141 $(
142 #[$meta]
143 stringify!($meta_lint_name),
144 )+
145 )+
146 ];
147 }
148
149 pub struct Checker<V: 'static + DeserializeOwned> {
150 config: CheckerConfig<V>,
151 context: Context,
152
153 $(
154 $lint_name: $lint_path,
155 )+
156
157 $(
158 $(
159 #[$meta]
160 $meta_lint_name: $meta_lint_path,
161 )+
162 )+
163 }
164
165 impl<V: 'static + DeserializeOwned> Checker<V> {
166 pub fn new(
168 mut config: CheckerConfig<V>,
169 standard_library: StandardLibrary,
170 ) -> Result<Self, CheckerError> where V: for<'de> Deserializer<'de> {
171 macro_rules! lint_field {
172 ($name:ident, $path:ty) => {{
173 let lint_name = stringify!($name);
174
175 let lint = <$path>::new({
176 match config.config.remove(lint_name) {
177 Some(entry_generic) => {
178 <$path as Lint>::Config::deserialize(entry_generic).map_err(|error| {
179 CheckerError {
180 name: lint_name,
181 problem: CheckerErrorProblem::ConfigDeserializeError(Box::new(error)),
182 }
183 })?
184 }
185
186 None => {
187 <$path as Lint>::Config::default()
188 }
189 }
190 }).map_err(|error| {
191 CheckerError {
192 name: stringify!($name),
193 problem: CheckerErrorProblem::LintNewError(Box::new(error)),
194 }
195 })?;
196
197 lint
198 }};
199 }
200
201 Ok(Self {
202 $(
203 $lint_name: {
204 lint_field!($lint_name, $lint_path)
205 },
206 )+
207 $(
208 $(
209 #[$meta]
210 $meta_lint_name: {
211 lint_field!($meta_lint_name, $meta_lint_path)
212 },
213 )+
214 )+
215
216 context: Context {
217 standard_library,
218 user_set_standard_library: config.std.as_ref().map(|std_text| {
219 std_text.split('+').map(ToOwned::to_owned).collect()
220 }),
221 },
222
223 config,
224 })
225 }
226
227 pub fn test_on(&self, ast: &Ast) -> Vec<CheckerDiagnostic> {
228 let mut diagnostics = Vec::new();
229
230 let ast_context = AstContext::from_ast(ast);
231
232 macro_rules! check_lint {
233 ($name:ident) => {
234 let lint = &self.$name;
235
236 let lint_pass = {
237 profiling::scope!(&format!("lint: {}", stringify!($name)));
238 lint.pass(ast, &self.context, &ast_context)
239 };
240
241 diagnostics.extend(&mut lint_pass.into_iter().map(|diagnostic| {
242 CheckerDiagnostic {
243 diagnostic,
244 severity: self.get_lint_severity(lint, stringify!($name)),
245 }
246 }));
247 };
248 }
249
250 $(
251 check_lint!($lint_name);
252 )+
253
254 $(
255 $(
256 #[$meta]
257 {
258 check_lint!($meta_lint_name);
259 }
260 )+
261 )+
262
263 diagnostics = lint_filtering::filter_diagnostics(
264 ast,
265 diagnostics,
266 self.get_lint_severity(&self.invalid_lint_filter, "invalid_lint_filter"),
267 );
268
269 diagnostics
270 }
271
272 fn get_lint_severity<R: Lint>(&self, _lint: &R, name: &'static str) -> Severity {
273 match self.config.lints.get(name) {
274 Some(variation) => variation.to_severity(),
275 None => R::SEVERITY,
276 }
277 }
278 }
279 };
280}
281
282#[derive(Debug)]
283pub struct CheckerDiagnostic {
284 pub diagnostic: Diagnostic,
285 pub severity: Severity,
286}
287
288pub fn lint_exists(name: &str) -> bool {
289 ALL_LINTS.contains(&name)
290}
291
292use_lints! {
293 almost_swapped: lints::almost_swapped::AlmostSwappedLint,
294 bad_string_escape: lints::bad_string_escape::BadStringEscapeLint,
295 compare_nan: lints::compare_nan::CompareNanLint,
296 constant_table_comparison: lints::constant_table_comparison::ConstantTableComparisonLint,
297 deprecated: lints::deprecated::DeprecatedLint,
298 divide_by_zero: lints::divide_by_zero::DivideByZeroLint,
299 duplicate_keys: lints::duplicate_keys::DuplicateKeysLint,
300 empty_if: lints::empty_if::EmptyIfLint,
301 empty_loop: lints::empty_loop::EmptyLoopLint,
302 global_usage: lints::global_usage::GlobalLint,
303 high_cyclomatic_complexity: lints::high_cyclomatic_complexity::HighCyclomaticComplexityLint,
304 if_same_then_else: lints::if_same_then_else::IfSameThenElseLint,
305 ifs_same_cond: lints::ifs_same_cond::IfsSameCondLint,
306 incorrect_standard_library_use: lints::standard_library::StandardLibraryLint,
307 invalid_lint_filter: lints::invalid_lint_filter::InvalidLintFilterLint,
308 manual_table_clone: lints::manual_table_clone::ManualTableCloneLint,
309 mismatched_arg_count: lints::mismatched_arg_count::MismatchedArgCountLint,
310 mixed_table: lints::mixed_table::MixedTableLint,
311 multiple_statements: lints::multiple_statements::MultipleStatementsLint,
312 must_use: lints::must_use::MustUseLint,
313 parenthese_conditions: lints::parenthese_conditions::ParentheseConditionsLint,
314 restricted_module_paths: lints::restricted_module_paths::RestrictedModulePathsLint,
315 shadowing: lints::shadowing::ShadowingLint,
316 suspicious_reverse_loop: lints::suspicious_reverse_loop::SuspiciousReverseLoopLint,
317 type_check_inside_call: lints::type_check_inside_call::TypeCheckInsideCallLint,
318 unbalanced_assignments: lints::unbalanced_assignments::UnbalancedAssignmentsLint,
319 undefined_variable: lints::undefined_variable::UndefinedVariableLint,
320 unscoped_variables: lints::unscoped_variables::UnscopedVariablesLint,
321 unused_variable: lints::unused_variable::UnusedVariableLint,
322
323 #[cfg(feature = "roblox")]
324 {
325 roblox_incorrect_color3_new_bounds: lints::roblox_incorrect_color3_new_bounds::Color3BoundsLint,
326 roblox_incorrect_roact_usage: lints::roblox_incorrect_roact_usage::IncorrectRoactUsageLint,
327 roblox_manual_fromscale_or_fromoffset: lints::roblox_manual_fromscale_or_fromoffset::ManualFromScaleOrFromOffsetLint,
328 roblox_suspicious_udim2_new: lints::roblox_suspicious_udim2_new::SuspiciousUDim2NewLint,
329 },
330}