selene_lib/lints/
roblox_suspicious_udim2_new.rs1use super::*;
2use crate::ast_util::range;
3use std::convert::Infallible;
4
5use full_moon::{
6 ast::{self, Ast},
7 visitors::Visitor,
8};
9
10pub struct SuspiciousUDim2NewLint;
11
12fn create_diagnostic(mismatch: &MismatchedArgCount) -> Diagnostic {
13 let code = "roblox_suspicious_udim2_new";
14 let message = format!(
15 "UDim2.new takes 4 numbers, but {} {} provided.",
16 mismatch.args_provided,
17 if mismatch.args_provided == 1 {
18 "was"
19 } else {
20 "were"
21 }
22 );
23 let primary_label = Label::new(mismatch.call_range);
24
25 if mismatch.args_provided <= 2 && mismatch.args_are_numbers {
26 Diagnostic::new_complete(
27 code,
28 message,
29 primary_label,
30 vec![if mismatch.args_are_between_0_and_1 {
31 "did you mean to use UDim2.fromScale instead?"
32 } else {
33 "did you mean to use UDim2.fromOffset instead?"
34 }
35 .to_owned()],
36 Vec::new(),
37 )
38 } else {
39 Diagnostic::new(code, message, primary_label)
40 }
41}
42
43impl Lint for SuspiciousUDim2NewLint {
44 type Config = ();
45 type Error = Infallible;
46
47 const SEVERITY: Severity = Severity::Warning;
48 const LINT_TYPE: LintType = LintType::Correctness;
49
50 fn new(_: Self::Config) -> Result<Self, Self::Error> {
51 Ok(SuspiciousUDim2NewLint)
52 }
53
54 fn pass(&self, ast: &Ast, context: &Context, _: &AstContext) -> Vec<Diagnostic> {
55 if !context.is_roblox() {
56 return Vec::new();
57 }
58
59 let mut visitor = UDim2CountVisitor::default();
60
61 visitor.visit_ast(ast);
62
63 visitor.args.iter().map(create_diagnostic).collect()
64 }
65}
66
67#[derive(Default)]
68struct UDim2CountVisitor {
69 args: Vec<MismatchedArgCount>,
70}
71
72struct MismatchedArgCount {
73 args_provided: usize,
74 call_range: (usize, usize),
75 args_are_between_0_and_1: bool,
76 args_are_numbers: bool,
77}
78
79impl Visitor for UDim2CountVisitor {
80 fn visit_function_call(&mut self, call: &ast::FunctionCall) {
81 if_chain::if_chain! {
82 if let ast::Prefix::Name(token) = call.prefix();
83 if token.token().to_string() == "UDim2";
84 let mut suffixes = call.suffixes().collect::<Vec<_>>();
85
86 if suffixes.len() == 2; let call_suffix = suffixes.pop().unwrap();
88 let index_suffix = suffixes.pop().unwrap();
89
90 if let ast::Suffix::Index(ast::Index::Dot { name, .. }) = index_suffix;
91 if name.token().to_string() == "new";
92
93 if let ast::Suffix::Call(ast::Call::AnonymousCall(
94 ast::FunctionArgs::Parentheses { arguments, .. }
95 )) = call_suffix;
96
97 then {
98 let args_provided = arguments.len();
99
100 if args_provided == 0 || args_provided >= 4 {
101 return;
102 }
103
104 let numbers_passed = arguments.iter().filter(|expression| {
105 matches!(expression, ast::Expression::Number(_))
106 }).count();
107
108 if args_provided == 2 && numbers_passed == 0 {
110 return;
111 };
112
113 self.args.push(MismatchedArgCount {
114 call_range: range(call),
115 args_provided,
116 args_are_between_0_and_1: arguments.iter().all(|argument| {
117 match argument.to_string().parse::<f32>() {
118 Ok(number) => (0.0..=1.0).contains(&number),
119 Err(_) => false,
120 }
121 }),
122 args_are_numbers: numbers_passed == args_provided,
123 });
124 }
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::{super::test_util::test_lint, *};
132
133 #[test]
134 fn test_roblox_suspicious_udim2_new() {
135 test_lint(
136 SuspiciousUDim2NewLint::new(()).unwrap(),
137 "roblox_suspicious_udim2_new",
138 "roblox_suspicious_udim2_new",
139 );
140 }
141}