forked from daren.hsu/line_push
313 lines
6.0 KiB
JavaScript
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'
|
|
}
|
|
};
|