| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 | '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',		]),		// `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,	},};
 |