Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support linting complex udim2 constructors that can be simplified #601

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/src/lints/roblox_complex_udim2_new.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# roblox_complex_udim2_new
Quenty marked this conversation as resolved.
Show resolved Hide resolved
## What it does
Checks for uses of `UDim2.new` where the arguments could be simplified to `UDim2.fromScale` or `UDim2.fromOffset`.

## Why this is bad
This reduces readability of `UDim2.new()` construction.

## Example
```lua
UDim2.new(1, 0, 1, 0)
```

## Remarks
This lint is only active if you are using the Roblox standard library.
1 change: 1 addition & 0 deletions selene-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ use_lints! {

#[cfg(feature = "roblox")]
{
roblox_complex_udim2_new: lints::roblox_complex_udim2_new::ComplexUDim2NewLint,
roblox_incorrect_color3_new_bounds: lints::roblox_incorrect_color3_new_bounds::Color3BoundsLint,
roblox_incorrect_roact_usage: lints::roblox_incorrect_roact_usage::IncorrectRoactUsageLint,
roblox_suspicious_udim2_new: lints::roblox_suspicious_udim2_new::SuspiciousUDim2NewLint,
Expand Down
3 changes: 3 additions & 0 deletions selene-lib/src/lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ pub mod undefined_variable;
pub mod unscoped_variables;
pub mod unused_variable;

#[cfg(feature = "roblox")]
pub mod roblox_complex_udim2_new;

#[cfg(feature = "roblox")]
pub mod roblox_incorrect_color3_new_bounds;

Expand Down
176 changes: 176 additions & 0 deletions selene-lib/src/lints/roblox_complex_udim2_new.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
use super::*;
use crate::ast_util::range;
use std::convert::Infallible;

use full_moon::{
ast::{self, Ast},
visitors::Visitor,
};

pub struct ComplexUDim2NewLint;

fn create_diagnostic(mismatch: &UDim2ComplexArgs) -> Diagnostic {
Quenty marked this conversation as resolved.
Show resolved Hide resolved
let code = "roblox_complex_udim2_new";
let primary_label = Label::new(mismatch.call_range);

if mismatch.no_scale {
Diagnostic::new_complete(
code,
"this UDim2.new call only sets offset, and can be simplified using UDim2.fromOffset"
.to_owned(),
primary_label,
vec![format!(
"try: UDim2.fromOffset({}, {})",
mismatch.arg_0, mismatch.arg_1
)],
Vec::new(),
)
} else {
Diagnostic::new_complete(
code,
"this UDim2.new call only sets scale, and can be simplified using UDim2.fromScale"
.to_owned(),
primary_label,
vec![format!(
"try: UDim2.fromScale({}, {})",
mismatch.arg_0, mismatch.arg_1
)],
Vec::new(),
)
}
}

impl Lint for ComplexUDim2NewLint {
type Config = ();
type Error = Infallible;

const SEVERITY: Severity = Severity::Warning;
const LINT_TYPE: LintType = LintType::Style;

fn new(_: Self::Config) -> Result<Self, Self::Error> {
Ok(ComplexUDim2NewLint)
}

fn pass(&self, ast: &Ast, context: &Context, _: &AstContext) -> Vec<Diagnostic> {
if !context.is_roblox() {
return Vec::new();
}

let mut visitor = UDim2NewVisitor::default();

visitor.visit_ast(ast);

visitor.args.iter().map(create_diagnostic).collect()
}
}

#[derive(Default)]
struct UDim2NewVisitor {
args: Vec<UDim2ComplexArgs>,
}

struct UDim2ComplexArgs {
call_range: (usize, usize),
arg_0: f32,
arg_1: f32,
no_scale: bool,
Quenty marked this conversation as resolved.
Show resolved Hide resolved
}

impl Visitor for UDim2NewVisitor {
fn visit_function_call(&mut self, call: &ast::FunctionCall) {
if_chain::if_chain! {
if let ast::Prefix::Name(token) = call.prefix();
if token.token().to_string() == "UDim2";
let mut suffixes = call.suffixes().collect::<Vec<_>>();

if suffixes.len() == 2; // .new and ()
let call_suffix = suffixes.pop().unwrap();
let index_suffix = suffixes.pop().unwrap();

if let ast::Suffix::Index(ast::Index::Dot { name, .. }) = index_suffix;
if name.token().to_string() == "new";

if let ast::Suffix::Call(ast::Call::AnonymousCall(
ast::FunctionArgs::Parentheses { arguments, .. }
)) = call_suffix;

then {
let args_provided = arguments.len();
if args_provided != 4 {
return;
}


let numbers_passed = arguments.iter().filter(|expression| {
matches!(expression, ast::Expression::Number(_))
}).count();
if numbers_passed != 4 {
return;
}
Quenty marked this conversation as resolved.
Show resolved Hide resolved

let mut iter = arguments.iter();
let x_scale = match iter.next().unwrap().to_string().parse::<f32>() {
Ok(value) => value,
Err(_) => return,
};
Quenty marked this conversation as resolved.
Show resolved Hide resolved
let x_offset = match iter.next().unwrap().to_string().parse::<f32>() {
Ok(value) => value,
Err(_) => return,
};
let y_scale = match iter.next().unwrap().to_string().parse::<f32>() {
Ok(value) => value,
Err(_) => return,
};
let y_offset = match iter.next().unwrap().to_string().parse::<f32>() {
Ok(value) => value,
Err(_) => return,
};

let no_scale = x_scale == 0.0 && y_scale == 0.0;
let no_offset = x_offset == 0.0 && y_offset == 0.0;

let arg_0;
let arg_1;
if no_scale && no_offset
{
// Skip any lint
return;
}
else if no_scale
{
arg_0 = x_offset;
arg_1 = y_offset;
}
else if no_offset {
arg_0 = x_scale;
arg_1 = y_scale;
}
else
{
return;
}

self.args.push(UDim2ComplexArgs {
call_range: range(call),
arg_0: arg_0,
arg_1: arg_1,
no_scale: no_scale,
Quenty marked this conversation as resolved.
Show resolved Hide resolved
});
}
}
}
}

#[cfg(test)]
mod tests {
use super::{super::test_util::test_lint, *};

#[test]
fn test_roblox_complex_udim2_new() {
test_lint(
ComplexUDim2NewLint::new(()).unwrap(),
"roblox_complex_udim2_new",
"roblox_complex_udim2_new",
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
UDim2.new(0)
UDim2.new(0.5)
UDim2.new(1)
UDim2.new(2)
UDim2.new(a)
UDim2.new(1, 1)
UDim2.new(1, 2)
UDim2.new(a, b)
UDim2.new(1, a)
UDim2.new(1, 1, 1)
UDim2.new(a, b, c)
UDim2.new(1, 1, 1, 1)
UDim2.new(a, b, c, d)
UDim2.fromOffset(1, 1)
UDim2.fromScale(1, 1)
UDim2.new()
UDim2.new(1, 0, 1, 0)
UDim2.new(0, 0, 1, 0)
UDim2.new(0, 5, 1, 5)
UDim2.new(0, 5, 0, 1)
UDim2.new(0, 0, 0, 1)
UDim2.new(0, 1, 0, 1)
UDim2.new(0, a, 0, 1)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[selene]
name = "roblox"
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
error[roblox_complex_udim2_new]: this UDim2.new call only sets scale, and can be simplified using UDim2.fromScale
┌─ roblox_complex_udim2_new.lua:17:1
17 │ UDim2.new(1, 0, 1, 0)
│ ^^^^^^^^^^^^^^^^^^^^^
= try: UDim2.fromScale(1, 1)

error[roblox_complex_udim2_new]: this UDim2.new call only sets scale, and can be simplified using UDim2.fromScale
┌─ roblox_complex_udim2_new.lua:18:1
18 │ UDim2.new(0, 0, 1, 0)
│ ^^^^^^^^^^^^^^^^^^^^^
= try: UDim2.fromScale(0, 1)

error[roblox_complex_udim2_new]: this UDim2.new call only sets offset, and can be simplified using UDim2.fromOffset
┌─ roblox_complex_udim2_new.lua:20:1
20 │ UDim2.new(0, 5, 0, 1)
│ ^^^^^^^^^^^^^^^^^^^^^
= try: UDim2.fromOffset(5, 1)

error[roblox_complex_udim2_new]: this UDim2.new call only sets offset, and can be simplified using UDim2.fromOffset
┌─ roblox_complex_udim2_new.lua:21:1
21 │ UDim2.new(0, 0, 0, 1)
│ ^^^^^^^^^^^^^^^^^^^^^
= try: UDim2.fromOffset(0, 1)

error[roblox_complex_udim2_new]: this UDim2.new call only sets offset, and can be simplified using UDim2.fromOffset
┌─ roblox_complex_udim2_new.lua:22:1
22 │ UDim2.new(0, 1, 0, 1)
│ ^^^^^^^^^^^^^^^^^^^^^
= try: UDim2.fromOffset(1, 1)