selene_lib/
lib.rs

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    // Not locked behind Roblox feature so that selene.toml for Roblox will
72    // run even without it.
73    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, PartialEq, Eq, Deserialize)]
114#[serde(rename_all = "kebab-case")]
115pub enum RobloxStdSource {
116    Floating,
117    Pinned,
118}
119
120impl Default for RobloxStdSource {
121    fn default() -> Self {
122        Self::Floating
123    }
124}
125
126macro_rules! use_lints {
127    {
128        $(
129            $lint_name:ident: $lint_path:ty,
130        )+
131
132        $(
133            #[$meta:meta]
134            {
135                $($meta_lint_name:ident: $meta_lint_path:ty,)+
136            },
137        )+
138    } => {
139        lazy_static::lazy_static! {
140            static ref ALL_LINTS: Vec<&'static str> = vec![
141                $(
142                    stringify!($lint_name),
143                )+
144
145                $(
146                    $(
147                        #[$meta]
148                        stringify!($meta_lint_name),
149                    )+
150                )+
151            ];
152        }
153
154        pub struct Checker<V: 'static + DeserializeOwned> {
155            config: CheckerConfig<V>,
156            context: Context,
157
158            $(
159                $lint_name: $lint_path,
160            )+
161
162            $(
163                $(
164                    #[$meta]
165                    $meta_lint_name: $meta_lint_path,
166                )+
167            )+
168        }
169
170        impl<V: 'static + DeserializeOwned> Checker<V> {
171            // TODO: Be more strict about config? Make sure all keys exist
172            pub fn new(
173                mut config: CheckerConfig<V>,
174                standard_library: StandardLibrary,
175            ) -> Result<Self, CheckerError> where V: for<'de> Deserializer<'de> {
176                macro_rules! lint_field {
177                    ($name:ident, $path:ty) => {{
178                        let lint_name = stringify!($name);
179
180                        let lint = <$path>::new({
181                            match config.config.remove(lint_name) {
182                                Some(entry_generic) => {
183                                    <$path as Lint>::Config::deserialize(entry_generic).map_err(|error| {
184                                        CheckerError {
185                                            name: lint_name,
186                                            problem: CheckerErrorProblem::ConfigDeserializeError(Box::new(error)),
187                                        }
188                                    })?
189                                }
190
191                                None => {
192                                    <$path as Lint>::Config::default()
193                                }
194                            }
195                        }).map_err(|error| {
196                            CheckerError {
197                                name: stringify!($name),
198                                problem: CheckerErrorProblem::LintNewError(Box::new(error)),
199                            }
200                        })?;
201
202                        lint
203                    }};
204                }
205
206                Ok(Self {
207                    $(
208                        $lint_name: {
209                            lint_field!($lint_name, $lint_path)
210                        },
211                    )+
212                    $(
213                        $(
214                            #[$meta]
215                            $meta_lint_name: {
216                                lint_field!($meta_lint_name, $meta_lint_path)
217                            },
218                        )+
219                    )+
220
221                    context: Context {
222                        standard_library,
223                        user_set_standard_library: config.std.as_ref().map(|std_text| {
224                            std_text.split('+').map(ToOwned::to_owned).collect()
225                        }),
226                    },
227
228                    config,
229                })
230            }
231
232            pub fn test_on(&self, ast: &Ast) -> Vec<CheckerDiagnostic> {
233                let mut diagnostics = Vec::new();
234
235                let ast_context = AstContext::from_ast(ast);
236
237                macro_rules! check_lint {
238                    ($name:ident) => {
239                        let lint = &self.$name;
240
241                        let lint_pass = {
242                            profiling::scope!(&format!("lint: {}", stringify!($name)));
243                            lint.pass(ast, &self.context, &ast_context)
244                        };
245
246                        diagnostics.extend(&mut lint_pass.into_iter().map(|diagnostic| {
247                            CheckerDiagnostic {
248                                diagnostic,
249                                severity: self.get_lint_severity(lint, stringify!($name)),
250                            }
251                        }));
252                    };
253                }
254
255                $(
256                    check_lint!($lint_name);
257                )+
258
259                $(
260                    $(
261                        #[$meta]
262                        {
263                            check_lint!($meta_lint_name);
264                        }
265                    )+
266                )+
267
268                diagnostics = lint_filtering::filter_diagnostics(
269                    ast,
270                    diagnostics,
271                    self.get_lint_severity(&self.invalid_lint_filter, "invalid_lint_filter"),
272                );
273
274                diagnostics
275            }
276
277            fn get_lint_severity<R: Lint>(&self, _lint: &R, name: &'static str) -> Severity {
278                match self.config.lints.get(name) {
279                    Some(variation) => variation.to_severity(),
280                    None => R::SEVERITY,
281                }
282            }
283        }
284    };
285}
286
287#[derive(Debug)]
288pub struct CheckerDiagnostic {
289    pub diagnostic: Diagnostic,
290    pub severity: Severity,
291}
292
293pub fn lint_exists(name: &str) -> bool {
294    ALL_LINTS.contains(&name)
295}
296
297use_lints! {
298    almost_swapped: lints::almost_swapped::AlmostSwappedLint,
299    bad_string_escape: lints::bad_string_escape::BadStringEscapeLint,
300    compare_nan: lints::compare_nan::CompareNanLint,
301    constant_table_comparison: lints::constant_table_comparison::ConstantTableComparisonLint,
302    deprecated: lints::deprecated::DeprecatedLint,
303    divide_by_zero: lints::divide_by_zero::DivideByZeroLint,
304    duplicate_keys: lints::duplicate_keys::DuplicateKeysLint,
305    empty_if: lints::empty_if::EmptyIfLint,
306    empty_loop: lints::empty_loop::EmptyLoopLint,
307    global_usage: lints::global_usage::GlobalLint,
308    high_cyclomatic_complexity: lints::high_cyclomatic_complexity::HighCyclomaticComplexityLint,
309    if_same_then_else: lints::if_same_then_else::IfSameThenElseLint,
310    ifs_same_cond: lints::ifs_same_cond::IfsSameCondLint,
311    incorrect_standard_library_use: lints::standard_library::StandardLibraryLint,
312    invalid_lint_filter: lints::invalid_lint_filter::InvalidLintFilterLint,
313    manual_table_clone: lints::manual_table_clone::ManualTableCloneLint,
314    mismatched_arg_count: lints::mismatched_arg_count::MismatchedArgCountLint,
315    mixed_table: lints::mixed_table::MixedTableLint,
316    multiple_statements: lints::multiple_statements::MultipleStatementsLint,
317    must_use: lints::must_use::MustUseLint,
318    parenthese_conditions: lints::parenthese_conditions::ParentheseConditionsLint,
319    restricted_module_paths: lints::restricted_module_paths::RestrictedModulePathsLint,
320    shadowing: lints::shadowing::ShadowingLint,
321    suspicious_reverse_loop: lints::suspicious_reverse_loop::SuspiciousReverseLoopLint,
322    type_check_inside_call: lints::type_check_inside_call::TypeCheckInsideCallLint,
323    unbalanced_assignments: lints::unbalanced_assignments::UnbalancedAssignmentsLint,
324    undefined_variable: lints::undefined_variable::UndefinedVariableLint,
325    unscoped_variables: lints::unscoped_variables::UnscopedVariablesLint,
326    unused_variable: lints::unused_variable::UnusedVariableLint,
327
328    #[cfg(feature = "roblox")]
329    {
330        roblox_incorrect_color3_new_bounds: lints::roblox_incorrect_color3_new_bounds::Color3BoundsLint,
331        roblox_incorrect_roact_usage: lints::roblox_incorrect_roact_usage::IncorrectRoactUsageLint,
332        roblox_manual_fromscale_or_fromoffset: lints::roblox_manual_fromscale_or_fromoffset::ManualFromScaleOrFromOffsetLint,
333        roblox_suspicious_udim2_new: lints::roblox_suspicious_udim2_new::SuspiciousUDim2NewLint,
334    },
335}