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 Complexity,
77
78 Correctness,
81
82 Performance,
84
85 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}