| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466 | 'use strict';const {	isParenthesized,	isCommaToken,	isSemicolonToken,	isClosingParenToken,	findVariable,	hasSideEffect,} = require('@eslint-community/eslint-utils');const {methodCallSelector, referenceIdentifierSelector} = require('./selectors/index.js');const {extendFixRange} = require('./fix/index.js');const needsSemicolon = require('./utils/needs-semicolon.js');const shouldAddParenthesesToExpressionStatementExpression = require('./utils/should-add-parentheses-to-expression-statement-expression.js');const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js');const {getParentheses, getParenthesizedRange} = require('./utils/parentheses.js');const isFunctionSelfUsedInside = require('./utils/is-function-self-used-inside.js');const {isNodeMatches} = require('./utils/is-node-matches.js');const assertToken = require('./utils/assert-token.js');const {fixSpaceAroundKeyword, removeParentheses} = require('./fix/index.js');const {isArrowFunctionBody} = require('./ast/index.js');const MESSAGE_ID_ERROR = 'no-array-for-each/error';const MESSAGE_ID_SUGGESTION = 'no-array-for-each/suggestion';const messages = {	[MESSAGE_ID_ERROR]: 'Use `for…of` instead of `.forEach(…)`.',	[MESSAGE_ID_SUGGESTION]: 'Switch to `for…of`.',};const forEachMethodCallSelector = methodCallSelector({	method: 'forEach',	includeOptionalCall: true,	includeOptionalMember: true,});const continueAbleNodeTypes = new Set([	'WhileStatement',	'DoWhileStatement',	'ForStatement',	'ForOfStatement',	'ForInStatement',]);const stripChainExpression = node =>	(node.parent.type === 'ChainExpression' && node.parent.expression === node)		? node.parent		: node;function isReturnStatementInContinueAbleNodes(returnStatement, callbackFunction) {	for (let node = returnStatement; node && node !== callbackFunction; node = node.parent) {		if (continueAbleNodeTypes.has(node.type)) {			return true;		}	}	return false;}function shouldSwitchReturnStatementToBlockStatement(returnStatement) {	const {parent} = returnStatement;	switch (parent.type) {		case 'IfStatement': {			return parent.consequent === returnStatement || parent.alternate === returnStatement;		}		// These parent's body need switch to `BlockStatement` too, but since they are "continueAble", won't fix		// case 'ForStatement':		// case 'ForInStatement':		// case 'ForOfStatement':		// case 'WhileStatement':		// case 'DoWhileStatement':		case 'WithStatement': {			return parent.body === returnStatement;		}		default: {			return false;		}	}}function getFixFunction(callExpression, functionInfo, context) {	const sourceCode = context.getSourceCode();	const [callback] = callExpression.arguments;	const parameters = callback.params;	const iterableObject = callExpression.callee.object;	const {returnStatements} = functionInfo.get(callback);	const isOptionalObject = callExpression.callee.optional;	const ancestor = stripChainExpression(callExpression).parent;	const objectText = sourceCode.getText(iterableObject);	const getForOfLoopHeadText = () => {		const [elementText, indexText] = parameters.map(parameter => sourceCode.getText(parameter));		const shouldUseEntries = parameters.length === 2;		let text = 'for (';		text += isFunctionParameterVariableReassigned(callback, context) ? 'let' : 'const';		text += ' ';		text += shouldUseEntries ? `[${indexText}, ${elementText}]` : elementText;		text += ' of ';		const shouldAddParenthesesToObject			= isParenthesized(iterableObject, sourceCode)			|| (				// `1?.forEach()` -> `(1).entries()`				isOptionalObject				&& shouldUseEntries				&& shouldAddParenthesesToMemberExpressionObject(iterableObject, sourceCode)			);		text += shouldAddParenthesesToObject ? `(${objectText})` : objectText;		if (shouldUseEntries) {			text += '.entries()';		}		text += ') ';		return text;	};	const getForOfLoopHeadRange = () => {		const [start] = callExpression.range;		const [end] = getParenthesizedRange(callback.body, sourceCode);		return [start, end];	};	function * replaceReturnStatement(returnStatement, fixer) {		const returnToken = sourceCode.getFirstToken(returnStatement);		assertToken(returnToken, {			expected: 'return',			ruleId: 'no-array-for-each',		});		if (!returnStatement.argument) {			yield fixer.replaceText(returnToken, 'continue');			return;		}		// Remove `return`		yield fixer.remove(returnToken);		const previousToken = sourceCode.getTokenBefore(returnToken);		const nextToken = sourceCode.getTokenAfter(returnToken);		let textBefore = '';		let textAfter = '';		const shouldAddParentheses			= !isParenthesized(returnStatement.argument, sourceCode)				&& shouldAddParenthesesToExpressionStatementExpression(returnStatement.argument);		if (shouldAddParentheses) {			textBefore = `(${textBefore}`;			textAfter = `${textAfter})`;		}		const insertBraces = shouldSwitchReturnStatementToBlockStatement(returnStatement);		if (insertBraces) {			textBefore = `{ ${textBefore}`;		} else if (needsSemicolon(previousToken, sourceCode, shouldAddParentheses ? '(' : nextToken.value)) {			textBefore = `;${textBefore}`;		}		if (textBefore) {			yield fixer.insertTextBefore(nextToken, textBefore);		}		if (textAfter) {			yield fixer.insertTextAfter(returnStatement.argument, textAfter);		}		const returnStatementHasSemicolon = isSemicolonToken(sourceCode.getLastToken(returnStatement));		if (!returnStatementHasSemicolon) {			yield fixer.insertTextAfter(returnStatement, ';');		}		yield fixer.insertTextAfter(returnStatement, ' continue;');		if (insertBraces) {			yield fixer.insertTextAfter(returnStatement, ' }');		}	}	const shouldRemoveExpressionStatementLastToken = token => {		if (!isSemicolonToken(token)) {			return false;		}		if (callback.body.type !== 'BlockStatement') {			return false;		}		return true;	};	function * removeCallbackParentheses(fixer) {		// Opening parenthesis tokens already included in `getForOfLoopHeadRange`		const closingParenthesisTokens = getParentheses(callback, sourceCode)			.filter(token => isClosingParenToken(token));		for (const closingParenthesisToken of closingParenthesisTokens) {			yield fixer.remove(closingParenthesisToken);		}	}	return function * (fixer) {		// `(( foo.forEach(bar => bar) ))`		yield * removeParentheses(callExpression, fixer, sourceCode);		// Replace these with `for (const … of …) `		// foo.forEach(bar =>    bar)		// ^^^^^^^^^^^^^^^^^^^^^^		// foo.forEach(bar =>    (bar))		// ^^^^^^^^^^^^^^^^^^^^^^		// foo.forEach(bar =>    {})		// ^^^^^^^^^^^^^^^^^^^^^^		// foo.forEach(function(bar)    {})		// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^		yield fixer.replaceTextRange(getForOfLoopHeadRange(), getForOfLoopHeadText());		// Parenthesized callback function		// foo.forEach( ((bar => {})) )		//                         ^^		yield * removeCallbackParentheses(fixer);		const [			penultimateToken,			lastToken,		] = sourceCode.getLastTokens(callExpression, 2);		// The possible trailing comma token of `Array#forEach()` CallExpression		// foo.forEach(bar => {},)		//                      ^		if (isCommaToken(penultimateToken)) {			yield fixer.remove(penultimateToken);		}		// The closing parenthesis token of `Array#forEach()` CallExpression		// foo.forEach(bar => {})		//                      ^		yield fixer.remove(lastToken);		for (const returnStatement of returnStatements) {			yield * replaceReturnStatement(returnStatement, fixer);		}		if (ancestor.type === 'ExpressionStatement') {			const expressionStatementLastToken = sourceCode.getLastToken(ancestor);			// Remove semicolon if it's not needed anymore			// foo.forEach(bar => {});			//                       ^			if (shouldRemoveExpressionStatementLastToken(expressionStatementLastToken)) {				yield fixer.remove(expressionStatementLastToken, fixer);			}		} else if (ancestor.type === 'ArrowFunctionExpression') {			yield fixer.insertTextBefore(callExpression, '{ ');			yield fixer.insertTextAfter(callExpression, ' }');		}		yield * fixSpaceAroundKeyword(fixer, callExpression.parent, sourceCode);		if (isOptionalObject) {			yield fixer.insertTextBefore(callExpression, `if (${objectText}) `);		}		// Prevent possible variable conflicts		yield * extendFixRange(fixer, callExpression.parent.range);	};}const isChildScope = (child, parent) => {	for (let scope = child; scope; scope = scope.upper) {		if (scope === parent) {			return true;		}	}	return false;};function isFunctionParametersSafeToFix(callbackFunction, {context, scope, callExpression, allIdentifiers}) {	const variables = context.getDeclaredVariables(callbackFunction);	for (const variable of variables) {		if (variable.defs.length !== 1) {			return false;		}		const [definition] = variable.defs;		if (definition.type !== 'Parameter') {			continue;		}		const variableName = definition.name.name;		const [callExpressionStart, callExpressionEnd] = callExpression.range;		for (const identifier of allIdentifiers) {			const {name, range: [start, end]} = identifier;			if (				name !== variableName				|| start < callExpressionStart				|| end > callExpressionEnd			) {				continue;			}			const variable = findVariable(scope, identifier);			if (!variable || variable.scope === scope || isChildScope(scope, variable.scope)) {				return false;			}		}	}	return true;}function isFunctionParameterVariableReassigned(callbackFunction, context) {	return context.getDeclaredVariables(callbackFunction)		.filter(variable => variable.defs[0].type === 'Parameter')		.some(variable =>			variable.references.some(reference => !reference.init && reference.isWrite()),		);}function isFixable(callExpression, {scope, functionInfo, allIdentifiers, context}) {	// Check `CallExpression`	if (callExpression.optional || callExpression.arguments.length !== 1) {		return false;	}	// Check ancestors, we only fix `ExpressionStatement`	const callOrChainExpression = stripChainExpression(callExpression);	if (		callOrChainExpression.parent.type !== 'ExpressionStatement'		&& !isArrowFunctionBody(callOrChainExpression)	) {		return false;	}	// Check `CallExpression.arguments[0]`;	const [callback] = callExpression.arguments;	if (		// Leave non-function type to `no-array-callback-reference` rule		(callback.type !== 'FunctionExpression' && callback.type !== 'ArrowFunctionExpression')		|| callback.async		|| callback.generator	) {		return false;	}	// Check `callback.params`	const parameters = callback.params;	if (		!(parameters.length === 1 || parameters.length === 2)		// `array.forEach((element = defaultValue) => {})`		|| (parameters.length === 1 && parameters[0].type === 'AssignmentPattern')		// https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1814		|| (parameters.length === 2 && parameters[1].type !== 'Identifier')		|| parameters.some(({type, typeAnnotation}) => type === 'RestElement' || typeAnnotation)		|| !isFunctionParametersSafeToFix(callback, {scope, callExpression, allIdentifiers, context})	) {		return false;	}	// Check `ReturnStatement`s in `callback`	const {returnStatements, scope: callbackScope} = functionInfo.get(callback);	if (returnStatements.some(returnStatement => isReturnStatementInContinueAbleNodes(returnStatement, callback))) {		return false;	}	if (isFunctionSelfUsedInside(callback, callbackScope)) {		return false;	}	return true;}const ignoredObjects = [	'React.Children',	'Children',	'R',	// https://www.npmjs.com/package/p-iteration	'pIteration',];/** @param {import('eslint').Rule.RuleContext} context */const create = context => {	const functionStack = [];	const callExpressions = [];	const allIdentifiers = [];	const functionInfo = new Map();	const sourceCode = context.getSourceCode();	return {		':function'(node) {			functionStack.push(node);			functionInfo.set(node, {				returnStatements: [],				scope: context.getScope(),			});		},		':function:exit'() {			functionStack.pop();		},		[referenceIdentifierSelector()](node) {			allIdentifiers.push(node);		},		':function ReturnStatement'(node) {			const currentFunction = functionStack[functionStack.length - 1];			const {returnStatements} = functionInfo.get(currentFunction);			returnStatements.push(node);		},		[forEachMethodCallSelector](node) {			if (isNodeMatches(node.callee.object, ignoredObjects)) {				return;			}			callExpressions.push({				node,				scope: context.getScope(),			});		},		* 'Program:exit'() {			for (const {node, scope} of callExpressions) {				const iterable = node.callee;				const problem = {					node: iterable.property,					messageId: MESSAGE_ID_ERROR,				};				if (!isFixable(node, {scope, allIdentifiers, functionInfo, context})) {					yield problem;					continue;				}				const shouldUseSuggestion = iterable.optional && hasSideEffect(iterable, sourceCode);				const fix = getFixFunction(node, functionInfo, context);				if (shouldUseSuggestion) {					problem.suggest = [						{							messageId: MESSAGE_ID_SUGGESTION,							fix,						},					];				} else {					problem.fix = fix;				}				yield problem;			}		},	};};/** @type {import('eslint').Rule.RuleModule} */module.exports = {	create,	meta: {		type: 'suggestion',		docs: {			description: 'Prefer `for…of` over the `forEach` method.',		},		fixable: 'code',		hasSuggestions: true,		messages,	},};
 |