diff --git a/docs/rules/prefer-exponentiation-operator.md b/docs/rules/prefer-exponentiation-operator.md new file mode 100644 index 00000000000..f5f9e47db5a --- /dev/null +++ b/docs/rules/prefer-exponentiation-operator.md @@ -0,0 +1,46 @@ +# Disallow the use of `Math.pow` in favor of the `**` operator (prefer-exponentiation-operator) + +Introduced in ES2016, the infix exponentiation operator `**` is an alternative for the standard `Math.pow` function. + +Infix notation is considered to be more readable and thus more preferable than the function notation. + +## Rule Details + +This rule disallows calls to `Math.pow` and suggests using the `**` operator instead. + +Examples of **incorrect** code for this rule: + +```js +/*eslint prefer-exponentiation-operator: "error"*/ + +const foo = Math.pow(2, 8); + +const bar = Math.pow(a, b); + +let baz = Math.pow(a + b, c + d); + +let quux = Math.pow(-1, n); +``` + +Examples of **correct** code for this rule: + +```js +/*eslint prefer-exponentiation-operator: "error"*/ + +const foo = 2 ** 8; + +const bar = a ** b; + +let baz = (a + b) ** (c + d); + +let quux = (-1) ** n; +``` + +## When Not To Use It + +This rule should not be used unless ES2016 is supported in your codebase. + +## Further Reading + +* [MDN Arithmetic Operators - Exponentiation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators#Exponentiation) +* [Issue 5848: Exponentiation operator ** has different results for numbers and variables from 50 upwards](https://bugs.chromium.org/p/v8/issues/detail?id=5848) diff --git a/lib/rules/index.js b/lib/rules/index.js index 8b0abc4ee7a..dbda93fb325 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -238,6 +238,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({ "prefer-arrow-callback": () => require("./prefer-arrow-callback"), "prefer-const": () => require("./prefer-const"), "prefer-destructuring": () => require("./prefer-destructuring"), + "prefer-exponentiation-operator": () => require("./prefer-exponentiation-operator"), "prefer-named-capture-group": () => require("./prefer-named-capture-group"), "prefer-numeric-literals": () => require("./prefer-numeric-literals"), "prefer-object-spread": () => require("./prefer-object-spread"), diff --git a/lib/rules/prefer-exponentiation-operator.js b/lib/rules/prefer-exponentiation-operator.js new file mode 100644 index 00000000000..5e75ef4724f --- /dev/null +++ b/lib/rules/prefer-exponentiation-operator.js @@ -0,0 +1,189 @@ +/** + * @fileoverview Rule to disallow Math.pow in favor of the ** operator + * @author Milos Djermanovic + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("./utils/ast-utils"); +const { CALL, ReferenceTracker } = require("eslint-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const PRECEDENCE_OF_EXPONENTIATION_EXPR = astUtils.getPrecedence({ type: "BinaryExpression", operator: "**" }); + +/** + * Determines whether the given node needs parens if used as the base in an exponentiation binary expression. + * @param {ASTNode} base The node to check. + * @returns {boolean} `true` if the node needs to be parenthesised. + */ +function doesBaseNeedParens(base) { + return ( + + // '**' is right-associative, parens are needed when Math.pow(a ** b, c) is converted to (a ** b) ** c + astUtils.getPrecedence(base) <= PRECEDENCE_OF_EXPONENTIATION_EXPR || + + // An unary operator cannot be used immediately before an exponentiation expression + base.type === "UnaryExpression" + ); +} + +/** + * Determines whether the given node needs parens if used as the exponent in an exponentiation binary expression. + * @param {ASTNode} exponent The node to check. + * @returns {boolean} `true` if the node needs to be parenthesised. + */ +function doesExponentNeedParens(exponent) { + + // '**' is right-associative, there is no need for parens when Math.pow(a, b ** c) is converted to a ** b ** c + return astUtils.getPrecedence(exponent) < PRECEDENCE_OF_EXPONENTIATION_EXPR; +} + +/** + * Determines whether an exponentiation binary expression at the place of the given node would need parens. + * @param {ASTNode} node A node that would be replaced by an exponentiation binary expression. + * @param {SourceCode} sourceCode A SourceCode object. + * @returns {boolean} `true` if the expression needs to be parenthesised. + */ +function doesExponentiationExpressionNeedParens(node, sourceCode) { + const parent = node.parent; + + const needsParens = ( + parent.type === "ClassDeclaration" || + ( + parent.type.endsWith("Expression") && + astUtils.getPrecedence(parent) >= PRECEDENCE_OF_EXPONENTIATION_EXPR && + !(parent.type === "BinaryExpression" && parent.operator === "**" && parent.right === node) && + !((parent.type === "CallExpression" || parent.type === "NewExpression") && parent.arguments.includes(node)) && + !(parent.type === "MemberExpression" && parent.computed && parent.property === node) && + !(parent.type === "ArrayExpression") + ) + ); + + return needsParens && !astUtils.isParenthesised(sourceCode, node); +} + +/** + * Optionally parenthesizes given text. + * @param {string} text The text to parenthesize. + * @param {boolean} shouldParenthesize If `true`, the text will be parenthesised. + * @returns {string} parenthesised or unchanged text. + */ +function parenthesizeIfShould(text, shouldParenthesize) { + return shouldParenthesize ? `(${text})` : text; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: "suggestion", + + docs: { + description: "disallow the use of `Math.pow` in favor of the `**` operator", + category: "Stylistic Issues", + recommended: false, + url: "https://eslint.org/docs/rules/prefer-exponentiation-operator" + }, + + schema: [], + fixable: "code", + + messages: { + useExponentiation: "Use the '**' operator instead of 'Math.pow'." + } + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + /** + * Reports the given node. + * @param {ASTNode} node 'Math.pow()' node to report. + * @returns {void} + */ + function report(node) { + context.report({ + node, + messageId: "useExponentiation", + fix(fixer) { + if ( + node.arguments.length !== 2 || + node.arguments.some(arg => arg.type === "SpreadElement") || + sourceCode.getCommentsInside(node).length > 0 + ) { + return null; + } + + const base = node.arguments[0], + exponent = node.arguments[1], + baseText = sourceCode.getText(base), + exponentText = sourceCode.getText(exponent), + shouldParenthesizeBase = doesBaseNeedParens(base), + shouldParenthesizeExponent = doesExponentNeedParens(exponent), + shouldParenthesizeAll = doesExponentiationExpressionNeedParens(node, sourceCode); + + let prefix = "", + suffix = ""; + + if (!shouldParenthesizeAll) { + if (!shouldParenthesizeBase) { + const firstReplacementToken = sourceCode.getFirstToken(base), + tokenBefore = sourceCode.getTokenBefore(node); + + if ( + tokenBefore && + tokenBefore.range[1] === node.range[0] && + !astUtils.canTokensBeAdjacent(tokenBefore, firstReplacementToken) + ) { + prefix = " "; // a+Math.pow(++b, c) -> a+ ++b**c + } + } + if (!shouldParenthesizeExponent) { + const lastReplacementToken = sourceCode.getLastToken(exponent), + tokenAfter = sourceCode.getTokenAfter(node); + + if ( + tokenAfter && + node.range[1] === tokenAfter.range[0] && + !astUtils.canTokensBeAdjacent(lastReplacementToken, tokenAfter) + ) { + suffix = " "; // Math.pow(a, b)in c -> a**b in c + } + } + } + + const baseReplacement = parenthesizeIfShould(baseText, shouldParenthesizeBase), + exponentReplacement = parenthesizeIfShould(exponentText, shouldParenthesizeExponent), + replacement = parenthesizeIfShould(`${baseReplacement}**${exponentReplacement}`, shouldParenthesizeAll); + + return fixer.replaceText(node, `${prefix}${replacement}${suffix}`); + } + }); + } + + return { + Program() { + const scope = context.getScope(); + const tracker = new ReferenceTracker(scope); + const trackMap = { + Math: { + pow: { [CALL]: true } + } + }; + + for (const { node } of tracker.iterateGlobalReferences(trackMap)) { + report(node); + } + } + }; + } +}; diff --git a/tests/lib/rules/prefer-exponentiation-operator.js b/tests/lib/rules/prefer-exponentiation-operator.js new file mode 100644 index 00000000000..03d6d57480b --- /dev/null +++ b/tests/lib/rules/prefer-exponentiation-operator.js @@ -0,0 +1,309 @@ +/** + * @fileoverview Tests for the prefer-exponentiation-operator rule + * @author Milos Djermanovic + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/prefer-exponentiation-operator"); +const { RuleTester } = require("../../../lib/rule-tester"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Create an object for the invalid array + * @param {string} code source code + * @param {string} output fixed source code + * @returns {Object} result object + * @private + */ +function invalid(code, output) { + return { + code, + output, + errors: [ + { + messageId: "useExponentiation", + type: "CallExpression" + } + ] + }; +} + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } }); + +ruleTester.run("prefer-exponentiation-operator", rule, { + valid: [ + + // not Math.pow() + "Object.pow(a, b)", + "Math.max(a, b)", + "Math", + "Math(a, b)", + "pow", + "pow(a, b)", + "Math.pow", + "Math.Pow(a, b)", + "math.pow(a, b)", + "foo.Math.pow(a, b)", + "new Math.pow(a, b)", + "Math[pow](a, b)", + + // not the global Math + "/* globals Math:off*/ Math.pow(a, b)", + "let Math; Math.pow(a, b);", + "if (foo) { const Math = 1; Math.pow(a, b); }", + "var x = function Math() { Math.pow(a, b); }", + "function foo(Math) { Math.pow(a, b); }", + "function foo() { Math.pow(a, b); var Math; }" + ], + + invalid: [ + + invalid("Math.pow(a, b)", "a**b"), + invalid("(Math).pow(a, b)", "a**b"), + invalid("Math['pow'](a, b)", "a**b"), + invalid("(Math)['pow'](a, b)", "a**b"), + invalid("var x=Math\n. pow( a, \n b )", "var x=a**b"), + + // able to catch some workarounds + invalid("Math[`pow`](a, b)", "a**b"), + invalid("Math[`${'pow'}`](a, b)", "a**b"), + invalid("Math['p' + 'o' + 'w'](a, b)", "a**b"), + + // non-expression parents that don't require parens + invalid("var x = Math.pow(a, b);", "var x = a**b;"), + invalid("if(Math.pow(a, b)){}", "if(a**b){}"), + invalid("for(;Math.pow(a, b);){}", "for(;a**b;){}"), + invalid("switch(foo){ case Math.pow(a, b): break; }", "switch(foo){ case a**b: break; }"), + invalid("{ foo: Math.pow(a, b) }", "{ foo: a**b }"), + invalid("function foo(bar, baz = Math.pow(a, b), quux){}", "function foo(bar, baz = a**b, quux){}"), + invalid("`${Math.pow(a, b)}`", "`${a**b}`"), + + // non-expression parents that do require parens + invalid("class C extends Math.pow(a, b) {}", "class C extends (a**b) {}"), + + // parents with a higher precedence + invalid("+ Math.pow(a, b)", "+ (a**b)"), + invalid("- Math.pow(a, b)", "- (a**b)"), + invalid("! Math.pow(a, b)", "! (a**b)"), + invalid("typeof Math.pow(a, b)", "typeof (a**b)"), + invalid("void Math.pow(a, b)", "void (a**b)"), + invalid("Math.pow(a, b) .toString()", "(a**b) .toString()"), + invalid("Math.pow(a, b) ()", "(a**b) ()"), + invalid("Math.pow(a, b) ``", "(a**b) ``"), + invalid("(class extends Math.pow(a, b) {})", "(class extends (a**b) {})"), + + // already parenthesised, shouldn't insert extra parens + invalid("+(Math.pow(a, b))", "+(a**b)"), + invalid("(Math.pow(a, b)).toString()", "(a**b).toString()"), + invalid("(class extends (Math.pow(a, b)) {})", "(class extends (a**b) {})"), + invalid("class C extends (Math.pow(a, b)) {}", "class C extends (a**b) {}"), + + // parents with a higher precedence, but the expression's role doesn't require parens + invalid("f(Math.pow(a, b))", "f(a**b)"), + invalid("f(foo, Math.pow(a, b))", "f(foo, a**b)"), + invalid("f(Math.pow(a, b), foo)", "f(a**b, foo)"), + invalid("f(foo, Math.pow(a, b), bar)", "f(foo, a**b, bar)"), + invalid("new F(Math.pow(a, b))", "new F(a**b)"), + invalid("new F(foo, Math.pow(a, b))", "new F(foo, a**b)"), + invalid("new F(Math.pow(a, b), foo)", "new F(a**b, foo)"), + invalid("new F(foo, Math.pow(a, b), bar)", "new F(foo, a**b, bar)"), + invalid("obj[Math.pow(a, b)]", "obj[a**b]"), + invalid("[foo, Math.pow(a, b), bar]", "[foo, a**b, bar]"), + + // parents with a lower precedence + invalid("a * Math.pow(b, c)", "a * b**c"), + invalid("Math.pow(a, b) * c", "a**b * c"), + invalid("a + Math.pow(b, c)", "a + b**c"), + invalid("Math.pow(a, b)/c", "a**b/c"), + invalid("a < Math.pow(b, c)", "a < b**c"), + invalid("Math.pow(a, b) > c", "a**b > c"), + invalid("a === Math.pow(b, c)", "a === b**c"), + invalid("a ? Math.pow(b, c) : d", "a ? b**c : d"), + invalid("a = Math.pow(b, c)", "a = b**c"), + invalid("a += Math.pow(b, c)", "a += b**c"), + invalid("function *f() { yield Math.pow(a, b) }", "function *f() { yield a**b }"), + invalid("a, Math.pow(b, c), d", "a, b**c, d"), + + // '**' is right-associative, that applies to both parent and child nodes + invalid("a ** Math.pow(b, c)", "a ** b**c"), + invalid("Math.pow(a, b) ** c", "(a**b) ** c"), + invalid("Math.pow(a, b ** c)", "a**b ** c"), + invalid("Math.pow(a ** b, c)", "(a ** b)**c"), + invalid("a ** Math.pow(b ** c, d ** e) ** f", "a ** ((b ** c)**d ** e) ** f"), + + // doesn't remove already existing unnecessary parens around the whole expression + invalid("(Math.pow(a, b))", "(a**b)"), + invalid("foo + (Math.pow(a, b))", "foo + (a**b)"), + invalid("(Math.pow(a, b)) + foo", "(a**b) + foo"), + invalid("`${(Math.pow(a, b))}`", "`${(a**b)}`"), + + // base and exponent with a higher precedence + invalid("Math.pow(2, 3)", "2**3"), + invalid("Math.pow(a.foo, b)", "a.foo**b"), + invalid("Math.pow(a, b.foo)", "a**b.foo"), + invalid("Math.pow(a(), b)", "a()**b"), + invalid("Math.pow(a, b())", "a**b()"), + invalid("Math.pow(++a, ++b)", "++a**++b"), + invalid("Math.pow(a++, ++b)", "a++**++b"), + invalid("Math.pow(a--, b--)", "a--**b--"), + invalid("Math.pow(--a, b--)", "--a**b--"), + + // doesn't preserve unnecessary parens around base and exponent + invalid("Math.pow((a), (b))", "a**b"), + invalid("Math.pow(((a)), ((b)))", "a**b"), + invalid("Math.pow((a.foo), b)", "a.foo**b"), + invalid("Math.pow(a, (b.foo))", "a**b.foo"), + invalid("Math.pow((a()), b)", "a()**b"), + invalid("Math.pow(a, (b()))", "a**b()"), + + // unary expressions are exception by the language - parens are required for the base to disambiguate operator precedence + invalid("Math.pow(+a, b)", "(+a)**b"), + invalid("Math.pow(a, +b)", "a**+b"), + invalid("Math.pow(-a, b)", "(-a)**b"), + invalid("Math.pow(a, -b)", "a**-b"), + invalid("Math.pow(-2, 3)", "(-2)**3"), + invalid("Math.pow(2, -3)", "2**-3"), + + // base and exponent with a lower precedence + invalid("Math.pow(a * b, c)", "(a * b)**c"), + invalid("Math.pow(a, b * c)", "a**(b * c)"), + invalid("Math.pow(a / b, c)", "(a / b)**c"), + invalid("Math.pow(a, b / c)", "a**(b / c)"), + invalid("Math.pow(a + b, 3)", "(a + b)**3"), + invalid("Math.pow(2, a - b)", "2**(a - b)"), + invalid("Math.pow(a + b, c + d)", "(a + b)**(c + d)"), + invalid("Math.pow(a = b, c = d)", "(a = b)**(c = d)"), + invalid("Math.pow(a += b, c -= d)", "(a += b)**(c -= d)"), + invalid("Math.pow((a, b), (c, d))", "(a, b)**(c, d)"), + invalid("function *f() { Math.pow(yield, yield) }", "function *f() { (yield)**(yield) }"), + + // doesn't put extra parens + invalid("Math.pow((a + b), (c + d))", "(a + b)**(c + d)"), + + // tokens that can be adjacent + invalid("a+Math.pow(b, c)+d", "a+b**c+d"), + + // tokens that cannot be adjacent + invalid("a+Math.pow(++b, c)", "a+ ++b**c"), + invalid("(a)+(Math).pow((++b), c)", "(a)+ ++b**c"), + invalid("Math.pow(a, b)in c", "a**b in c"), + invalid("Math.pow(a, (b))in (c)", "a**b in (c)"), + invalid("a+Math.pow(++b, c)in d", "a+ ++b**c in d"), + invalid("a+Math.pow( ++b, c )in d", "a+ ++b**c in d"), + + // tokens that cannot be adjacent, but there is already space or something else between + invalid("a+ Math.pow(++b, c) in d", "a+ ++b**c in d"), + invalid("a+/**/Math.pow(++b, c)/**/in d", "a+/**/++b**c/**/in d"), + invalid("a+(Math.pow(++b, c))in d", "a+(++b**c)in d"), + + // tokens that cannot be adjacent, but the autofix inserts parens required for precedence, so there is no need for an extra space + invalid("+Math.pow(++a, b)", "+(++a**b)"), + invalid("Math.pow(a, b + c)in d", "a**(b + c)in d"), + + // multiple invalid and full message and location test + { + code: "Math.pow(a, b) + Math.pow(c,\n d)", + output: "a**b + c**d", + errors: [ + { + message: "Use the '**' operator instead of 'Math.pow'.", + type: "CallExpression", + line: 1, + column: 1, + endLine: 1, + endColumn: 15 + }, + { + message: "Use the '**' operator instead of 'Math.pow'.", + type: "CallExpression", + line: 1, + column: 18, + endLine: 2, + endColumn: 4 + } + ] + }, + { + code: "Math.pow(Math.pow(a, b), Math.pow(c, d))", + output: "Math.pow(a, b)**Math.pow(c, d)", // tests perform only one autofix iteration, below is the following one + errors: [ + { + messageId: "useExponentiation", + type: "CallExpression", + column: 1, + endColumn: 41 + }, + { + messageId: "useExponentiation", + type: "CallExpression", + column: 10, + endColumn: 24 + }, + { + messageId: "useExponentiation", + type: "CallExpression", + column: 26, + endColumn: 40 + } + ] + }, + { + code: "Math.pow(a, b)**Math.pow(c, d)", + output: "(a**b)**c**d", + errors: [ + { + messageId: "useExponentiation", + type: "CallExpression", + column: 1, + endColumn: 15 + }, + { + messageId: "useExponentiation", + type: "CallExpression", + column: 17, + endColumn: 31 + } + ] + }, + + // shouldn't autofix if the call doesn't have exactly two arguments + invalid("Math.pow()", null), + invalid("Math.pow(a)", null), + invalid("Math.pow(a, b, c)", null), + invalid("Math.pow(a, b, c, d)", null), + + // shouldn't autofix if any of the arguments is spread + invalid("Math.pow(...a)", null), + invalid("Math.pow(...a, b)", null), + invalid("Math.pow(a, ...b)", null), + invalid("Math.pow(a, b, ...c)", null), + + // shouldn't autofix if that would remove comments + invalid("/* comment */Math.pow(a, b)", "/* comment */a**b"), + invalid("Math/**/.pow(a, b)", null), + invalid("Math//\n.pow(a, b)", null), + invalid("Math[//\n'pow'](a, b)", null), + invalid("Math['pow'/**/](a, b)", null), + invalid("Math./**/pow(a, b)", null), + invalid("Math.pow/**/(a, b)", null), + invalid("Math.pow//\n(a, b)", null), + invalid("Math.pow(/**/a, b)", null), + invalid("Math.pow(a,//\n b)", null), + invalid("Math.pow(a, b/**/)", null), + invalid("Math.pow(a, b//\n)", null), + invalid("Math.pow(a, b)/* comment */;", "a**b/* comment */;"), + invalid("Math.pow(a, b)// comment\n;", "a**b// comment\n;") + ] +}); diff --git a/tools/rule-types.json b/tools/rule-types.json index 6c72abb6cb2..4bc07d4e43e 100644 --- a/tools/rule-types.json +++ b/tools/rule-types.json @@ -225,6 +225,7 @@ "prefer-arrow-callback": "suggestion", "prefer-const": "suggestion", "prefer-destructuring": "suggestion", + "prefer-exponentiation-operator": "suggestion", "prefer-named-capture-group": "suggestion", "prefer-numeric-literals": "suggestion", "prefer-object-spread": "suggestion",