selene_lib/
lints.rs

1use crate::{ast_util::scopes::ScopeManager, standard_library::StandardLibrary};
2use std::convert::TryInto;
3
4use codespan_reporting::diagnostic::{
5    Diagnostic as CodespanDiagnostic, Label as CodespanLabel, Severity as CodespanSeverity,
6};
7use full_moon::{ast::Ast, node::Node};
8use serde::de::DeserializeOwned;
9
10pub mod almost_swapped;
11pub mod bad_string_escape;
12pub mod compare_nan;
13pub mod constant_table_comparison;
14pub mod deprecated;
15pub mod divide_by_zero;
16pub mod duplicate_keys;
17pub mod empty_if;
18pub mod empty_loop;
19pub mod global_usage;
20pub mod high_cyclomatic_complexity;
21pub mod if_same_then_else;
22pub mod ifs_same_cond;
23pub mod invalid_lint_filter;
24pub mod manual_table_clone;
25pub mod mismatched_arg_count;
26pub mod mixed_table;
27pub mod multiple_statements;
28pub mod must_use;
29pub mod parenthese_conditions;
30pub mod restricted_module_paths;
31pub mod shadowing;
32pub mod standard_library;
33pub mod suspicious_reverse_loop;
34pub mod type_check_inside_call;
35pub mod unbalanced_assignments;
36pub mod undefined_variable;
37pub mod unscoped_variables;
38pub mod unused_variable;
39
40#[cfg(feature = "roblox")]
41pub mod roblox_incorrect_color3_new_bounds;
42
43#[cfg(feature = "roblox")]
44pub mod roblox_incorrect_roact_usage;
45
46#[cfg(feature = "roblox")]
47pub mod roblox_manual_fromscale_or_fromoffset;
48
49#[cfg(feature = "roblox")]
50pub mod roblox_suspicious_udim2_new;
51
52#[cfg(test)]
53mod test_util;
54
55pub trait Lint {
56    type Config: DeserializeOwned;
57    type Error: std::error::Error;
58
59    const SEVERITY: Severity;
60    const LINT_TYPE: LintType;
61
62    fn new(config: Self::Config) -> Result<Self, Self::Error>
63    where
64        Self: Sized;
65
66    fn pass(
67        &self,
68        ast: &full_moon::ast::Ast,
69        context: &Context,
70        ast_context: &AstContext,
71    ) -> Vec<Diagnostic>;
72}
73
74pub enum LintType {
75    /// Code that does something simple but in a complex way
76    Complexity,
77
78    /// Code that is outright wrong or very very useless
79    /// Should have severity "Error"
80    Correctness,
81
82    /// Code that can be written in a faster way
83    Performance,
84
85    /// Code that should be written in a more idiomatic way
86    Style,
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum Severity {
91    Allow,
92    Error,
93    Warning,
94}
95
96#[derive(Debug)]
97pub struct Diagnostic {
98    pub code: &'static str,
99    pub message: String,
100    pub notes: Vec<String>,
101    pub primary_label: Label,
102    pub secondary_labels: Vec<Label>,
103}
104
105impl Diagnostic {
106    pub fn new(code: &'static str, message: String, primary_label: Label) -> Self {
107        Self {
108            code,
109            message,
110            primary_label,
111
112            notes: Vec::new(),
113            secondary_labels: Vec::new(),
114        }
115    }
116
117    pub fn new_complete(
118        code: &'static str,
119        message: String,
120        primary_label: Label,
121        notes: Vec<String>,
122        secondary_labels: Vec<Label>,
123    ) -> Self {
124        Self {
125            code,
126            message,
127            notes,
128            primary_label,
129            secondary_labels,
130        }
131    }
132
133    pub fn into_codespan_diagnostic(
134        self,
135        file_id: codespan::FileId,
136        severity: CodespanSeverity,
137    ) -> CodespanDiagnostic<codespan::FileId> {
138        let mut labels = Vec::with_capacity(1 + self.secondary_labels.len());
139        labels.push(self.primary_label.codespan_label(file_id));
140        labels.extend(&mut self.secondary_labels.iter().map(|label| {
141            CodespanLabel::secondary(file_id, codespan::Span::new(label.range.0, label.range.1))
142                .with_message(label.message.as_ref().unwrap_or(&"".to_owned()).to_owned())
143        }));
144
145        CodespanDiagnostic {
146            code: Some(self.code.to_owned()),
147            labels,
148            message: self.message.to_owned(),
149            notes: self.notes,
150            severity,
151        }
152    }
153
154    pub fn start_position(&self) -> u32 {
155        self.primary_label.range.0
156    }
157}
158
159#[derive(Debug, PartialEq, Eq)]
160pub struct Label {
161    pub message: Option<String>,
162    pub range: (u32, u32),
163}
164
165impl Label {
166    pub fn new<P: TryInto<u32>>(range: (P, P)) -> Label {
167        let range = (
168            range
169                .0
170                .try_into()
171                .unwrap_or_else(|_| panic!("TryInto failed for Label::new range")),
172            range
173                .1
174                .try_into()
175                .unwrap_or_else(|_| panic!("TryInto failed for Label::new range")),
176        );
177
178        Label {
179            range,
180            message: None,
181        }
182    }
183
184    pub fn from_node<N: Node>(node: N, message: Option<String>) -> Label {
185        let (start, end) = node.range().expect("node passed returned a None range");
186
187        Label {
188            message,
189            range: (start.bytes() as u32, end.bytes() as u32),
190        }
191    }
192
193    pub fn new_with_message<P: TryInto<u32>>(range: (P, P), message: String) -> Label {
194        let range = (
195            range
196                .0
197                .try_into()
198                .unwrap_or_else(|_| panic!("TryInto failed for Label::new range")),
199            range
200                .1
201                .try_into()
202                .unwrap_or_else(|_| panic!("TryInto failed for Label::new range")),
203        );
204
205        Label {
206            range,
207            message: Some(message),
208        }
209    }
210
211    pub fn codespan_label(&self, file_id: codespan::FileId) -> CodespanLabel<codespan::FileId> {
212        CodespanLabel::primary(
213            file_id.to_owned(),
214            codespan::Span::new(self.range.0, self.range.1),
215        )
216        .with_message(self.message.as_ref().unwrap_or(&"".to_owned()).to_owned())
217    }
218}
219
220#[derive(Clone, Debug)]
221pub struct Context {
222    pub standard_library: StandardLibrary,
223    pub user_set_standard_library: Option<Vec<String>>,
224}
225
226impl Context {
227    #[cfg(feature = "roblox")]
228    pub fn is_roblox(&self) -> bool {
229        self.standard_library.name.as_deref() == Some("roblox")
230    }
231
232    #[cfg(not(feature = "roblox"))]
233    pub fn is_roblox(&self) -> bool {
234        false
235    }
236}
237
238#[derive(Debug)]
239pub struct AstContext {
240    pub scope_manager: ScopeManager,
241}
242
243impl AstContext {
244    pub fn from_ast(ast: &Ast) -> Self {
245        Self {
246            scope_manager: ScopeManager::new(ast),
247        }
248    }
249}