1use super::*;
2use crate::{
3 ast_util::{range, strip_parentheses},
4 standard_library::RobloxClass,
5};
6use std::{
7 collections::{BTreeMap, HashMap},
8 convert::Infallible,
9};
10
11use full_moon::{
12 ast::{self, Ast},
13 tokenizer::{TokenReference, TokenType},
14 visitors::Visitor,
15};
16use if_chain::if_chain;
17
18pub struct IncorrectRoactUsageLint;
19
20fn is_lua_valid_table_key_identifier(string: &str) -> bool {
22 let first_char = string.chars().nth(1).unwrap();
24 if !first_char.is_alphabetic() && first_char != '_' {
25 return false;
26 }
27
28 string
29 .chars()
30 .skip(1)
31 .take(string.len() - 2)
32 .all(|c| c.is_alphanumeric() || c == '_')
33}
34
35fn get_lua_table_key_format(expression: &ast::Expression) -> String {
36 match expression {
37 ast::Expression::String(token) => {
38 let string = token.to_string();
39 if is_lua_valid_table_key_identifier(&string) {
40 string[1..string.len() - 1].to_string()
41 } else {
42 format!("[{string}]")
43 }
44 }
45 _ => format!("[{expression}]"),
46 }
47}
48
49impl Lint for IncorrectRoactUsageLint {
50 type Config = ();
51 type Error = Infallible;
52
53 const SEVERITY: Severity = Severity::Error;
54 const LINT_TYPE: LintType = LintType::Correctness;
55
56 fn new(_: Self::Config) -> Result<Self, Self::Error> {
57 Ok(IncorrectRoactUsageLint)
58 }
59
60 fn pass(&self, ast: &Ast, context: &Context, _: &AstContext) -> Vec<Diagnostic> {
61 if !context.is_roblox() {
62 return Vec::new();
63 }
64
65 let roblox_classes = &context.standard_library.roblox_classes;
66
67 if roblox_classes.is_empty() {
69 return Vec::new();
70 }
71
72 let mut visitor = IncorrectRoactUsageVisitor {
73 definitions_of_create_element: HashMap::new(),
74 invalid_events: Vec::new(),
75 invalid_properties: Vec::new(),
76 unknown_class: Vec::new(),
77
78 roblox_classes,
79 };
80
81 visitor.visit_ast(ast);
82
83 let mut diagnostics = Vec::new();
84
85 for invalid_event in visitor.invalid_events {
86 diagnostics.push(Diagnostic::new(
87 "roblox_incorrect_roact_usage",
88 format!(
89 "`{}` is not a valid event for `{}`",
90 invalid_event.event_name, invalid_event.class_name
91 ),
92 Label::new(invalid_event.range),
93 ));
94 }
95
96 for invalid_property in visitor.invalid_properties {
97 match invalid_property.property_name.as_str() {
98 "Name" => {
99 diagnostics.push(Diagnostic::new_complete(
100 "roblox_incorrect_roact_usage",
101 format!(
102 "`{}` is assigned through the element's key for Roblox instances",
103 invalid_property.property_name
104 ),
105 Label::new(invalid_property.range),
106 vec![format!(
107 "try: {} = {}(...)",
108 get_lua_table_key_format(&invalid_property.property_value),
109 invalid_property.create_element_expression,
110 )],
111 Vec::new(),
112 ));
113 }
114 _ => {
115 diagnostics.push(Diagnostic::new(
116 "roblox_incorrect_roact_usage",
117 format!(
118 "`{}` is not a property of `{}`",
119 invalid_property.property_name, invalid_property.class_name
120 ),
121 Label::new(invalid_property.range),
122 ));
123 }
124 }
125 }
126
127 for unknown_class in visitor.unknown_class {
128 diagnostics.push(Diagnostic::new(
129 "roblox_incorrect_roact_usage",
130 format!("`{}` is not a valid class", unknown_class.name),
131 Label::new(unknown_class.range),
132 ));
133 }
134
135 diagnostics
136 }
137}
138
139fn is_roact_or_react_create_element(
140 prefix: &ast::Prefix,
141 suffixes: &[&ast::Suffix],
142) -> Option<LibraryName> {
143 if_chain! {
144 if let ast::Prefix::Name(prefix_token) = prefix;
145 if let Some(library_name) = match prefix_token.token().to_string().as_str() {
146 "Roact" => Some(LibraryName::Roact),
147 "React" => Some(LibraryName::React),
148 _ => None,
149 };
150 if suffixes.len() == 1;
151 if let ast::Suffix::Index(ast::Index::Dot { name, .. }) = suffixes[0];
152 if name.token().to_string() == "createElement";
153 then {
154 Some(library_name)
155 } else {
156 None
157 }
158 }
159}
160
161#[derive(Debug, PartialEq, Clone, Copy)]
162enum LibraryName {
163 Roact,
164 React,
165}
166
167#[derive(Debug)]
168struct IncorrectRoactUsageVisitor<'a> {
169 definitions_of_create_element: HashMap<String, LibraryName>,
170 invalid_events: Vec<InvalidEvent>,
171 invalid_properties: Vec<InvalidProperty>,
172 unknown_class: Vec<UnknownClass>,
173
174 roblox_classes: &'a BTreeMap<String, RobloxClass>,
175}
176
177#[derive(Debug)]
178struct InvalidEvent {
179 class_name: String,
180 event_name: String,
181 range: (usize, usize),
182}
183
184#[derive(Debug)]
185struct InvalidProperty {
186 class_name: String,
187 property_name: String,
188 property_value: ast::Expression,
189 create_element_expression: String,
190 range: (usize, usize),
191}
192
193#[derive(Debug)]
194struct UnknownClass {
195 name: String,
196 range: (usize, usize),
197}
198
199impl<'a> IncorrectRoactUsageVisitor<'a> {
200 fn check_class_name(&mut self, token: &TokenReference) -> Option<(String, &'a RobloxClass)> {
201 let name = if let TokenType::StringLiteral { literal, .. } = token.token_type() {
202 literal.to_string()
203 } else {
204 return None;
205 };
206
207 match self.roblox_classes.get(&name) {
208 Some(roblox_class) => Some((name, roblox_class)),
209
210 None => {
211 self.unknown_class.push(UnknownClass {
212 name,
213 range: range(token),
214 });
215
216 None
217 }
218 }
219 }
220}
221
222impl Visitor for IncorrectRoactUsageVisitor<'_> {
223 fn visit_function_call(&mut self, call: &ast::FunctionCall) {
224 let mut suffixes = call.suffixes().collect::<Vec<_>>();
226 let call_suffix = suffixes.pop();
227
228 let mut library_name = None;
229 let mut create_element_expression = String::new();
230
231 if suffixes.is_empty() {
232 if let ast::Prefix::Name(name) = call.prefix() {
235 if let Some(react_name) = self
236 .definitions_of_create_element
237 .get(&name.token().to_string())
238 {
239 library_name = Some(*react_name);
240 create_element_expression = name.token().to_string();
241 }
242 }
243 } else if suffixes.len() == 1 {
244 library_name = is_roact_or_react_create_element(call.prefix(), &suffixes);
247
248 if let ast::Prefix::Name(name) = call.prefix() {
249 create_element_expression = format!("{}{}", name.token(), suffixes[0]);
250 }
251 }
252
253 let react_name = match library_name {
254 Some(name) => name,
255 None => return,
256 };
257
258 let ((name, class), arguments) = if_chain! {
259 if let Some(ast::Suffix::Call(ast::Call::AnonymousCall(
260 ast::FunctionArgs::Parentheses { arguments, .. }
261 ))) = call_suffix;
262 if !arguments.is_empty();
263 let mut iter = arguments.iter();
264
265 let name_arg = iter.next().unwrap();
267 if let ast::Expression::String(token) = name_arg;
268 if let Some((name, class)) = self.check_class_name(token);
269
270 if let Some(ast::Expression::TableConstructor(table)) = iter.next();
272
273 then {
274 ((name, class), table)
275 } else {
276 return;
277 }
278 };
279
280 for field in arguments.fields() {
281 match field {
282 ast::Field::NameKey { key, value, .. } => {
283 let property_name = key.token().to_string();
284
285 if react_name == LibraryName::React
286 && ["ref", "key", "children"].contains(&property_name.as_str())
287 {
288 continue;
289 }
290
291 if !class.has_property(self.roblox_classes, &property_name)
292 || property_name == "Name"
293 {
294 self.invalid_properties.push(InvalidProperty {
295 class_name: name.clone(),
296 property_name,
297 property_value: value.clone(),
298 create_element_expression: create_element_expression.clone().to_owned(),
299 range: range(key),
300 });
301 }
302 }
303
304 ast::Field::ExpressionKey { brackets, key, .. } => {
305 let key = strip_parentheses(key);
306
307 if_chain::if_chain! {
308 if let ast::Expression::Var(ast::Var::Expression(var_expression)) = key;
309
310 if let ast::Prefix::Name(constant_roact_name) = var_expression.prefix();
311 if ["Roact", "React"].contains(&constant_roact_name.token().to_string().as_str());
312
313 let mut suffixes = var_expression.suffixes();
314 if let Some(ast::Suffix::Index(ast::Index::Dot { name: constant_event_name, .. })) = suffixes.next();
315 if constant_event_name.token().to_string() == "Event";
316
317 if let Some(ast::Suffix::Index(ast::Index::Dot { name: event_name, .. })) = suffixes.next();
318 then {
319 let event_name = event_name.token().to_string();
320 if !class.has_event(self.roblox_classes, &event_name) {
321 self.invalid_events.push(InvalidEvent {
322 class_name: name.clone(),
323 event_name,
324 range: range(brackets),
325 });
326 }
327 }
328 }
329 }
330
331 _ => {}
332 }
333 }
334 }
335
336 fn visit_local_assignment(&mut self, node: &ast::LocalAssignment) {
337 for (name, expr) in node.names().iter().zip(node.expressions().iter()) {
338 if_chain! {
339 if let ast::Expression::Var(ast::Var::Expression(var_expr)) = expr;
340 if let Some(roact_or_react) = is_roact_or_react_create_element(var_expr.prefix(), &var_expr.suffixes().collect::<Vec<_>>());
341 then {
342 self.definitions_of_create_element.insert(name.token().to_string(), roact_or_react);
343 }
344 };
345 }
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::{super::test_util::test_lint, *};
352
353 #[test]
354 fn test_mixed_roact_react_usage() {
355 test_lint(
356 IncorrectRoactUsageLint::new(()).unwrap(),
357 "roblox_incorrect_roact_usage",
358 "mixed_roact_react_usage",
359 );
360 }
361
362 #[test]
363 fn test_old_roblox_std() {
364 test_lint(
365 IncorrectRoactUsageLint::new(()).unwrap(),
366 "roblox_incorrect_roact_usage",
367 "old_roblox_std",
368 );
369 }
370
371 #[test]
372 fn test_roblox_incorrect_react_usage() {
373 test_lint(
374 IncorrectRoactUsageLint::new(()).unwrap(),
375 "roblox_incorrect_roact_usage",
376 "roblox_incorrect_react_usage",
377 );
378 }
379
380 #[test]
381 fn test_roblox_incorrect_roact_usage() {
382 test_lint(
383 IncorrectRoactUsageLint::new(()).unwrap(),
384 "roblox_incorrect_roact_usage",
385 "roblox_incorrect_roact_usage",
386 );
387 }
388}