325 lines
8.7 KiB
JavaScript
325 lines
8.7 KiB
JavaScript
|
'use strict';
|
||
|
const {isCommaToken} = require('@eslint-community/eslint-utils');
|
||
|
const {
|
||
|
matches,
|
||
|
newExpressionSelector,
|
||
|
methodCallSelector,
|
||
|
} = require('./selectors/index.js');
|
||
|
const typedArray = require('./shared/typed-array.js');
|
||
|
const {
|
||
|
removeParentheses,
|
||
|
fixSpaceAroundKeyword,
|
||
|
addParenthesizesToReturnOrThrowExpression,
|
||
|
} = require('./fix/index.js');
|
||
|
const isOnSameLine = require('./utils/is-on-same-line.js');
|
||
|
const {
|
||
|
isParenthesized,
|
||
|
} = require('./utils/parentheses.js');
|
||
|
const {isNewExpression} = require('./ast/index.js');
|
||
|
|
||
|
const SPREAD_IN_LIST = 'spread-in-list';
|
||
|
const ITERABLE_TO_ARRAY = 'iterable-to-array';
|
||
|
const ITERABLE_TO_ARRAY_IN_FOR_OF = 'iterable-to-array-in-for-of';
|
||
|
const ITERABLE_TO_ARRAY_IN_YIELD_STAR = 'iterable-to-array-in-yield-star';
|
||
|
const CLONE_ARRAY = 'clone-array';
|
||
|
const messages = {
|
||
|
[SPREAD_IN_LIST]: 'Spread an {{argumentType}} literal in {{parentDescription}} is unnecessary.',
|
||
|
[ITERABLE_TO_ARRAY]: '`{{parentDescription}}` accepts iterable as argument, it\'s unnecessary to convert to an array.',
|
||
|
[ITERABLE_TO_ARRAY_IN_FOR_OF]: '`for…of` can iterate over iterable, it\'s unnecessary to convert to an array.',
|
||
|
[ITERABLE_TO_ARRAY_IN_YIELD_STAR]: '`yield*` can delegate iterable, it\'s unnecessary to convert to an array.',
|
||
|
[CLONE_ARRAY]: 'Unnecessarily cloning an array.',
|
||
|
};
|
||
|
|
||
|
const uselessSpreadInListSelector = matches([
|
||
|
'ArrayExpression > SpreadElement.elements > ArrayExpression.argument',
|
||
|
'ObjectExpression > SpreadElement.properties > ObjectExpression.argument',
|
||
|
'CallExpression > SpreadElement.arguments > ArrayExpression.argument',
|
||
|
'NewExpression > SpreadElement.arguments > ArrayExpression.argument',
|
||
|
]);
|
||
|
|
||
|
const singleArraySpreadSelector = [
|
||
|
'ArrayExpression',
|
||
|
'[elements.length=1]',
|
||
|
'[elements.0.type="SpreadElement"]',
|
||
|
].join('');
|
||
|
const uselessIterableToArraySelector = matches([
|
||
|
[
|
||
|
matches([
|
||
|
newExpressionSelector({names: ['Map', 'WeakMap', 'Set', 'WeakSet'], argumentsLength: 1}),
|
||
|
newExpressionSelector({names: typedArray, minimumArguments: 1}),
|
||
|
methodCallSelector({
|
||
|
object: 'Promise',
|
||
|
methods: ['all', 'allSettled', 'any', 'race'],
|
||
|
argumentsLength: 1,
|
||
|
}),
|
||
|
methodCallSelector({
|
||
|
objects: ['Array', ...typedArray],
|
||
|
method: 'from',
|
||
|
argumentsLength: 1,
|
||
|
}),
|
||
|
methodCallSelector({object: 'Object', method: 'fromEntries', argumentsLength: 1}),
|
||
|
]),
|
||
|
' > ',
|
||
|
`${singleArraySpreadSelector}.arguments:first-child`,
|
||
|
].join(''),
|
||
|
`ForOfStatement > ${singleArraySpreadSelector}.right`,
|
||
|
`YieldExpression[delegate=true] > ${singleArraySpreadSelector}.argument`,
|
||
|
]);
|
||
|
|
||
|
const uselessArrayCloneSelector = [
|
||
|
`${singleArraySpreadSelector} > .elements:first-child > .argument`,
|
||
|
matches([
|
||
|
// Array methods returns a new array
|
||
|
methodCallSelector([
|
||
|
'concat',
|
||
|
'copyWithin',
|
||
|
'filter',
|
||
|
'flat',
|
||
|
'flatMap',
|
||
|
'map',
|
||
|
'slice',
|
||
|
'splice',
|
||
|
'toReversed',
|
||
|
'toSorted',
|
||
|
'toSpliced',
|
||
|
'with',
|
||
|
]),
|
||
|
// `String#split()`
|
||
|
methodCallSelector('split'),
|
||
|
// `Object.keys()` and `Object.values()`
|
||
|
methodCallSelector({object: 'Object', methods: ['keys', 'values'], argumentsLength: 1}),
|
||
|
// `await Promise.all()` and `await Promise.allSettled`
|
||
|
[
|
||
|
'AwaitExpression',
|
||
|
methodCallSelector({
|
||
|
object: 'Promise',
|
||
|
methods: ['all', 'allSettled'],
|
||
|
argumentsLength: 1,
|
||
|
path: 'argument',
|
||
|
}),
|
||
|
].join(''),
|
||
|
// `Array.from()`, `Array.of()`
|
||
|
methodCallSelector({object: 'Array', methods: ['from', 'of']}),
|
||
|
// `new Array()`
|
||
|
newExpressionSelector('Array'),
|
||
|
]),
|
||
|
].join('');
|
||
|
|
||
|
const parentDescriptions = {
|
||
|
ArrayExpression: 'array literal',
|
||
|
ObjectExpression: 'object literal',
|
||
|
CallExpression: 'arguments',
|
||
|
NewExpression: 'arguments',
|
||
|
};
|
||
|
|
||
|
function getCommaTokens(arrayExpression, sourceCode) {
|
||
|
let startToken = sourceCode.getFirstToken(arrayExpression);
|
||
|
|
||
|
return arrayExpression.elements.map((element, index, elements) => {
|
||
|
if (index === elements.length - 1) {
|
||
|
const penultimateToken = sourceCode.getLastToken(arrayExpression, {skip: 1});
|
||
|
if (isCommaToken(penultimateToken)) {
|
||
|
return penultimateToken;
|
||
|
}
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const commaToken = sourceCode.getTokenAfter(element || startToken, isCommaToken);
|
||
|
startToken = commaToken;
|
||
|
return commaToken;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function * unwrapSingleArraySpread(fixer, arrayExpression, sourceCode) {
|
||
|
const [
|
||
|
openingBracketToken,
|
||
|
spreadToken,
|
||
|
thirdToken,
|
||
|
] = sourceCode.getFirstTokens(arrayExpression, 3);
|
||
|
|
||
|
// `[...value]`
|
||
|
// ^
|
||
|
yield fixer.remove(openingBracketToken);
|
||
|
|
||
|
// `[...value]`
|
||
|
// ^^^
|
||
|
yield fixer.remove(spreadToken);
|
||
|
|
||
|
const [
|
||
|
commaToken,
|
||
|
closingBracketToken,
|
||
|
] = sourceCode.getLastTokens(arrayExpression, 2);
|
||
|
|
||
|
// `[...value]`
|
||
|
// ^
|
||
|
yield fixer.remove(closingBracketToken);
|
||
|
|
||
|
// `[...value,]`
|
||
|
// ^
|
||
|
if (isCommaToken(commaToken)) {
|
||
|
yield fixer.remove(commaToken);
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
```js
|
||
|
function foo() {
|
||
|
return [
|
||
|
...value,
|
||
|
];
|
||
|
}
|
||
|
```
|
||
|
*/
|
||
|
const {parent} = arrayExpression;
|
||
|
if (
|
||
|
(parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
|
||
|
&& parent.argument === arrayExpression
|
||
|
&& !isOnSameLine(openingBracketToken, thirdToken)
|
||
|
&& !isParenthesized(arrayExpression, sourceCode)
|
||
|
) {
|
||
|
yield * addParenthesizesToReturnOrThrowExpression(fixer, parent, sourceCode);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
yield * fixSpaceAroundKeyword(fixer, arrayExpression, sourceCode);
|
||
|
}
|
||
|
|
||
|
/** @param {import('eslint').Rule.RuleContext} context */
|
||
|
const create = context => {
|
||
|
const sourceCode = context.getSourceCode();
|
||
|
|
||
|
return {
|
||
|
[uselessSpreadInListSelector](spreadObject) {
|
||
|
const spreadElement = spreadObject.parent;
|
||
|
const spreadToken = sourceCode.getFirstToken(spreadElement);
|
||
|
const parentType = spreadElement.parent.type;
|
||
|
|
||
|
return {
|
||
|
node: spreadToken,
|
||
|
messageId: SPREAD_IN_LIST,
|
||
|
data: {
|
||
|
argumentType: spreadObject.type === 'ArrayExpression' ? 'array' : 'object',
|
||
|
parentDescription: parentDescriptions[parentType],
|
||
|
},
|
||
|
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
||
|
* fix(fixer) {
|
||
|
// `[...[foo]]`
|
||
|
// ^^^
|
||
|
yield fixer.remove(spreadToken);
|
||
|
|
||
|
// `[...(( [foo] ))]`
|
||
|
// ^^ ^^
|
||
|
yield * removeParentheses(spreadObject, fixer, sourceCode);
|
||
|
|
||
|
// `[...[foo]]`
|
||
|
// ^
|
||
|
const firstToken = sourceCode.getFirstToken(spreadObject);
|
||
|
yield fixer.remove(firstToken);
|
||
|
|
||
|
const [
|
||
|
penultimateToken,
|
||
|
lastToken,
|
||
|
] = sourceCode.getLastTokens(spreadObject, 2);
|
||
|
|
||
|
// `[...[foo]]`
|
||
|
// ^
|
||
|
yield fixer.remove(lastToken);
|
||
|
|
||
|
// `[...[foo,]]`
|
||
|
// ^
|
||
|
if (isCommaToken(penultimateToken)) {
|
||
|
yield fixer.remove(penultimateToken);
|
||
|
}
|
||
|
|
||
|
if (parentType !== 'CallExpression' && parentType !== 'NewExpression') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const commaTokens = getCommaTokens(spreadObject, sourceCode);
|
||
|
for (const [index, commaToken] of commaTokens.entries()) {
|
||
|
if (spreadObject.elements[index]) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// `call(...[foo, , bar])`
|
||
|
// ^ Replace holes with `undefined`
|
||
|
yield fixer.insertTextBefore(commaToken, 'undefined');
|
||
|
}
|
||
|
},
|
||
|
};
|
||
|
},
|
||
|
[uselessIterableToArraySelector](arrayExpression) {
|
||
|
const {parent} = arrayExpression;
|
||
|
let parentDescription = '';
|
||
|
let messageId = ITERABLE_TO_ARRAY;
|
||
|
switch (parent.type) {
|
||
|
case 'ForOfStatement': {
|
||
|
messageId = ITERABLE_TO_ARRAY_IN_FOR_OF;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
case 'YieldExpression': {
|
||
|
messageId = ITERABLE_TO_ARRAY_IN_YIELD_STAR;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
case 'NewExpression': {
|
||
|
parentDescription = `new ${parent.callee.name}(…)`;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
case 'CallExpression': {
|
||
|
parentDescription = `${parent.callee.object.name}.${parent.callee.property.name}(…)`;
|
||
|
break;
|
||
|
}
|
||
|
// No default
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
node: arrayExpression,
|
||
|
messageId,
|
||
|
data: {parentDescription},
|
||
|
fix: fixer => unwrapSingleArraySpread(fixer, arrayExpression, sourceCode),
|
||
|
};
|
||
|
},
|
||
|
[uselessArrayCloneSelector](node) {
|
||
|
const arrayExpression = node.parent.parent;
|
||
|
const problem = {
|
||
|
node: arrayExpression,
|
||
|
messageId: CLONE_ARRAY,
|
||
|
};
|
||
|
|
||
|
if (
|
||
|
// `[...new Array(1)]` -> `new Array(1)` is not safe to fix since there are holes
|
||
|
isNewExpression(node, {name: 'Array'})
|
||
|
// `[...foo.slice(1)]` -> `foo.slice(1)` is not safe to fix since `foo` can be a string
|
||
|
|| (
|
||
|
node.type === 'CallExpression'
|
||
|
&& node.callee.type === 'MemberExpression'
|
||
|
&& node.callee.property.type === 'Identifier'
|
||
|
&& node.callee.property.name === 'slice'
|
||
|
)
|
||
|
) {
|
||
|
return problem;
|
||
|
}
|
||
|
|
||
|
return Object.assign(problem, {
|
||
|
fix: fixer => unwrapSingleArraySpread(fixer, arrayExpression, sourceCode),
|
||
|
});
|
||
|
},
|
||
|
};
|
||
|
};
|
||
|
|
||
|
/** @type {import('eslint').Rule.RuleModule} */
|
||
|
module.exports = {
|
||
|
create,
|
||
|
meta: {
|
||
|
type: 'suggestion',
|
||
|
docs: {
|
||
|
description: 'Disallow unnecessary spread.',
|
||
|
},
|
||
|
fixable: 'code',
|
||
|
messages,
|
||
|
},
|
||
|
};
|