selene_lib/lints/
roblox_manual_fromscale_or_fromoffset.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 ManualFromScaleOrFromOffsetLint;
11
12fn create_diagnostic(args: &UDim2ComplexArgs) -> Diagnostic {
13    let code = "roblox_manual_fromscale_or_fromoffset";
14    let primary_label = Label::new(args.call_range);
15
16    match args.complexity_type {
17        UDim2ConstructorType::OffsetOnly => Diagnostic::new_complete(
18            code,
19            "this UDim2.new call only sets offset, and can be simplified using UDim2.fromOffset"
20                .to_owned(),
21            primary_label,
22            vec![format!(
23                "try: UDim2.fromOffset({}, {})",
24                args.arg_0, args.arg_1
25            )],
26            Vec::new(),
27        ),
28        UDim2ConstructorType::ScaleOnly => Diagnostic::new_complete(
29            code,
30            "this UDim2.new call only sets scale, and can be simplified using UDim2.fromScale"
31                .to_owned(),
32            primary_label,
33            vec![format!(
34                "try: UDim2.fromScale({}, {})",
35                args.arg_0, args.arg_1
36            )],
37            Vec::new(),
38        ),
39    }
40}
41
42impl Lint for ManualFromScaleOrFromOffsetLint {
43    type Config = ();
44    type Error = Infallible;
45
46    const SEVERITY: Severity = Severity::Warning;
47    const LINT_TYPE: LintType = LintType::Style;
48
49    fn new(_: Self::Config) -> Result<Self, Self::Error> {
50        Ok(ManualFromScaleOrFromOffsetLint)
51    }
52
53    fn pass(&self, ast: &Ast, context: &Context, _: &AstContext) -> Vec<Diagnostic> {
54        if !context.is_roblox() {
55            return Vec::new();
56        }
57
58        let mut visitor = UDim2NewVisitor::default();
59
60        visitor.visit_ast(ast);
61
62        visitor.args.iter().map(create_diagnostic).collect()
63    }
64}
65
66#[derive(Default)]
67struct UDim2NewVisitor {
68    args: Vec<UDim2ComplexArgs>,
69}
70
71#[derive(PartialEq)]
72enum UDim2ConstructorType {
73    ScaleOnly,
74    OffsetOnly,
75}
76
77struct UDim2ComplexArgs {
78    complexity_type: UDim2ConstructorType,
79    call_range: (usize, usize),
80    arg_0: String,
81    arg_1: String,
82}
83
84impl Visitor for UDim2NewVisitor {
85    fn visit_function_call(&mut self, call: &ast::FunctionCall) {
86        if_chain::if_chain! {
87            if let ast::Prefix::Name(token) = call.prefix();
88            if token.token().to_string() == "UDim2";
89            let mut suffixes = call.suffixes().collect::<Vec<_>>();
90
91            if suffixes.len() == 2; // .new and ()
92            let call_suffix = suffixes.pop().unwrap();
93            let index_suffix = suffixes.pop().unwrap();
94
95            if let ast::Suffix::Index(ast::Index::Dot { name, .. }) = index_suffix;
96            if name.token().to_string() == "new";
97
98            if let ast::Suffix::Call(ast::Call::AnonymousCall(
99                ast::FunctionArgs::Parentheses { arguments, .. }
100            )) = call_suffix;
101
102            then {
103                if arguments.len() != 4 {
104                    return;
105                }
106
107                let mut iter = arguments.iter();
108                let x_scale = iter.next().unwrap().to_string();
109                let x_offset = iter.next().unwrap().to_string();
110                let y_scale = iter.next().unwrap().to_string();
111                let y_offset = iter.next().unwrap().to_string();
112
113                let only_offset = x_scale.parse::<f32>() == Ok(0.0) && y_scale.parse::<f32>() == Ok(0.0);
114                let only_scale = x_offset.parse::<f32>() == Ok(0.0) && y_offset.parse::<f32>() == Ok(0.0);
115
116                if only_offset && only_scale
117                {
118                    // Skip linting if all are zero
119                }
120                else if only_offset
121                {
122                    self.args.push(UDim2ComplexArgs {
123                        call_range: range(call),
124                        arg_0: x_offset,
125                        arg_1: y_offset,
126                        complexity_type: UDim2ConstructorType::OffsetOnly,
127                    });
128                }
129                else if only_scale
130                {
131                    self.args.push(UDim2ComplexArgs {
132                        call_range: range(call),
133                        arg_0: x_scale,
134                        arg_1: y_scale,
135                        complexity_type: UDim2ConstructorType::ScaleOnly,
136                    });
137                }
138            }
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::{super::test_util::test_lint, *};
146
147    #[test]
148    fn test_manual_fromscale_or_fromoffset() {
149        test_lint(
150            ManualFromScaleOrFromOffsetLint::new(()).unwrap(),
151            "roblox_manual_fromscale_or_fromoffset",
152            "roblox_manual_fromscale_or_fromoffset",
153        );
154    }
155}