prefer-exponentiation-operator.js 6.6 KB

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