prefer-modern-math-apis.js 3.6 KB

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