Skip to content

Commit de65de6

Browse files
mdjermanovickaicataldo
authored andcommitted
New: Add prefer-exponentiation-operator rule (fixes #10482) (#12360)
* New: Add prefer-exponentiation-operator rule (fixes #10482) * Check ClassDeclaration
1 parent c78f4a7 commit de65de6

File tree

5 files changed

+546
-0
lines changed

5 files changed

+546
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Disallow the use of `Math.pow` in favor of the `**` operator (prefer-exponentiation-operator)
2+
3+
Introduced in ES2016, the infix exponentiation operator `**` is an alternative for the standard `Math.pow` function.
4+
5+
Infix notation is considered to be more readable and thus more preferable than the function notation.
6+
7+
## Rule Details
8+
9+
This rule disallows calls to `Math.pow` and suggests using the `**` operator instead.
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```js
14+
/*eslint prefer-exponentiation-operator: "error"*/
15+
16+
const foo = Math.pow(2, 8);
17+
18+
const bar = Math.pow(a, b);
19+
20+
let baz = Math.pow(a + b, c + d);
21+
22+
let quux = Math.pow(-1, n);
23+
```
24+
25+
Examples of **correct** code for this rule:
26+
27+
```js
28+
/*eslint prefer-exponentiation-operator: "error"*/
29+
30+
const foo = 2 ** 8;
31+
32+
const bar = a ** b;
33+
34+
let baz = (a + b) ** (c + d);
35+
36+
let quux = (-1) ** n;
37+
```
38+
39+
## When Not To Use It
40+
41+
This rule should not be used unless ES2016 is supported in your codebase.
42+
43+
## Further Reading
44+
45+
* [MDN Arithmetic Operators - Exponentiation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators#Exponentiation)
46+
* [Issue 5848: Exponentiation operator ** has different results for numbers and variables from 50 upwards](https://bugs.chromium.org/p/v8/issues/detail?id=5848)

lib/rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({
238238
"prefer-arrow-callback": () => require("./prefer-arrow-callback"),
239239
"prefer-const": () => require("./prefer-const"),
240240
"prefer-destructuring": () => require("./prefer-destructuring"),
241+
"prefer-exponentiation-operator": () => require("./prefer-exponentiation-operator"),
241242
"prefer-named-capture-group": () => require("./prefer-named-capture-group"),
242243
"prefer-numeric-literals": () => require("./prefer-numeric-literals"),
243244
"prefer-object-spread": () => require("./prefer-object-spread"),
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* @fileoverview Rule to disallow Math.pow in favor of the ** operator
3+
* @author Milos Djermanovic
4+
*/
5+
6+
"use strict";
7+
8+
//------------------------------------------------------------------------------
9+
// Requirements
10+
//------------------------------------------------------------------------------
11+
12+
const astUtils = require("./utils/ast-utils");
13+
const { CALL, ReferenceTracker } = require("eslint-utils");
14+
15+
//------------------------------------------------------------------------------
16+
// Helpers
17+
//------------------------------------------------------------------------------
18+
19+
const PRECEDENCE_OF_EXPONENTIATION_EXPR = astUtils.getPrecedence({ type: "BinaryExpression", operator: "**" });
20+
21+
/**
22+
* Determines whether the given node needs parens if used as the base in an exponentiation binary expression.
23+
* @param {ASTNode} base The node to check.
24+
* @returns {boolean} `true` if the node needs to be parenthesised.
25+
*/
26+
function doesBaseNeedParens(base) {
27+
return (
28+
29+
// '**' is right-associative, parens are needed when Math.pow(a ** b, c) is converted to (a ** b) ** c
30+
astUtils.getPrecedence(base) <= PRECEDENCE_OF_EXPONENTIATION_EXPR ||
31+
32+
// An unary operator cannot be used immediately before an exponentiation expression
33+
base.type === "UnaryExpression"
34+
);
35+
}
36+
37+
/**
38+
* Determines whether the given node needs parens if used as the exponent in an exponentiation binary expression.
39+
* @param {ASTNode} exponent The node to check.
40+
* @returns {boolean} `true` if the node needs to be parenthesised.
41+
*/
42+
function doesExponentNeedParens(exponent) {
43+
44+
// '**' is right-associative, there is no need for parens when Math.pow(a, b ** c) is converted to a ** b ** c
45+
return astUtils.getPrecedence(exponent) < PRECEDENCE_OF_EXPONENTIATION_EXPR;
46+
}
47+
48+
/**
49+
* Determines whether an exponentiation binary expression at the place of the given node would need parens.
50+
* @param {ASTNode} node A node that would be replaced by an exponentiation binary expression.
51+
* @param {SourceCode} sourceCode A SourceCode object.
52+
* @returns {boolean} `true` if the expression needs to be parenthesised.
53+
*/
54+
function doesExponentiationExpressionNeedParens(node, sourceCode) {
55+
const parent = node.parent;
56+
57+
const needsParens = (
58+
parent.type === "ClassDeclaration" ||
59+
(
60+
parent.type.endsWith("Expression") &&
61+
astUtils.getPrecedence(parent) >= PRECEDENCE_OF_EXPONENTIATION_EXPR &&
62+
!(parent.type === "BinaryExpression" && parent.operator === "**" && parent.right === node) &&
63+
!((parent.type === "CallExpression" || parent.type === "NewExpression") && parent.arguments.includes(node)) &&
64+
!(parent.type === "MemberExpression" && parent.computed && parent.property === node) &&
65+
!(parent.type === "ArrayExpression")
66+
)
67+
);
68+
69+
return needsParens && !astUtils.isParenthesised(sourceCode, node);
70+
}
71+
72+
/**
73+
* Optionally parenthesizes given text.
74+
* @param {string} text The text to parenthesize.
75+
* @param {boolean} shouldParenthesize If `true`, the text will be parenthesised.
76+
* @returns {string} parenthesised or unchanged text.
77+
*/
78+
function parenthesizeIfShould(text, shouldParenthesize) {
79+
return shouldParenthesize ? `(${text})` : text;
80+
}
81+
82+
//------------------------------------------------------------------------------
83+
// Rule Definition
84+
//------------------------------------------------------------------------------
85+
86+
module.exports = {
87+
meta: {
88+
type: "suggestion",
89+
90+
docs: {
91+
description: "disallow the use of `Math.pow` in favor of the `**` operator",
92+
category: "Stylistic Issues",
93+
recommended: false,
94+
url: "https://eslint.org/docs/rules/prefer-exponentiation-operator"
95+
},
96+
97+
schema: [],
98+
fixable: "code",
99+
100+
messages: {
101+
useExponentiation: "Use the '**' operator instead of 'Math.pow'."
102+
}
103+
},
104+
105+
create(context) {
106+
const sourceCode = context.getSourceCode();
107+
108+
/**
109+
* Reports the given node.
110+
* @param {ASTNode} node 'Math.pow()' node to report.
111+
* @returns {void}
112+
*/
113+
function report(node) {
114+
context.report({
115+
node,
116+
messageId: "useExponentiation",
117+
fix(fixer) {
118+
if (
119+
node.arguments.length !== 2 ||
120+
node.arguments.some(arg => arg.type === "SpreadElement") ||
121+
sourceCode.getCommentsInside(node).length > 0
122+
) {
123+
return null;
124+
}
125+
126+
const base = node.arguments[0],
127+
exponent = node.arguments[1],
128+
baseText = sourceCode.getText(base),
129+
exponentText = sourceCode.getText(exponent),
130+
shouldParenthesizeBase = doesBaseNeedParens(base),
131+
shouldParenthesizeExponent = doesExponentNeedParens(exponent),
132+
shouldParenthesizeAll = doesExponentiationExpressionNeedParens(node, sourceCode);
133+
134+
let prefix = "",
135+
suffix = "";
136+
137+
if (!shouldParenthesizeAll) {
138+
if (!shouldParenthesizeBase) {
139+
const firstReplacementToken = sourceCode.getFirstToken(base),
140+
tokenBefore = sourceCode.getTokenBefore(node);
141+
142+
if (
143+
tokenBefore &&
144+
tokenBefore.range[1] === node.range[0] &&
145+
!astUtils.canTokensBeAdjacent(tokenBefore, firstReplacementToken)
146+
) {
147+
prefix = " "; // a+Math.pow(++b, c) -> a+ ++b**c
148+
}
149+
}
150+
if (!shouldParenthesizeExponent) {
151+
const lastReplacementToken = sourceCode.getLastToken(exponent),
152+
tokenAfter = sourceCode.getTokenAfter(node);
153+
154+
if (
155+
tokenAfter &&
156+
node.range[1] === tokenAfter.range[0] &&
157+
!astUtils.canTokensBeAdjacent(lastReplacementToken, tokenAfter)
158+
) {
159+
suffix = " "; // Math.pow(a, b)in c -> a**b in c
160+
}
161+
}
162+
}
163+
164+
const baseReplacement = parenthesizeIfShould(baseText, shouldParenthesizeBase),
165+
exponentReplacement = parenthesizeIfShould(exponentText, shouldParenthesizeExponent),
166+
replacement = parenthesizeIfShould(`${baseReplacement}**${exponentReplacement}`, shouldParenthesizeAll);
167+
168+
return fixer.replaceText(node, `${prefix}${replacement}${suffix}`);
169+
}
170+
});
171+
}
172+
173+
return {
174+
Program() {
175+
const scope = context.getScope();
176+
const tracker = new ReferenceTracker(scope);
177+
const trackMap = {
178+
Math: {
179+
pow: { [CALL]: true }
180+
}
181+
};
182+
183+
for (const { node } of tracker.iterateGlobalReferences(trackMap)) {
184+
report(node);
185+
}
186+
}
187+
};
188+
}
189+
};

0 commit comments

Comments
 (0)