line_push/node_modules/eslint-plugin-unicorn/rules/prefer-negative-index.js
2022-07-21 03:28:35 +00:00

313 lines
6.0 KiB
JavaScript

'use strict';
const getDocumentationUrl = require('./utils/get-documentation-url');
const isLiteralValue = require('./utils/is-literal-value');
const methods = new Map([
[
'slice',
{
argumentsIndexes: [0, 1],
supportObjects: new Set([
'Array',
'String',
'ArrayBuffer',
'Int8Array',
'Uint8Array',
'Uint8ClampedArray',
'Int16Array',
'Uint16Array',
'Int32Array',
'Uint32Array',
'Float32Array',
'Float64Array',
'BigInt64Array',
'BigUint64Array'
// `{Blob,File}#slice()` are not generally used
// 'Blob'
// 'File'
])
}
],
[
'splice',
{
argumentsIndexes: [0],
supportObjects: new Set([
'Array'
])
}
]
]);
const OPERATOR_MINUS = '-';
const isPropertiesEqual = (node1, node2) => properties => {
return properties.every(property => isEqual(node1[property], node2[property]));
};
const isTemplateElementEqual = (node1, node2) => {
return (
node1.value &&
node2.value &&
node1.tail === node2.tail &&
isPropertiesEqual(node1.value, node2.value)(['cooked', 'raw'])
);
};
const isTemplateLiteralEqual = (node1, node2) => {
const {quasis: quasis1} = node1;
const {quasis: quasis2} = node2;
return (
quasis1.length === quasis2.length &&
quasis1.every((templateElement, index) =>
isEqual(templateElement, quasis2[index])
)
);
};
const isEqual = (node1, node2) => {
if (node1 === node2) {
return true;
}
const compare = isPropertiesEqual(node1, node2);
if (!compare(['type'])) {
return false;
}
const {type} = node1;
switch (type) {
case 'Identifier':
return compare(['name', 'computed']);
case 'Literal':
return compare(['value', 'raw']);
case 'TemplateLiteral':
return isTemplateLiteralEqual(node1, node2);
case 'TemplateElement':
return isTemplateElementEqual(node1, node2);
case 'BinaryExpression':
return compare(['operator', 'left', 'right']);
case 'MemberExpression':
return compare(['object', 'property']);
default:
return false;
}
};
const isLengthMemberExpression = node => node &&
node.type === 'MemberExpression' &&
node.property &&
node.property.type === 'Identifier' &&
node.property.name === 'length' &&
node.object;
const isLiteralPositiveValue = node =>
node &&
node.type === 'Literal' &&
typeof node.value === 'number' &&
node.value > 0;
const getLengthMemberExpression = node => {
if (!node) {
return;
}
const {type, operator, left, right} = node;
if (
type !== 'BinaryExpression' ||
operator !== OPERATOR_MINUS ||
!left ||
!isLiteralPositiveValue(right)
) {
return;
}
if (isLengthMemberExpression(left)) {
return left;
}
// Nested BinaryExpression
return getLengthMemberExpression(left);
};
const getRemoveAbleNode = (target, argument) => {
const lengthMemberExpression = getLengthMemberExpression(argument);
if (
lengthMemberExpression &&
isEqual(target, lengthMemberExpression.object)
) {
return lengthMemberExpression;
}
};
const getRemovalRange = (node, sourceCode) => {
let before = sourceCode.getTokenBefore(node);
let after = sourceCode.getTokenAfter(node);
let [start, end] = node.range;
let hasParentheses = true;
while (hasParentheses) {
hasParentheses =
before.type === 'Punctuator' &&
before.value === '(' &&
after.type === 'Punctuator' &&
after.value === ')';
if (hasParentheses) {
before = sourceCode.getTokenBefore(before);
after = sourceCode.getTokenAfter(after);
start = before.range[1];
end = after.range[0];
}
}
const [nextStart] = after.range;
const textBetween = sourceCode.text.slice(end, nextStart);
end += textBetween.match(/\S|$/).index;
return [start, end];
};
const getMemberName = node => {
const {type, property} = node;
if (
type === 'MemberExpression' &&
property &&
property.type === 'Identifier'
) {
return property.name;
}
};
function parse(node) {
const {callee, arguments: originalArguments} = node;
let method = callee.property.name;
let target = callee.object;
let argumentsNodes = originalArguments;
if (methods.has(method)) {
return {
method,
target,
argumentsNodes
};
}
if (method !== 'call' && method !== 'apply') {
return;
}
const isApply = method === 'apply';
method = getMemberName(callee.object);
if (!methods.has(method)) {
return;
}
const {supportObjects} = methods.get(method);
const parentCallee = callee.object.object;
if (
// [].{slice,splice}
(
parentCallee.type === 'ArrayExpression' &&
parentCallee.elements.length === 0
) ||
// ''.slice
(
method === 'slice' &&
isLiteralValue(parentCallee, '')
) ||
// {Array,String...}.prototype.slice
// Array.prototype.splice
(
getMemberName(parentCallee) === 'prototype' &&
parentCallee.object.type === 'Identifier' &&
supportObjects.has(parentCallee.object.name)
)
) {
[target] = originalArguments;
if (isApply) {
const [, secondArgument] = originalArguments;
if (!secondArgument || secondArgument.type !== 'ArrayExpression') {
return;
}
argumentsNodes = secondArgument.elements;
} else {
argumentsNodes = originalArguments.slice(1);
}
return {
method,
target,
argumentsNodes
};
}
}
const create = context => ({
CallExpression: node => {
if (node.callee.type !== 'MemberExpression') {
return;
}
const parsed = parse(node);
if (!parsed) {
return;
}
const {
method,
target,
argumentsNodes
} = parsed;
const {argumentsIndexes} = methods.get(method);
const removableNodes = argumentsIndexes
.map(index => getRemoveAbleNode(target, argumentsNodes[index]))
.filter(Boolean);
if (removableNodes.length === 0) {
return;
}
context.report({
node,
message: `Prefer negative index over length minus index for \`${method}\`.`,
fix(fixer) {
const sourceCode = context.getSourceCode();
return removableNodes.map(
node => fixer.removeRange(
getRemovalRange(node, sourceCode)
)
);
}
});
}
});
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocumentationUrl(__filename)
},
fixable: 'code'
}
};