selene_lib/lints/
roblox_suspicious_udim2_new.rs

1use 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; // .new and ()
87            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                // Prevents false positives for UDim2.new(UDim.new(), UDim.new())
109                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}