Skip to content

Commit

Permalink
prefer-modern-math-api: Check cases that Math.hypot() should be p…
Browse files Browse the repository at this point in the history
…referred (#2080)
  • Loading branch information
fisker committed May 9, 2023
1 parent db4f78a commit 9bbc948
Show file tree
Hide file tree
Showing 5 changed files with 340 additions and 1 deletion.
21 changes: 21 additions & 0 deletions docs/rules/prefer-modern-math-apis.md
Expand Up @@ -59,6 +59,27 @@ Math.LOG2E * Math.log(x)
Math.log(x) / Math.LN2
```

## Prefer `Math.hypot(…)` over

```js
Math.sqrt(a * a + b * b)
```

```js
Math.sqrt(a ** 2 + b ** 2)
```

```js
Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2))
```

*This case requires [`prefer-exponentiation-operator`](https://eslint.org/docs/latest/rules/prefer-exponentiation-operator) rule to fix it first.*

```js
Math.sqrt(x ** 2)
// This case fix to `Math.abs(x)`, since it should be better than `Math.hypot(x)`
```

## Separate rule for `Math.trunc()`

See [`unicorn/prefer-math-trunc`](./prefer-math-trunc.md) rule.
66 changes: 65 additions & 1 deletion rules/prefer-modern-math-apis.js
@@ -1,5 +1,9 @@
'use strict';
const {getParenthesizedText} = require('./utils/parentheses.js');
const {getParenthesizedText, getParenthesizedRange} = require('./utils/parentheses.js');
const {methodCallSelector} = require('./selectors/index.js');
const isSameReference = require('./utils/is-same-reference.js');
const {isLiteral} = require('./ast/index.js');
const {replaceNodeOrTokenAndSpacesBefore, removeParentheses} = require('./fix/index.js');

const MESSAGE_ID = 'prefer-modern-math-apis';
const messages = {
Expand Down Expand Up @@ -102,11 +106,71 @@ const checkFunctions = [
createLogCallDivideConstantCheck({constantName: 'LN2', replacementMethod: 'log2'}),
];

const mathSqrtCallSelector = methodCallSelector({object: 'Math', method: 'sqrt', argumentsLength: 1});

const isPlusExpression = node => node.type === 'BinaryExpression' && node.operator === '+';

const isPow2Expression = node =>
node.type === 'BinaryExpression'
&& (
// `x * x`
(node.operator === '*' && isSameReference(node.left, node.right))
// `x ** 2`
|| (node.operator === '**' && isLiteral(node.right, 2))
);

const flatPlusExpression = node =>
isPlusExpression(node)
? [node.left, node.right].flatMap(child => flatPlusExpression(child))
: [node];

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const nodes = [];

return {
[mathSqrtCallSelector](callExpression) {
const expressions = flatPlusExpression(callExpression.arguments[0]);
if (expressions.some(expression => !isPow2Expression(expression))) {
return;
}

const replacementMethod = expressions.length === 1 ? 'abs' : 'hypot';
const plusExpressions = new Set(expressions.length === 1 ? [] : expressions.map(expression => expression.parent));

return {
node: callExpression.callee.property,
messageId: MESSAGE_ID,
data: {
replacement: `Math.${replacementMethod}(…)`,
description: 'Math.sqrt(…)',
},
* fix(fixer) {
const {sourceCode} = context;

// `Math.sqrt` -> `Math.{hypot,abs}`
yield fixer.replaceText(callExpression.callee.property, replacementMethod);

// `a ** 2 + b ** 2` -> `a, b`
for (const expression of plusExpressions) {
const plusToken = sourceCode.getTokenAfter(expression.left, token => token.type === 'Punctuator' && token.value === '+');

yield * replaceNodeOrTokenAndSpacesBefore(plusToken, ',', fixer, sourceCode);
yield * removeParentheses(expression, fixer, sourceCode);
}

// `x ** 2` => `x`
// `x * a` => `x`
for (const expression of expressions) {
yield fixer.removeRange([
getParenthesizedRange(expression.left, sourceCode)[1],
expression.range[1],
]);
}
},
};
},

BinaryExpression(node) {
nodes.push(node);
},
Expand Down
30 changes: 30 additions & 0 deletions test/prefer-modern-math-apis.mjs
Expand Up @@ -65,3 +65,33 @@ test.snapshot({
`,
].flatMap(code => duplicateLog10Test(code)),
});

// `Math.hypot`
test.snapshot({
valid: [
'Math.notSqrt(a ** 2 + b ** 2)',
'NotMath.sqrt(a ** 2 + b ** 2)',
'Math.sqrt(a ** 2 - b ** 2)',
'Math.sqrt(a ** 2 + 2 ** b)',
'Math.sqrt(a * c + b * c)',
'Math.sqrt((++a) * (++a))',
// Leave this to `prefer-exponentiation-operator` rule
'Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2))',
],
invalid: [
'Math.sqrt(a * a + b * b)',
'Math.sqrt(a ** 2 + b ** 2)',
'Math.sqrt(a * a + b ** 2)',
'Math.sqrt(a * a + b * b + c * c)',
'Math.sqrt(a ** 2 + b ** 2 + c ** 2)',
'Math.sqrt(a * a)',
'Math.sqrt(a ** 2)',
'Math.sqrt(a * a,)',
'Math.sqrt(a ** 2,)',
'Math.sqrt((a, b) ** 2)',
'Math.sqrt((++a) ** 2)',
'Math.sqrt(a * a + b * b,)',
'Math.sqrt(a ** 2 + b ** 2,)',
'Math.sqrt((( a ** 2 )) + (( b ** 2 + c ** 2 )) + (( d )) * (( d )) + (( e )) ** (( 2 )))',
],
});
224 changes: 224 additions & 0 deletions test/snapshots/prefer-modern-math-apis.mjs.md
Expand Up @@ -257,3 +257,227 @@ Generated by [AVA](https://avajs.dev).
5 | );␊
6 | }␊
`

## Invalid #1
1 | Math.sqrt(a * a + b * b)

> Output
`␊
1 | Math.hypot(a, b)␊
`

> Error 1/1
`␊
> 1 | Math.sqrt(a * a + b * b)␊
| ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊
`

## Invalid #2
1 | Math.sqrt(a ** 2 + b ** 2)

> Output
`␊
1 | Math.hypot(a, b)␊
`

> Error 1/1
`␊
> 1 | Math.sqrt(a ** 2 + b ** 2)␊
| ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊
`

## Invalid #3
1 | Math.sqrt(a * a + b ** 2)

> Output
`␊
1 | Math.hypot(a, b)␊
`

> Error 1/1
`␊
> 1 | Math.sqrt(a * a + b ** 2)␊
| ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊
`

## Invalid #4
1 | Math.sqrt(a * a + b * b + c * c)

> Output
`␊
1 | Math.hypot(a, b, c)␊
`

> Error 1/1
`␊
> 1 | Math.sqrt(a * a + b * b + c * c)␊
| ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊
`

## Invalid #5
1 | Math.sqrt(a ** 2 + b ** 2 + c ** 2)

> Output
`␊
1 | Math.hypot(a, b, c)␊
`

> Error 1/1
`␊
> 1 | Math.sqrt(a ** 2 + b ** 2 + c ** 2)␊
| ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊
`

## Invalid #6
1 | Math.sqrt(a * a)

> Output
`␊
1 | Math.abs(a)␊
`

> Error 1/1
`␊
> 1 | Math.sqrt(a * a)␊
| ^^^^ Prefer \`Math.abs(…)\` over \`Math.sqrt(…)\`.␊
`

## Invalid #7
1 | Math.sqrt(a ** 2)

> Output
`␊
1 | Math.abs(a)␊
`

> Error 1/1
`␊
> 1 | Math.sqrt(a ** 2)␊
| ^^^^ Prefer \`Math.abs(…)\` over \`Math.sqrt(…)\`.␊
`

## Invalid #8
1 | Math.sqrt(a * a,)

> Output
`␊
1 | Math.abs(a,)␊
`

> Error 1/1
`␊
> 1 | Math.sqrt(a * a,)␊
| ^^^^ Prefer \`Math.abs(…)\` over \`Math.sqrt(…)\`.␊
`

## Invalid #9
1 | Math.sqrt(a ** 2,)

> Output
`␊
1 | Math.abs(a,)␊
`

> Error 1/1
`␊
> 1 | Math.sqrt(a ** 2,)␊
| ^^^^ Prefer \`Math.abs(…)\` over \`Math.sqrt(…)\`.␊
`

## Invalid #10
1 | Math.sqrt((a, b) ** 2)

> Output
`␊
1 | Math.abs((a, b))␊
`

> Error 1/1
`␊
> 1 | Math.sqrt((a, b) ** 2)␊
| ^^^^ Prefer \`Math.abs(…)\` over \`Math.sqrt(…)\`.␊
`

## Invalid #11
1 | Math.sqrt((++a) ** 2)

> Output
`␊
1 | Math.abs((++a))␊
`

> Error 1/1
`␊
> 1 | Math.sqrt((++a) ** 2)␊
| ^^^^ Prefer \`Math.abs(…)\` over \`Math.sqrt(…)\`.␊
`

## Invalid #12
1 | Math.sqrt(a * a + b * b,)

> Output
`␊
1 | Math.hypot(a, b,)␊
`

> Error 1/1
`␊
> 1 | Math.sqrt(a * a + b * b,)␊
| ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊
`

## Invalid #13
1 | Math.sqrt(a ** 2 + b ** 2,)

> Output
`␊
1 | Math.hypot(a, b,)␊
`

> Error 1/1
`␊
> 1 | Math.sqrt(a ** 2 + b ** 2,)␊
| ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊
`

## Invalid #14
1 | Math.sqrt((( a ** 2 )) + (( b ** 2 + c ** 2 )) + (( d )) * (( d )) + (( e )) ** (( 2 )))

> Output
`␊
1 | Math.hypot((( a )), b, c , (( d )), (( e )))␊
`

> Error 1/1
`␊
> 1 | Math.sqrt((( a ** 2 )) + (( b ** 2 + c ** 2 )) + (( d )) * (( d )) + (( e )) ** (( 2 )))␊
| ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊
`
Binary file modified test/snapshots/prefer-modern-math-apis.mjs.snap
Binary file not shown.

0 comments on commit 9bbc948

Please sign in to comment.