Skip to content

Commit 6dfdeb0

Browse files
authored
Add prefer-modern-math-apis rule (#1780)
1 parent ce8a4b7 commit 6dfdeb0

File tree

8 files changed

+534
-1
lines changed

8 files changed

+534
-1
lines changed

configs/recommended.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ module.exports = {
8484
'unicorn/prefer-keyboard-event-key': 'error',
8585
'unicorn/prefer-math-trunc': 'error',
8686
'unicorn/prefer-modern-dom-apis': 'error',
87+
'unicorn/prefer-modern-math-apis': 'error',
8788
'unicorn/prefer-module': 'error',
8889
'unicorn/prefer-negative-index': 'error',
8990
'unicorn/prefer-node-protocol': 'error',

docs/rules/prefer-modern-math-apis.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Prefer modern `Math` APIs over legacy patterns
2+
3+
<!-- Do not manually modify RULE_NOTICE part. Run: `npm run generate-rule-notices` -->
4+
<!-- RULE_NOTICE -->
5+
*This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.*
6+
7+
🔧 *This rule is [auto-fixable](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).*
8+
<!-- /RULE_NOTICE -->
9+
10+
Math additions in ES2015:
11+
12+
- [Math.sign()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign)
13+
- [Math.trunc()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc)
14+
- [Math.cbrt()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cbrt)
15+
- [Math.expm1()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/expm1)
16+
- [Math.log1p()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log1p)
17+
- [Math.log10()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log10)
18+
- [Math.log2()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log2)
19+
- [Math.sinh()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sinh)
20+
- [Math.cosh()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cosh)
21+
- [Math.tanh()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tanh)
22+
- [Math.asinh()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asinh)
23+
- [Math.acosh()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acosh)
24+
- [Math.atanh()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atanh)
25+
- [Math.hypot()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/hypot)
26+
- [Math.clz32()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32)
27+
- [Math.imul()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul)
28+
- [Math.fround()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround)
29+
30+
Currently, we only check a few known cases, but we are open to add more patterns.
31+
32+
If you find a suitable case for this rule, please [open an issue](https://github.com/sindresorhus/eslint-plugin-unicorn/issues/new?title=%20%60prefer-modern-math-apis%60%20%20change%20request&labels=evaluating).
33+
34+
## Prefer `Math.log10(x)` over
35+
36+
```js
37+
Math.log(x) * Math.LOG10E
38+
```
39+
40+
```js
41+
Math.LOG10E * Math.log(x)
42+
```
43+
44+
```js
45+
Math.log(x) / Math.LN10
46+
```
47+
48+
## Prefer `Math.log2(x)` over
49+
50+
```js
51+
Math.log(x) * Math.LOG2E
52+
```
53+
54+
```js
55+
Math.LOG2E * Math.log(x)
56+
```
57+
58+
```js
59+
Math.log(x) / Math.LN2
60+
```
61+
62+
## Separate rule for `Math.trunc()`
63+
64+
See [`unicorn/prefer-math-trunc`](./prefer-math-trunc.md) rule.

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ Each rule has emojis denoting:
124124
| [prefer-keyboard-event-key](docs/rules/prefer-keyboard-event-key.md) | Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. || 🔧 | |
125125
| [prefer-math-trunc](docs/rules/prefer-math-trunc.md) | Enforce the use of `Math.trunc` instead of bitwise operators. || 🔧 | 💡 |
126126
| [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()`. || 🔧 | |
127+
| [prefer-modern-math-apis](docs/rules/prefer-modern-math-apis.md) | Prefer modern `Math` APIs over legacy patterns. || 🔧 | |
127128
| [prefer-module](docs/rules/prefer-module.md) | Prefer JavaScript modules (ESM) over CommonJS. || 🔧 | 💡 |
128129
| [prefer-negative-index](docs/rules/prefer-negative-index.md) | Prefer negative index over `.length - index` for `{String,Array,TypedArray}#slice()`, `Array#splice()` and `Array#at()`. || 🔧 | |
129130
| [prefer-node-protocol](docs/rules/prefer-node-protocol.md) | Prefer using the `node:` protocol when importing Node.js builtin modules. || 🔧 | |

rules/prefer-modern-math-apis.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
'use strict';
2+
const {getParenthesizedText} = require('./utils/parentheses.js');
3+
4+
const MESSAGE_ID = 'prefer-modern-math-apis';
5+
const messages = {
6+
[MESSAGE_ID]: 'Prefer `{{replacement}}` over `{{description}}`.',
7+
};
8+
9+
const isMathProperty = (node, property) =>
10+
node.type === 'MemberExpression'
11+
&& !node.optional
12+
&& !node.computed
13+
&& node.object.type === 'Identifier'
14+
&& node.object.name === 'Math'
15+
&& node.property.type === 'Identifier'
16+
&& node.property.name === property;
17+
18+
const isMathMethodCall = (node, method) =>
19+
node.type === 'CallExpression'
20+
&& !node.optional
21+
&& isMathProperty(node.callee, method)
22+
&& node.arguments.length === 1
23+
&& node.arguments[0].type !== 'SpreadElement';
24+
25+
// `Math.log(x) * Math.LOG10E` -> `Math.log10(x)`
26+
// `Math.LOG10E * Math.log(x)` -> `Math.log10(x)`
27+
// `Math.log(x) * Math.LOG2E` -> `Math.log2(x)`
28+
// `Math.LOG2E * Math.log(x)` -> `Math.log2(x)`
29+
function createLogCallTimesConstantCheck({constantName, replacementMethod}) {
30+
const replacement = `Math.${replacementMethod}(…)`;
31+
32+
return function (node, context) {
33+
if (!(node.type === 'BinaryExpression' && node.operator === '*')) {
34+
return;
35+
}
36+
37+
let mathLogCall;
38+
let description;
39+
if (isMathMethodCall(node.left, 'log') && isMathProperty(node.right, constantName)) {
40+
mathLogCall = node.left;
41+
description = `Math.log(…) * Math.${constantName}`;
42+
} else if (isMathMethodCall(node.right, 'log') && isMathProperty(node.left, constantName)) {
43+
mathLogCall = node.right;
44+
description = `Math.${constantName} * Math.log(…)`;
45+
}
46+
47+
if (!mathLogCall) {
48+
return;
49+
}
50+
51+
const [valueNode] = mathLogCall.arguments;
52+
53+
return {
54+
node,
55+
messageId: MESSAGE_ID,
56+
data: {
57+
replacement,
58+
description,
59+
},
60+
fix: fixer => fixer.replaceText(node, `Math.${replacementMethod}(${getParenthesizedText(valueNode, context.getSourceCode())})`),
61+
};
62+
};
63+
}
64+
65+
// `Math.log(x) / Math.LN10` -> `Math.log10(x)`
66+
// `Math.log(x) / Math.LN2` -> `Math.log2(x)`
67+
function createLogCallDivideConstantCheck({constantName, replacementMethod}) {
68+
const message = {
69+
messageId: MESSAGE_ID,
70+
data: {
71+
replacement: `Math.${replacementMethod}(…)`,
72+
description: `Math.log(…) / Math.${constantName}`,
73+
},
74+
};
75+
76+
return function (node, context) {
77+
if (
78+
!(
79+
node.type === 'BinaryExpression'
80+
&& node.operator === '/'
81+
&& isMathMethodCall(node.left, 'log')
82+
&& isMathProperty(node.right, constantName)
83+
)
84+
) {
85+
return;
86+
}
87+
88+
const [valueNode] = node.left.arguments;
89+
90+
return {
91+
...message,
92+
node,
93+
fix: fixer => fixer.replaceText(node, `Math.${replacementMethod}(${getParenthesizedText(valueNode, context.getSourceCode())})`),
94+
};
95+
};
96+
}
97+
98+
const checkFunctions = [
99+
createLogCallTimesConstantCheck({constantName: 'LOG10E', replacementMethod: 'log10'}),
100+
createLogCallTimesConstantCheck({constantName: 'LOG2E', replacementMethod: 'log2'}),
101+
createLogCallDivideConstantCheck({constantName: 'LN10', replacementMethod: 'log10'}),
102+
createLogCallDivideConstantCheck({constantName: 'LN2', replacementMethod: 'log2'}),
103+
];
104+
105+
/** @param {import('eslint').Rule.RuleContext} context */
106+
const create = context => {
107+
const nodes = [];
108+
109+
return {
110+
BinaryExpression(node) {
111+
nodes.push(node);
112+
},
113+
* 'Program:exit'() {
114+
for (const node of nodes) {
115+
for (const getProblem of checkFunctions) {
116+
const problem = getProblem(node, context);
117+
118+
if (problem) {
119+
yield problem;
120+
}
121+
}
122+
}
123+
},
124+
};
125+
};
126+
127+
/** @type {import('eslint').Rule.RuleModule} */
128+
module.exports = {
129+
create,
130+
meta: {
131+
type: 'suggestion',
132+
docs: {
133+
description: 'Prefer modern `Math` APIs over legacy patterns.',
134+
},
135+
fixable: 'code',
136+
messages,
137+
},
138+
};

test/package.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ function getNamedOptions(jsonSchema) {
6161
}
6262

6363
const RULES_WITHOUT_PASS_FAIL_SECTIONS = new Set([
64-
'filename-case', // Doesn't show code samples since it's just focused on filenames.
64+
// Doesn't show code samples since it's just focused on filenames.
65+
'filename-case',
66+
// Intended to not use `pass`/`fail` section in this rule.
67+
'prefer-modern-math-apis',
6568
]);
6669

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

test/prefer-modern-math-apis.mjs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import outdent from 'outdent';
2+
import {getTester} from './utils/test.mjs';
3+
4+
const {test} = getTester(import.meta);
5+
6+
// `Math.log10()` and `Math.log2()`
7+
const duplicateLog10Test = code => [
8+
code,
9+
// `Math.log2()` test
10+
code.replace(/Math\.LOG10E/g, 'Math.LOG2E').replace(/Math\.LN10/g, 'Math.LN2'),
11+
];
12+
test.snapshot({
13+
valid: [
14+
'Math.log(x) * Math.log(x)',
15+
16+
'Math.LOG10E * Math.LOG10E',
17+
'Math.log(x) * Math[LOG10E]',
18+
'Math.log(x) * LOG10E',
19+
'Math[log](x) * Math.LOG10E',
20+
'foo.Math.log(x) * Math.LOG10E',
21+
'Math.log(x) * foo.Math.LOG10E',
22+
'Math.log(x) * Math.NOT_LOG10E',
23+
'Math.log(x) * Math?.LOG10E',
24+
'Math?.log(x) * Math.LOG10E',
25+
'log(x) * Math.LOG10E',
26+
'new Math.log(x) * Math.LOG10E',
27+
'Math.not_log(x) + Math.LOG10E',
28+
'Math.log(x)[Math.LOG10E]',
29+
'Math.log() * Math.LOG10E',
30+
'Math.log(x, extraArgument) * Math.LOG10E',
31+
'Math.log(...x) * Math.LOG10E',
32+
33+
'Math.LN10 / Math.LN10',
34+
'Math.log(x) /Math[LN10]',
35+
'Math.log(x) / LN10',
36+
'Math[log](x) / Math.LN10',
37+
'foo.Math.log(x) / Math.LN10',
38+
'Math.log(x) / foo.Math.LN10',
39+
'Math.log(x) / Math.log(x)',
40+
'Math.log(x) / Math.NOT_LN10',
41+
'Math.log(x) / Math?.LN10',
42+
'Math?.log(x) / Math.LN10',
43+
'log(x) / Math.LN10',
44+
'new Math.log(x) / Math.LN10',
45+
'Math.not_log(x) + Math.LN10',
46+
'Math.log(x)[Math.LN10]',
47+
'Math.log() / Math.LN10',
48+
'Math.log(x, extraArgument) / Math.LN10',
49+
'Math.log(...x) / Math.LN10',
50+
].flatMap(code => duplicateLog10Test(code)),
51+
invalid: [
52+
'Math.log(x) * Math.LOG10E',
53+
'Math.LOG10E * Math.log(x)',
54+
'Math.log(x) / Math.LN10',
55+
'Math.log((( 0,x ))) * Math.LOG10E',
56+
'Math.LOG10E * Math.log((( 0,x )))',
57+
'Math.log((( 0,x ))) / Math.LN10',
58+
outdent`
59+
function foo(x) {
60+
return (
61+
Math.log(x)
62+
/ Math.LN10
63+
);
64+
}
65+
`,
66+
].flatMap(code => duplicateLog10Test(code)),
67+
});

0 commit comments

Comments
 (0)