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