210 lines
5.7 KiB
JavaScript
210 lines
5.7 KiB
JavaScript
import {getParenthesizedText, getParenthesizedRange, isSameReference} from './utils/index.js';
|
|
import {isLiteral, isMethodCall} from './ast/index.js';
|
|
import {replaceNodeOrTokenAndSpacesBefore, removeParentheses} from './fix/index.js';
|
|
|
|
const MESSAGE_ID = 'prefer-modern-math-apis';
|
|
const messages = {
|
|
[MESSAGE_ID]: 'Prefer `{{replacement}}` over `{{description}}`.',
|
|
};
|
|
|
|
const isMathProperty = (node, property) =>
|
|
node.type === 'MemberExpression'
|
|
&& !node.optional
|
|
&& !node.computed
|
|
&& node.object.type === 'Identifier'
|
|
&& node.object.name === 'Math'
|
|
&& node.property.type === 'Identifier'
|
|
&& node.property.name === property;
|
|
|
|
const isMathMethodCall = (node, method) =>
|
|
node.type === 'CallExpression'
|
|
&& !node.optional
|
|
&& isMathProperty(node.callee, method)
|
|
&& node.arguments.length === 1
|
|
&& node.arguments[0].type !== 'SpreadElement';
|
|
|
|
// `Math.log(x) * Math.LOG10E` -> `Math.log10(x)`
|
|
// `Math.LOG10E * Math.log(x)` -> `Math.log10(x)`
|
|
// `Math.log(x) * Math.LOG2E` -> `Math.log2(x)`
|
|
// `Math.LOG2E * Math.log(x)` -> `Math.log2(x)`
|
|
function createLogCallTimesConstantCheck({constantName, replacementMethod}) {
|
|
const replacement = `Math.${replacementMethod}(…)`;
|
|
|
|
return function (node, context) {
|
|
if (!(node.type === 'BinaryExpression' && node.operator === '*')) {
|
|
return;
|
|
}
|
|
|
|
let mathLogCall;
|
|
let description;
|
|
if (isMathMethodCall(node.left, 'log') && isMathProperty(node.right, constantName)) {
|
|
mathLogCall = node.left;
|
|
description = `Math.log(…) * Math.${constantName}`;
|
|
} else if (isMathMethodCall(node.right, 'log') && isMathProperty(node.left, constantName)) {
|
|
mathLogCall = node.right;
|
|
description = `Math.${constantName} * Math.log(…)`;
|
|
}
|
|
|
|
if (!mathLogCall) {
|
|
return;
|
|
}
|
|
|
|
const [valueNode] = mathLogCall.arguments;
|
|
|
|
return {
|
|
node,
|
|
messageId: MESSAGE_ID,
|
|
data: {
|
|
replacement,
|
|
description,
|
|
},
|
|
fix: fixer => fixer.replaceText(node, `Math.${replacementMethod}(${getParenthesizedText(valueNode, context)})`),
|
|
};
|
|
};
|
|
}
|
|
|
|
// `Math.log(x) / Math.LN10` -> `Math.log10(x)`
|
|
// `Math.log(x) / Math.LN2` -> `Math.log2(x)`
|
|
function createLogCallDivideConstantCheck({constantName, replacementMethod}) {
|
|
const message = {
|
|
messageId: MESSAGE_ID,
|
|
data: {
|
|
replacement: `Math.${replacementMethod}(…)`,
|
|
description: `Math.log(…) / Math.${constantName}`,
|
|
},
|
|
};
|
|
|
|
return function (node, context) {
|
|
if (
|
|
!(
|
|
node.type === 'BinaryExpression'
|
|
&& node.operator === '/'
|
|
&& isMathMethodCall(node.left, 'log')
|
|
&& isMathProperty(node.right, constantName)
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const [valueNode] = node.left.arguments;
|
|
|
|
return {
|
|
...message,
|
|
node,
|
|
fix: fixer => fixer.replaceText(node, `Math.${replacementMethod}(${getParenthesizedText(valueNode, context)})`),
|
|
};
|
|
};
|
|
}
|
|
|
|
const checkFunctions = [
|
|
createLogCallTimesConstantCheck({constantName: 'LOG10E', replacementMethod: 'log10'}),
|
|
createLogCallTimesConstantCheck({constantName: 'LOG2E', replacementMethod: 'log2'}),
|
|
createLogCallDivideConstantCheck({constantName: 'LN10', replacementMethod: 'log10'}),
|
|
createLogCallDivideConstantCheck({constantName: 'LN2', replacementMethod: 'log2'}),
|
|
];
|
|
|
|
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 {
|
|
CallExpression(callExpression) {
|
|
if (!isMethodCall(callExpression, {
|
|
object: 'Math',
|
|
method: 'sqrt',
|
|
argumentsLength: 1,
|
|
optionalCall: false,
|
|
optionalMember: false,
|
|
})) {
|
|
return;
|
|
}
|
|
|
|
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, context);
|
|
yield removeParentheses(expression, fixer, context);
|
|
}
|
|
|
|
// `x ** 2` => `x`
|
|
// `x * a` => `x`
|
|
for (const expression of expressions) {
|
|
yield fixer.removeRange([
|
|
getParenthesizedRange(expression.left, context)[1],
|
|
sourceCode.getRange(expression)[1],
|
|
]);
|
|
}
|
|
},
|
|
};
|
|
},
|
|
|
|
BinaryExpression(node) {
|
|
nodes.push(node);
|
|
},
|
|
* 'Program:exit'() {
|
|
for (const node of nodes) {
|
|
for (const getProblem of checkFunctions) {
|
|
const problem = getProblem(node, context);
|
|
|
|
if (problem) {
|
|
yield problem;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
};
|
|
|
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
const config = {
|
|
create,
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: {
|
|
description: 'Prefer modern `Math` APIs over legacy patterns.',
|
|
recommended: 'unopinionated',
|
|
},
|
|
fixable: 'code',
|
|
messages,
|
|
},
|
|
};
|
|
|
|
export default config;
|