| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 | 'use strict';const {isParenthesized, findVariable} = require('@eslint-community/eslint-utils');const {	not,	methodCallSelector,	notLeftHandSideSelector,} = require('./selectors/index.js');const getVariableIdentifiers = require('./utils/get-variable-identifiers.js');const avoidCapture = require('./utils/avoid-capture.js');const getScopes = require('./utils/get-scopes.js');const singular = require('./utils/singular.js');const {	extendFixRange,	removeMemberExpressionProperty,	removeMethodCall,	renameVariable,} = require('./fix/index.js');const ERROR_ZERO_INDEX = 'error-zero-index';const ERROR_SHIFT = 'error-shift';const ERROR_POP = 'error-pop';const ERROR_AT_MINUS_ONE = 'error-at-minus-one';const ERROR_DESTRUCTURING_DECLARATION = 'error-destructuring-declaration';const ERROR_DESTRUCTURING_ASSIGNMENT = 'error-destructuring-assignment';const ERROR_DECLARATION = 'error-variable';const SUGGESTION_NULLISH_COALESCING_OPERATOR = 'suggest-nullish-coalescing-operator';const SUGGESTION_LOGICAL_OR_OPERATOR = 'suggest-logical-or-operator';const messages = {	[ERROR_DECLARATION]: 'Prefer `.find(…)` over `.filter(…)`.',	[ERROR_ZERO_INDEX]: 'Prefer `.find(…)` over `.filter(…)[0]`.',	[ERROR_SHIFT]: 'Prefer `.find(…)` over `.filter(…).shift()`.',	[ERROR_POP]: 'Prefer `.findLast(…)` over `.filter(…).pop()`.',	[ERROR_AT_MINUS_ONE]: 'Prefer `.findLast(…)` over `.filter(…).at(-1)`.',	[ERROR_DESTRUCTURING_DECLARATION]: 'Prefer `.find(…)` over destructuring `.filter(…)`.',	// Same message as `ERROR_DESTRUCTURING_DECLARATION`, but different case	[ERROR_DESTRUCTURING_ASSIGNMENT]: 'Prefer `.find(…)` over destructuring `.filter(…)`.',	[SUGGESTION_NULLISH_COALESCING_OPERATOR]: 'Replace `.filter(…)` with `.find(…) ?? …`.',	[SUGGESTION_LOGICAL_OR_OPERATOR]: 'Replace `.filter(…)` with `.find(…) || …`.',};const filterMethodSelectorOptions = {	method: 'filter',	minimumArguments: 1,	maximumArguments: 2,};const filterVariableSelector = [	'VariableDeclaration',	// Exclude `export const foo = [];`	not('ExportNamedDeclaration > .declaration'),	' > ',	'VariableDeclarator.declarations',	'[id.type="Identifier"]',	methodCallSelector({		...filterMethodSelectorOptions,		path: 'init',	}),].join('');const zeroIndexSelector = [	'MemberExpression',	'[computed!=false]',	'[property.type="Literal"]',	'[property.raw="0"]',	notLeftHandSideSelector(),	methodCallSelector({		...filterMethodSelectorOptions,		path: 'object',	}),].join('');const shiftSelector = [	methodCallSelector({		method: 'shift',		argumentsLength: 0,	}),	methodCallSelector({		...filterMethodSelectorOptions,		path: 'callee.object',	}),].join('');const popSelector = [	methodCallSelector({		method: 'pop',		argumentsLength: 0,	}),	methodCallSelector({		...filterMethodSelectorOptions,		path: 'callee.object',	}),].join('');const atMinusOneSelector = [	methodCallSelector({		method: 'at',		argumentsLength: 1,	}),	'[arguments.0.type="UnaryExpression"]',	'[arguments.0.operator="-"]',	'[arguments.0.prefix]',	'[arguments.0.argument.type="Literal"]',	'[arguments.0.argument.raw=1]',	methodCallSelector({		...filterMethodSelectorOptions,		path: 'callee.object',	}),].join('');const destructuringDeclaratorSelector = [	'VariableDeclarator',	'[id.type="ArrayPattern"]',	'[id.elements.length=1]',	'[id.elements.0.type!="RestElement"]',	methodCallSelector({		...filterMethodSelectorOptions,		path: 'init',	}),].join('');const destructuringAssignmentSelector = [	'AssignmentExpression',	'[left.type="ArrayPattern"]',	'[left.elements.length=1]',	'[left.elements.0.type!="RestElement"]',	methodCallSelector({		...filterMethodSelectorOptions,		path: 'right',	}),].join('');// Need add `()` to the `AssignmentExpression`// - `ObjectExpression`: `[{foo}] = array.filter(bar)` fix to `{foo} = array.find(bar)`// - `ObjectPattern`: `[{foo = baz}] = array.filter(bar)`const assignmentNeedParenthesize = (node, sourceCode) => {	const isAssign = node.type === 'AssignmentExpression';	if (!isAssign || isParenthesized(node, sourceCode)) {		return false;	}	const {left} = getDestructuringLeftAndRight(node);	const [element] = left.elements;	const {type} = element.type === 'AssignmentPattern' ? element.left : element;	return type === 'ObjectExpression' || type === 'ObjectPattern';};// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Tableconst hasLowerPrecedence = (node, operator) => (	(node.type === 'LogicalExpression' && (		node.operator === operator		// https://tc39.es/proposal-nullish-coalescing/ says		// `??` has lower precedence than `||`		// But MDN says		// `??` has higher precedence than `||`		|| (operator === '||' && node.operator === '??')		|| (operator === '??' && (node.operator === '||' || node.operator === '&&'))	))	|| node.type === 'ConditionalExpression'	// Lower than `assignment`, should already parenthesized	/* c8 ignore next */	|| node.type === 'AssignmentExpression'	|| node.type === 'YieldExpression'	|| node.type === 'SequenceExpression');const getDestructuringLeftAndRight = node => {	/* c8 ignore next 3 */	if (!node) {		return {};	}	if (node.type === 'AssignmentExpression') {		return node;	}	if (node.type === 'VariableDeclarator') {		return {left: node.id, right: node.init};	}	return {};};function * fixDestructuring(node, sourceCode, fixer) {	const {left} = getDestructuringLeftAndRight(node);	const [element] = left.elements;	const leftText = sourceCode.getText(element.type === 'AssignmentPattern' ? element.left : element);	yield fixer.replaceText(left, leftText);	// `AssignmentExpression` always starts with `[` or `(`, so we don't need check ASI	if (assignmentNeedParenthesize(node, sourceCode)) {		yield fixer.insertTextBefore(node, '(');		yield fixer.insertTextAfter(node, ')');	}}const hasDefaultValue = node => getDestructuringLeftAndRight(node).left.elements[0].type === 'AssignmentPattern';const fixDestructuringDefaultValue = (node, sourceCode, fixer, operator) => {	const {left, right} = getDestructuringLeftAndRight(node);	const [element] = left.elements;	const defaultValue = element.right;	let defaultValueText = sourceCode.getText(defaultValue);	if (isParenthesized(defaultValue, sourceCode) || hasLowerPrecedence(defaultValue, operator)) {		defaultValueText = `(${defaultValueText})`;	}	return fixer.insertTextAfter(right, ` ${operator} ${defaultValueText}`);};const fixDestructuringAndReplaceFilter = (sourceCode, node) => {	const {property} = getDestructuringLeftAndRight(node).right.callee;	let suggest;	let fix;	if (hasDefaultValue(node)) {		suggest = [			{operator: '??', messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR},			{operator: '||', messageId: SUGGESTION_LOGICAL_OR_OPERATOR},		].map(({messageId, operator}) => ({			messageId,			* fix(fixer) {				yield fixer.replaceText(property, 'find');				yield fixDestructuringDefaultValue(node, sourceCode, fixer, operator);				yield * fixDestructuring(node, sourceCode, fixer);			},		}));	} else {		fix = function * (fixer) {			yield fixer.replaceText(property, 'find');			yield * fixDestructuring(node, sourceCode, fixer);		};	}	return {fix, suggest};};const isAccessingZeroIndex = node =>	node.parent.type === 'MemberExpression'	&& node.parent.computed === true	&& node.parent.object === node	&& node.parent.property.type === 'Literal'	&& node.parent.property.raw === '0';const isDestructuringFirstElement = node => {	const {left, right} = getDestructuringLeftAndRight(node.parent);	return left		&& right		&& right === node		&& left.type === 'ArrayPattern'		&& left.elements.length === 1		&& left.elements[0].type !== 'RestElement';};/** @param {import('eslint').Rule.RuleContext} context */const create = context => {	const sourceCode = context.getSourceCode();	const {		checkFromLast,	} = {		checkFromLast: false,		...context.options[0],	};	const listeners = {		[zeroIndexSelector](node) {			return {				node: node.object.callee.property,				messageId: ERROR_ZERO_INDEX,				fix: fixer => [					fixer.replaceText(node.object.callee.property, 'find'),					removeMemberExpressionProperty(fixer, node, sourceCode),				],			};		},		[shiftSelector](node) {			return {				node: node.callee.object.callee.property,				messageId: ERROR_SHIFT,				fix: fixer => [					fixer.replaceText(node.callee.object.callee.property, 'find'),					...removeMethodCall(fixer, node, sourceCode),				],			};		},		[destructuringDeclaratorSelector](node) {			return {				node: node.init.callee.property,				messageId: ERROR_DESTRUCTURING_DECLARATION,				...fixDestructuringAndReplaceFilter(sourceCode, node),			};		},		[destructuringAssignmentSelector](node) {			return {				node: node.right.callee.property,				messageId: ERROR_DESTRUCTURING_ASSIGNMENT,				...fixDestructuringAndReplaceFilter(sourceCode, node),			};		},		[filterVariableSelector](node) {			const scope = context.getScope();			const variable = findVariable(scope, node.id);			const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node.id);			if (identifiers.length === 0) {				return;			}			const zeroIndexNodes = [];			const destructuringNodes = [];			for (const identifier of identifiers) {				if (isAccessingZeroIndex(identifier)) {					zeroIndexNodes.push(identifier.parent);				} else if (isDestructuringFirstElement(identifier)) {					destructuringNodes.push(identifier.parent);				} else {					return;				}			}			const problem = {				node: node.init.callee.property,				messageId: ERROR_DECLARATION,			};			// `const [foo = bar] = baz` is not fixable			if (!destructuringNodes.some(node => hasDefaultValue(node))) {				problem.fix = function * (fixer) {					yield fixer.replaceText(node.init.callee.property, 'find');					const singularName = singular(node.id.name);					if (singularName) {						// Rename variable to be singularized now that it refers to a single item in the array instead of the entire array.						const singularizedName = avoidCapture(singularName, getScopes(scope));						yield * renameVariable(variable, singularizedName, fixer);						// Prevent possible variable conflicts						yield * extendFixRange(fixer, sourceCode.ast.range);					}					for (const node of zeroIndexNodes) {						yield removeMemberExpressionProperty(fixer, node, sourceCode);					}					for (const node of destructuringNodes) {						yield * fixDestructuring(node, sourceCode, fixer);					}				};			}			return problem;		},	};	if (!checkFromLast) {		return listeners;	}	return Object.assign(listeners, {		[popSelector](node) {			return {				node: node.callee.object.callee.property,				messageId: ERROR_POP,				fix: fixer => [					fixer.replaceText(node.callee.object.callee.property, 'findLast'),					...removeMethodCall(fixer, node, sourceCode),				],			};		},		[atMinusOneSelector](node) {			return {				node: node.callee.object.callee.property,				messageId: ERROR_AT_MINUS_ONE,				fix: fixer => [					fixer.replaceText(node.callee.object.callee.property, 'findLast'),					...removeMethodCall(fixer, node, sourceCode),				],			};		},	});};const schema = [	{		type: 'object',		additionalProperties: false,		properties: {			checkFromLast: {				type: 'boolean',				// TODO: Change default value to `true`, or remove the option when targeting Node.js 18.				default: false,			},		},	},];/** @type {import('eslint').Rule.RuleModule} */module.exports = {	create,	meta: {		type: 'suggestion',		docs: {			description: 'Prefer `.find(…)` and `.findLast(…)` over the first or last element from `.filter(…)`.',		},		fixable: 'code',		hasSuggestions: true,		schema,		messages,	},};
 |