routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+209
@@ -0,0 +1,209 @@
|
||||
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 = [];
|
||||
|
||||
context.on('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],
|
||||
]);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
context.on('BinaryExpression', node => {
|
||||
nodes.push(node);
|
||||
});
|
||||
|
||||
context.on('Program:exit', function * () {
|
||||
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;
|
||||
Reference in New Issue
Block a user