eslint-plugin-unicorn
Version:
Various awesome ESLint rules
222 lines (194 loc) • 6.33 kB
JavaScript
'use strict';
const {isCommaToken} = require('eslint-utils');
const {
matches,
newExpressionSelector,
methodCallSelector,
} = require('./selectors/index.js');
const typedArray = require('./shared/typed-array.js');
const {removeParentheses, fixSpaceAroundKeyword} = require('./fix/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 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.',
};
const uselessSpreadInListSelector = matches([
'ArrayExpression > SpreadElement.elements > ArrayExpression.argument',
'ObjectExpression > SpreadElement.properties > ObjectExpression.argument',
'CallExpression > SpreadElement.arguments > ArrayExpression.argument',
'NewExpression > SpreadElement.arguments > ArrayExpression.argument',
]);
const iterableToArraySelector = [
'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}),
]),
' > ',
`${iterableToArraySelector}.arguments:first-child`,
].join(''),
`ForOfStatement > ${iterableToArraySelector}.right`,
`YieldExpression[delegate=true] > ${iterableToArraySelector}.argument`,
]);
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;
});
}
/** @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](array) {
const {parent} = array;
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: array,
messageId,
data: {parentDescription},
* fix(fixer) {
if (parent.type === 'ForOfStatement') {
yield * fixSpaceAroundKeyword(fixer, array, sourceCode);
}
const [
openingBracketToken,
spreadToken,
] = sourceCode.getFirstTokens(array, 2);
// `[...iterable]`
// ^
yield fixer.remove(openingBracketToken);
// `[...iterable]`
// ^^^
yield fixer.remove(spreadToken);
const [
commaToken,
closingBracketToken,
] = sourceCode.getLastTokens(array, 2);
// `[...iterable]`
// ^
yield fixer.remove(closingBracketToken);
// `[...iterable,]`
// ^
if (isCommaToken(commaToken)) {
yield fixer.remove(commaToken);
}
},
};
},
};
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow unnecessary spread.',
},
fixable: 'code',
messages,
},
};