Skip to content

Commit 7369077

Browse files
axetroysindresorhusfisker
authored
Add prefer-math-min-max (#2432)
Co-authored-by: Sindre Sorhus <[email protected]> Co-authored-by: fisker <[email protected]>
1 parent 1e367bb commit 7369077

7 files changed

+761
-0
lines changed

docs/rules/prefer-math-min-max.md

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons
2+
3+
💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs-eslintconfigjs).
4+
5+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6+
7+
<!-- end auto-generated rule header -->
8+
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->
9+
10+
This rule enforces the use of `Math.min()` and `Math.max()` functions instead of ternary expressions when performing simple comparisons, such as selecting the minimum or maximum value between two or more options.
11+
12+
By replacing ternary expressions with these functions, the code becomes more concise, easier to understand, and less prone to errors. It also enhances consistency across the codebase, ensuring that the same approach is used for similar operations, ultimately improving the overall readability and maintainability of the code.
13+
14+
## Examples
15+
16+
<!-- Math.min() -->
17+
18+
```js
19+
height > 50 ? 50 : height; //
20+
Math.min(height, 50); //
21+
```
22+
23+
```js
24+
height >= 50 ? 50 : height; //
25+
Math.min(height, 50); //
26+
```
27+
28+
```js
29+
height < 50 ? height : 50; //
30+
Math.min(height, 50); //
31+
```
32+
33+
```js
34+
height <= 50 ? height : 50; //
35+
Math.min(height, 50); //
36+
```
37+
38+
<!-- Math.max() -->
39+
40+
```js
41+
height > 50 ? height : 50; //
42+
Math.max(height, 50); //
43+
```
44+
45+
```js
46+
height >= 50 ? height : 50; //
47+
Math.max(height, 50); //
48+
```
49+
50+
```js
51+
height < 50 ? 50 : height; //
52+
Math.max(height, 50); //
53+
```
54+
55+
```js
56+
height <= 50 ? 50 : height; //
57+
Math.max(height, 50); //
58+
```

readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
193193
| [prefer-json-parse-buffer](docs/rules/prefer-json-parse-buffer.md) | Prefer reading a JSON file as a buffer. | | 🔧 | |
194194
| [prefer-keyboard-event-key](docs/rules/prefer-keyboard-event-key.md) | Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. || 🔧 | |
195195
| [prefer-logical-operator-over-ternary](docs/rules/prefer-logical-operator-over-ternary.md) | Prefer using a logical operator over a ternary. || | 💡 |
196+
| [prefer-math-min-max](docs/rules/prefer-math-min-max.md) | Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons. || 🔧 | |
196197
| [prefer-math-trunc](docs/rules/prefer-math-trunc.md) | Enforce the use of `Math.trunc` instead of bitwise operators. || 🔧 | 💡 |
197198
| [prefer-modern-dom-apis](docs/rules/prefer-modern-dom-apis.md) | Prefer `.before()` over `.insertBefore()`, `.replaceWith()` over `.replaceChild()`, prefer one of `.before()`, `.after()`, `.append()` or `.prepend()` over `insertAdjacentText()` and `insertAdjacentElement()`. || 🔧 | |
198199
| [prefer-modern-math-apis](docs/rules/prefer-modern-math-apis.md) | Prefer modern `Math` APIs over legacy patterns. || 🔧 | |

rules/prefer-math-min-max.js

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
'use strict';
2+
const {fixSpaceAroundKeyword} = require('./fix/index.js');
3+
4+
const MESSAGE_ID = 'prefer-math-min-max';
5+
const messages = {
6+
[MESSAGE_ID]: 'Prefer `Math.{{method}}()` to simplify ternary expressions.',
7+
};
8+
9+
/** @param {import('eslint').Rule.RuleContext} context */
10+
const create = context => ({
11+
/** @param {import('estree').ConditionalExpression} conditionalExpression */
12+
ConditionalExpression(conditionalExpression) {
13+
const {test, consequent, alternate} = conditionalExpression;
14+
15+
if (test.type !== 'BinaryExpression') {
16+
return;
17+
}
18+
19+
const {operator, left, right} = test;
20+
const [leftText, rightText, alternateText, consequentText] = [left, right, alternate, consequent].map(node => context.sourceCode.getText(node));
21+
22+
const isGreaterOrEqual = operator === '>' || operator === '>=';
23+
const isLessOrEqual = operator === '<' || operator === '<=';
24+
25+
let method;
26+
27+
// Prefer `Math.min()`
28+
if (
29+
// `height > 50 ? 50 : height`
30+
(isGreaterOrEqual && leftText === alternateText && rightText === consequentText)
31+
// `height < 50 ? height : 50`
32+
|| (isLessOrEqual && leftText === consequentText && rightText === alternateText)
33+
) {
34+
method = 'min';
35+
} else if (
36+
// `height > 50 ? height : 50`
37+
(isGreaterOrEqual && leftText === consequentText && rightText === alternateText)
38+
// `height < 50 ? 50 : height`
39+
|| (isLessOrEqual && leftText === alternateText && rightText === consequentText)
40+
) {
41+
method = 'max';
42+
}
43+
44+
if (!method) {
45+
return;
46+
}
47+
48+
return {
49+
node: conditionalExpression,
50+
messageId: MESSAGE_ID,
51+
data: {method},
52+
/** @param {import('eslint').Rule.RuleFixer} fixer */
53+
* fix(fixer) {
54+
const {sourceCode} = context;
55+
56+
yield * fixSpaceAroundKeyword(fixer, conditionalExpression, sourceCode);
57+
58+
const argumentsText = [left, right]
59+
.map(node => node.type === 'SequenceExpression' ? `(${sourceCode.getText(node)})` : sourceCode.getText(node))
60+
.join(', ');
61+
62+
yield fixer.replaceText(conditionalExpression, `Math.${method}(${argumentsText})`);
63+
},
64+
};
65+
},
66+
});
67+
68+
/** @type {import('eslint').Rule.RuleModule} */
69+
module.exports = {
70+
create,
71+
meta: {
72+
type: 'problem',
73+
docs: {
74+
description: 'Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.',
75+
recommended: true,
76+
},
77+
fixable: 'code',
78+
messages,
79+
},
80+
};

test/package.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const RULES_WITHOUT_PASS_FAIL_SECTIONS = new Set([
3030
'filename-case',
3131
// Intended to not use `pass`/`fail` section in this rule.
3232
'prefer-modern-math-apis',
33+
'prefer-math-min-max',
3334
]);
3435

3536
test('Every rule is defined in index file in alphabetical order', t => {

test/prefer-math-min-max.mjs

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {outdent} from 'outdent';
2+
import {getTester} from './utils/test.mjs';
3+
4+
const {test} = getTester(import.meta);
5+
6+
test.snapshot({
7+
valid: [
8+
'height > 10 ? height : 20',
9+
'height > 50 ? Math.min(50, height) : height',
10+
'foo ? foo : bar',
11+
],
12+
invalid: [
13+
// Prefer `Math.min()`
14+
'height > 50 ? 50 : height',
15+
'height >= 50 ? 50 : height',
16+
'height < 50 ? height : 50',
17+
'height <= 50 ? height : 50',
18+
19+
// Prefer `Math.min()`
20+
'height > maxHeight ? maxHeight : height',
21+
'height < maxHeight ? height : maxHeight',
22+
23+
// Prefer `Math.min()`
24+
'window.height > 50 ? 50 : window.height',
25+
'window.height < 50 ? window.height : 50',
26+
27+
// Prefer `Math.max()`
28+
'height > 50 ? height : 50',
29+
'height >= 50 ? height : 50',
30+
'height < 50 ? 50 : height',
31+
'height <= 50 ? 50 : height',
32+
33+
// Prefer `Math.max()`
34+
'height > maxHeight ? height : maxHeight',
35+
'height < maxHeight ? maxHeight : height',
36+
37+
// Edge test when there is no space between ReturnStatement and ConditionalExpression
38+
outdent`
39+
function a() {
40+
return +foo > 10 ? 10 : +foo
41+
}
42+
`,
43+
outdent`
44+
function a() {
45+
return+foo > 10 ? 10 : +foo
46+
}
47+
`,
48+
49+
'(0,foo) > 10 ? 10 : (0,foo)',
50+
51+
'foo.bar() > 10 ? 10 : foo.bar()',
52+
outdent`
53+
async function foo() {
54+
return await foo.bar() > 10 ? 10 : await foo.bar()
55+
}
56+
`,
57+
outdent`
58+
async function foo() {
59+
await(+foo > 10 ? 10 : +foo)
60+
}
61+
`,
62+
outdent`
63+
function foo() {
64+
return(foo.bar() > 10) ? 10 : foo.bar()
65+
}
66+
`,
67+
outdent`
68+
function* foo() {
69+
yield+foo > 10 ? 10 : +foo
70+
}
71+
`,
72+
'export default+foo > 10 ? 10 : +foo',
73+
74+
'foo.length > bar.length ? bar.length : foo.length',
75+
],
76+
});

0 commit comments

Comments
 (0)