Skip to content

Commit af25752

Browse files
LBrianBrianYPLiuDonIsaaccamchenry
authoredOct 20, 2024··
feat(linter): add unicorn/prefer-math-min-max (#6621)
[unicorn/prefer-math-min-max](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/v56.0.0/docs/rules/prefer-math-min-max.md) of #684 --------- Co-authored-by: Brian Liu <brian@redflagsdating.com> Co-authored-by: Don Isaac <donald.isaac@gmail.com> Co-authored-by: Cam McHenry <camchenry@users.noreply.github.com>
1 parent 29c1447 commit af25752

File tree

3 files changed

+380
-0
lines changed

3 files changed

+380
-0
lines changed
 

‎crates/oxc_linter/src/rules.rs

+2
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ mod unicorn {
334334
pub mod prefer_event_target;
335335
pub mod prefer_includes;
336336
pub mod prefer_logical_operator_over_ternary;
337+
pub mod prefer_math_min_max;
337338
pub mod prefer_math_trunc;
338339
pub mod prefer_modern_dom_apis;
339340
pub mod prefer_modern_math_apis;
@@ -909,6 +910,7 @@ oxc_macros::declare_all_lint_rules! {
909910
unicorn::prefer_event_target,
910911
unicorn::prefer_includes,
911912
unicorn::prefer_logical_operator_over_ternary,
913+
unicorn::prefer_math_min_max,
912914
unicorn::prefer_math_trunc,
913915
unicorn::prefer_modern_dom_apis,
914916
unicorn::prefer_modern_math_apis,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
use std::borrow::Cow;
2+
3+
use oxc_ast::{
4+
ast::{BinaryExpression, BinaryOperator, Expression, UnaryOperator},
5+
AstKind,
6+
};
7+
use oxc_diagnostics::OxcDiagnostic;
8+
use oxc_macros::declare_oxc_lint;
9+
use oxc_span::Span;
10+
11+
use crate::{context::LintContext, rule::Rule, utils::is_same_reference, AstNode};
12+
13+
fn prefer_math_min_max_diagnostic(span: Span) -> OxcDiagnostic {
14+
OxcDiagnostic::warn(
15+
"Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.",
16+
)
17+
.with_label(span)
18+
}
19+
20+
#[derive(Debug, Default, Clone)]
21+
pub struct PreferMathMinMax;
22+
23+
#[derive(Debug)]
24+
enum TypeOptions {
25+
Min,
26+
Max,
27+
Unknown,
28+
}
29+
30+
declare_oxc_lint!(
31+
/// ### What it does
32+
///
33+
/// Prefers use of [`Math.min()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min)
34+
/// and [`Math.max()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max) instead of
35+
/// ternary expressions when performing simple comparisons for more concise, easier to understand, and less prone to errors.
36+
///
37+
/// ### Examples
38+
///
39+
/// Examples of **incorrect** code for this rule:
40+
/// ```javascript
41+
/// height > 50 ? 50 : height;
42+
/// height > 50 ? height : 50;
43+
/// ```
44+
///
45+
/// Examples of **correct** code for this rule:
46+
/// ```javascript
47+
/// Math.min(height, 50);
48+
/// Math.max(height, 50);
49+
/// ```
50+
PreferMathMinMax,
51+
pedantic,
52+
fix
53+
);
54+
55+
impl Rule for PreferMathMinMax {
56+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
57+
let AstKind::ConditionalExpression(conditional_expr) = node.kind() else {
58+
return;
59+
};
60+
61+
let Expression::BinaryExpression(test_expr) = &conditional_expr.test else {
62+
return;
63+
};
64+
65+
let condition_type =
66+
is_min_max(test_expr, &conditional_expr.consequent, &conditional_expr.alternate, ctx);
67+
68+
if matches!(condition_type, TypeOptions::Unknown) {
69+
return;
70+
}
71+
72+
ctx.diagnostic_with_fix(prefer_math_min_max_diagnostic(conditional_expr.span), |fixer| {
73+
let Some(consequent) = get_expr_value(&conditional_expr.consequent) else {
74+
return fixer.noop();
75+
};
76+
let Some(alternate) = get_expr_value(&conditional_expr.alternate) else {
77+
return fixer.noop();
78+
};
79+
80+
match condition_type {
81+
TypeOptions::Max => fixer.replace(
82+
conditional_expr.span,
83+
Cow::Owned(format!("Math.max({consequent}, {alternate})")),
84+
),
85+
TypeOptions::Min => fixer.replace(
86+
conditional_expr.span,
87+
Cow::Owned(format!("Math.min({consequent}, {alternate})")),
88+
),
89+
TypeOptions::Unknown => unreachable!(),
90+
}
91+
});
92+
}
93+
}
94+
95+
fn get_expr_value(expr: &Expression) -> Option<String> {
96+
match expr {
97+
Expression::NumericLiteral(lit) => Some(lit.to_string()),
98+
Expression::UnaryExpression(lit) => {
99+
let mut unary_str: String = String::from(lit.operator.as_str());
100+
101+
let Some(unary_lit) = get_expr_value(&lit.argument) else {
102+
return Some(unary_str.to_string());
103+
};
104+
105+
unary_str.push_str(unary_lit.as_str());
106+
107+
Some(unary_str.to_string())
108+
}
109+
Expression::Identifier(identifier) => Some(identifier.name.to_string()),
110+
_ => None,
111+
}
112+
}
113+
114+
fn is_same_expression(left: &Expression, right: &Expression, ctx: &LintContext) -> bool {
115+
if is_same_reference(left, right, ctx) {
116+
return true;
117+
}
118+
119+
match (left, right) {
120+
(Expression::UnaryExpression(left_expr), Expression::UnaryExpression(right_expr)) => {
121+
left_expr.operator == UnaryOperator::UnaryNegation
122+
&& right_expr.operator == UnaryOperator::UnaryNegation
123+
&& is_same_reference(&left_expr.argument, &right_expr.argument, ctx)
124+
}
125+
_ => false,
126+
}
127+
}
128+
129+
fn is_min_max(
130+
condition: &BinaryExpression,
131+
consequent: &Expression,
132+
alternate: &Expression,
133+
ctx: &LintContext,
134+
) -> TypeOptions {
135+
let is_matched = matches!(
136+
(condition.left.get_inner_expression(), condition.right.get_inner_expression()),
137+
(Expression::NumericLiteral(_) | Expression::UnaryExpression(_), Expression::Identifier(_))
138+
| (
139+
Expression::Identifier(_),
140+
Expression::NumericLiteral(_) | Expression::UnaryExpression(_)
141+
)
142+
);
143+
144+
if !condition.operator.is_compare() || !is_matched {
145+
return TypeOptions::Unknown;
146+
}
147+
148+
if is_same_expression(&condition.left, consequent, ctx)
149+
&& is_same_expression(&condition.right, alternate, ctx)
150+
{
151+
if matches!(condition.operator, BinaryOperator::LessThan | BinaryOperator::LessEqualThan) {
152+
return TypeOptions::Min;
153+
}
154+
return TypeOptions::Max;
155+
} else if is_same_expression(&condition.left, alternate, ctx)
156+
&& is_same_expression(&condition.right, consequent, ctx)
157+
{
158+
if matches!(condition.operator, BinaryOperator::LessThan | BinaryOperator::LessEqualThan) {
159+
return TypeOptions::Max;
160+
}
161+
return TypeOptions::Min;
162+
}
163+
164+
TypeOptions::Unknown
165+
}
166+
167+
#[test]
168+
fn test() {
169+
use crate::tester::Tester;
170+
171+
let pass = vec![
172+
r"const foo = height ? height : 50;",
173+
r"const foo = height == 'success' ? height : 'failed';",
174+
r"Math.min(height, 50);",
175+
r"Math.max(height, 50);",
176+
r"Math.min(50, height);",
177+
r"Math.max(-50, height);",
178+
r"const foo = height < 50n ? height : 50n;",
179+
];
180+
181+
let fail: Vec<&str> = vec![
182+
r"const foo = height < 50 ? height : 50;",
183+
r"const foo = height <= -50 ? height : -50;",
184+
r"const foo = 150.34 < height ? 150.34 : height;",
185+
r"const foo = -2.34 <= height ? -2.34 : height;",
186+
r"const foo = height > 10e3 ? 10e3 : height;",
187+
r"const foo = height >= -10e3 ? -10e3 : height;",
188+
r"const foo = 50 > height ? height : 50;",
189+
r"const foo = 50 >= height ? height : 50;",
190+
r"return height > 100 ? height : 100;",
191+
r"return height >= 50 ? height : 50;",
192+
r"const foo = (50 > height ? 50 : height) || 0;",
193+
r"const foo = (50 >= height ? 50 : height) || 0;",
194+
r"return (height < 50 ? 50 : height) || 100;",
195+
r"return (height <= 50 ? 50 : height) || 200;",
196+
r"const foo = 50 < height ? height: 50;",
197+
r"const foo = 50 <= height ? height: 50;",
198+
];
199+
200+
let fix = vec![
201+
(r"const foo = height < 100 ? height : 100;", r"const foo = Math.min(height, 100);", None),
202+
(
203+
r"const foo = height <= -100 ? height : -100;",
204+
r"const foo = Math.min(height, -100);",
205+
None,
206+
),
207+
(
208+
r"const foo = 150.34 < height ? 150.34 : height;",
209+
r"const foo = Math.min(150.34, height);",
210+
None,
211+
),
212+
(
213+
r"const foo = -0.34 <= height ? -0.34 : height;",
214+
r"const foo = Math.min(-0.34, height);",
215+
None,
216+
),
217+
(
218+
r"const foo = height > 10e3 ? 10e3 : height;",
219+
r"const foo = Math.min(10e3, height);",
220+
None,
221+
),
222+
(
223+
r"const foo = height >= -10e3 ? -10e3 : height;",
224+
r"const foo = Math.min(-10e3, height);",
225+
None,
226+
),
227+
(
228+
r"const foo = 10e3 > height ? height : 10e3;",
229+
r"const foo = Math.min(height, 10e3);",
230+
None,
231+
),
232+
(
233+
r"const foo = 10e3 >= height ? height : 10e3;",
234+
r"const foo = Math.min(height, 10e3);",
235+
None,
236+
),
237+
("return height > 100 ? height : 100;", "return Math.max(height, 100);", None),
238+
("return height >= 50 ? height : 50;", "return Math.max(height, 50);", None),
239+
(
240+
"return (10e3 > height ? 10e3 : height) || 200;",
241+
"return (Math.max(10e3, height)) || 200;",
242+
None,
243+
),
244+
(
245+
"return (-10e3 >= height ? -10e3 : height) || 200;",
246+
"return (Math.max(-10e3, height)) || 200;",
247+
None,
248+
),
249+
(
250+
"return (height < 2.99 ? 2.99 : height) || 0.99;",
251+
"return (Math.max(2.99, height)) || 0.99;",
252+
None,
253+
),
254+
(
255+
"return (height <= -0.99 ? -0.99 : height) || -3.99;",
256+
"return (Math.max(-0.99, height)) || -3.99;",
257+
None,
258+
),
259+
("return 10e6 < height ? height : 10e6;", "return Math.max(height, 10e6);", None),
260+
("return -10e4 <= height ? height : -10e4;", "return Math.max(height, -10e4);", None),
261+
];
262+
263+
Tester::new(PreferMathMinMax::NAME, pass, fail).expect_fix(fix).test_and_snapshot();
264+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
5+
╭─[prefer_math_min_max.tsx:1:13]
6+
1const foo = height < 50 ? height : 50;
7+
· ─────────────────────────
8+
╰────
9+
help: Replace `height < 50 ? height : 50` with `Math.min(height, 50)`.
10+
11+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
12+
╭─[prefer_math_min_max.tsx:1:13]
13+
1const foo = height <= -50 ? height : -50;
14+
· ────────────────────────────
15+
╰────
16+
help: Replace `height <= -50 ? height : -50` with `Math.min(height, -50)`.
17+
18+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
19+
╭─[prefer_math_min_max.tsx:1:13]
20+
1const foo = 150.34 < height ? 150.34 : height;
21+
· ─────────────────────────────────
22+
╰────
23+
help: Replace `150.34 < height ? 150.34 : height` with `Math.min(150.34, height)`.
24+
25+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
26+
╭─[prefer_math_min_max.tsx:1:13]
27+
1const foo = -2.34 <= height ? -2.34 : height;
28+
· ────────────────────────────────
29+
╰────
30+
help: Replace `-2.34 <= height ? -2.34 : height` with `Math.min(-2.34, height)`.
31+
32+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
33+
╭─[prefer_math_min_max.tsx:1:13]
34+
1const foo = height > 10e3 ? 10e3 : height;
35+
· ─────────────────────────────
36+
╰────
37+
help: Replace `height > 10e3 ? 10e3 : height` with `Math.min(10e3, height)`.
38+
39+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
40+
╭─[prefer_math_min_max.tsx:1:13]
41+
1const foo = height >= -10e3 ? -10e3 : height;
42+
· ────────────────────────────────
43+
╰────
44+
help: Replace `height >= -10e3 ? -10e3 : height` with `Math.min(-10e3, height)`.
45+
46+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
47+
╭─[prefer_math_min_max.tsx:1:13]
48+
1const foo = 50 > height ? height : 50;
49+
· ─────────────────────────
50+
╰────
51+
help: Replace `50 > height ? height : 50` with `Math.min(height, 50)`.
52+
53+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
54+
╭─[prefer_math_min_max.tsx:1:13]
55+
1const foo = 50 >= height ? height : 50;
56+
· ──────────────────────────
57+
╰────
58+
help: Replace `50 >= height ? height : 50` with `Math.min(height, 50)`.
59+
60+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
61+
╭─[prefer_math_min_max.tsx:1:8]
62+
1return height > 100 ? height : 100;
63+
· ───────────────────────────
64+
╰────
65+
help: Replace `height > 100 ? height : 100` with `Math.max(height, 100)`.
66+
67+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
68+
╭─[prefer_math_min_max.tsx:1:8]
69+
1return height >= 50 ? height : 50;
70+
· ──────────────────────────
71+
╰────
72+
help: Replace `height >= 50 ? height : 50` with `Math.max(height, 50)`.
73+
74+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
75+
╭─[prefer_math_min_max.tsx:1:14]
76+
1const foo = (50 > height ? 50 : height) || 0;
77+
· ─────────────────────────
78+
╰────
79+
help: Replace `50 > height ? 50 : height` with `Math.max(50, height)`.
80+
81+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
82+
╭─[prefer_math_min_max.tsx:1:14]
83+
1const foo = (50 >= height ? 50 : height) || 0;
84+
· ──────────────────────────
85+
╰────
86+
help: Replace `50 >= height ? 50 : height` with `Math.max(50, height)`.
87+
88+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
89+
╭─[prefer_math_min_max.tsx:1:9]
90+
1return (height < 50 ? 50 : height) || 100;
91+
· ─────────────────────────
92+
╰────
93+
help: Replace `height < 50 ? 50 : height` with `Math.max(50, height)`.
94+
95+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
96+
╭─[prefer_math_min_max.tsx:1:9]
97+
1return (height <= 50 ? 50 : height) || 200;
98+
· ──────────────────────────
99+
╰────
100+
help: Replace `height <= 50 ? 50 : height` with `Math.max(50, height)`.
101+
102+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
103+
╭─[prefer_math_min_max.tsx:1:13]
104+
1const foo = 50 < height ? height: 50;
105+
· ────────────────────────
106+
╰────
107+
help: Replace `50 < height ? height: 50` with `Math.max(height, 50)`.
108+
109+
eslint-plugin-unicorn(prefer-math-min-max): Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.
110+
╭─[prefer_math_min_max.tsx:1:13]
111+
1const foo = 50 <= height ? height: 50;
112+
· ─────────────────────────
113+
╰────
114+
help: Replace `50 <= height ? height: 50` with `Math.max(height, 50)`.

0 commit comments

Comments
 (0)
Please sign in to comment.