| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 | 'use strict';const {isClosingParenToken, getStaticValue} = require('@eslint-community/eslint-utils');const avoidCapture = require('./utils/avoid-capture.js');const getScopes = require('./utils/get-scopes.js');const singular = require('./utils/singular.js');const toLocation = require('./utils/to-location.js');const getReferences = require('./utils/get-references.js');const {isLiteral} = require('./ast/index.js');const MESSAGE_ID = 'no-for-loop';const messages = {	[MESSAGE_ID]: 'Use a `for-of` loop instead of this `for` loop.',};const defaultElementName = 'element';const isLiteralZero = node => isLiteral(node, 0);const isLiteralOne = node => isLiteral(node, 1);const isIdentifierWithName = (node, name) => node?.type === 'Identifier' && node.name === name;const getIndexIdentifierName = forStatement => {	const {init: variableDeclaration} = forStatement;	if (		!variableDeclaration		|| variableDeclaration.type !== 'VariableDeclaration'	) {		return;	}	if (variableDeclaration.declarations.length !== 1) {		return;	}	const [variableDeclarator] = variableDeclaration.declarations;	if (!isLiteralZero(variableDeclarator.init)) {		return;	}	if (variableDeclarator.id.type !== 'Identifier') {		return;	}	return variableDeclarator.id.name;};const getStrictComparisonOperands = binaryExpression => {	if (binaryExpression.operator === '<') {		return {			lesser: binaryExpression.left,			greater: binaryExpression.right,		};	}	if (binaryExpression.operator === '>') {		return {			lesser: binaryExpression.right,			greater: binaryExpression.left,		};	}};const getArrayIdentifierFromBinaryExpression = (binaryExpression, indexIdentifierName) => {	const operands = getStrictComparisonOperands(binaryExpression);	if (!operands) {		return;	}	const {lesser, greater} = operands;	if (!isIdentifierWithName(lesser, indexIdentifierName)) {		return;	}	if (greater.type !== 'MemberExpression') {		return;	}	if (		greater.object.type !== 'Identifier'		|| greater.property.type !== 'Identifier'	) {		return;	}	if (greater.property.name !== 'length') {		return;	}	return greater.object;};const getArrayIdentifier = (forStatement, indexIdentifierName) => {	const {test} = forStatement;	if (!test || test.type !== 'BinaryExpression') {		return;	}	return getArrayIdentifierFromBinaryExpression(test, indexIdentifierName);};const isLiteralOnePlusIdentifierWithName = (node, identifierName) => {	if (node?.type === 'BinaryExpression' && node.operator === '+') {		return (isIdentifierWithName(node.left, identifierName) && isLiteralOne(node.right))			|| (isIdentifierWithName(node.right, identifierName) && isLiteralOne(node.left));	}	return false;};const checkUpdateExpression = (forStatement, indexIdentifierName) => {	const {update} = forStatement;	if (!update) {		return false;	}	if (update.type === 'UpdateExpression') {		return update.operator === '++' && isIdentifierWithName(update.argument, indexIdentifierName);	}	if (		update.type === 'AssignmentExpression'		&& isIdentifierWithName(update.left, indexIdentifierName)	) {		if (update.operator === '+=') {			return isLiteralOne(update.right);		}		if (update.operator === '=') {			return isLiteralOnePlusIdentifierWithName(update.right, indexIdentifierName);		}	}	return false;};const isOnlyArrayOfIndexVariableRead = (arrayReferences, indexIdentifierName) => arrayReferences.every(reference => {	const node = reference.identifier.parent;	if (node.type !== 'MemberExpression') {		return false;	}	if (node.property.name !== indexIdentifierName) {		return false;	}	if (		node.parent.type === 'AssignmentExpression'		&& node.parent.left === node	) {		return false;	}	return true;});const getRemovalRange = (node, sourceCode) => {	const declarationNode = node.parent;	if (declarationNode.declarations.length === 1) {		const {line} = declarationNode.loc.start;		const lineText = sourceCode.lines[line - 1];		const isOnlyNodeOnLine = lineText.trim() === sourceCode.getText(declarationNode);		return isOnlyNodeOnLine ? [			sourceCode.getIndexFromLoc({line, column: 0}),			sourceCode.getIndexFromLoc({line: line + 1, column: 0}),		] : declarationNode.range;	}	const index = declarationNode.declarations.indexOf(node);	if (index === 0) {		return [			node.range[0],			declarationNode.declarations[1].range[0],		];	}	return [		declarationNode.declarations[index - 1].range[1],		node.range[1],	];};const resolveIdentifierName = (name, scope) => {	while (scope) {		const variable = scope.set.get(name);		if (variable) {			return variable;		}		scope = scope.upper;	}};const scopeContains = (ancestor, descendant) => {	while (descendant) {		if (descendant === ancestor) {			return true;		}		descendant = descendant.upper;	}	return false;};const nodeContains = (ancestor, descendant) => {	while (descendant) {		if (descendant === ancestor) {			return true;		}		descendant = descendant.parent;	}	return false;};const isIndexVariableUsedElsewhereInTheLoopBody = (indexVariable, bodyScope, arrayIdentifierName) => {	const inBodyReferences = indexVariable.references.filter(reference => scopeContains(bodyScope, reference.from));	const referencesOtherThanArrayAccess = inBodyReferences.filter(reference => {		const node = reference.identifier.parent;		if (node.type !== 'MemberExpression') {			return true;		}		if (node.object.name !== arrayIdentifierName) {			return true;		}		return false;	});	return referencesOtherThanArrayAccess.length > 0;};const isIndexVariableAssignedToInTheLoopBody = (indexVariable, bodyScope) =>	indexVariable.references		.filter(reference => scopeContains(bodyScope, reference.from))		.some(inBodyReference => inBodyReference.isWrite());const someVariablesLeakOutOfTheLoop = (forStatement, variables, forScope) =>	variables.some(		variable => !variable.references.every(			reference => scopeContains(forScope, reference.from) || nodeContains(forStatement, reference.identifier),		),	);const getReferencesInChildScopes = (scope, name) =>	getReferences(scope).filter(reference => reference.identifier.name === name);/** @param {import('eslint').Rule.RuleContext} context */const create = context => {	const sourceCode = context.getSourceCode();	const {scopeManager, text: sourceCodeText} = sourceCode;	return {		ForStatement(node) {			const indexIdentifierName = getIndexIdentifierName(node);			if (!indexIdentifierName) {				return;			}			const arrayIdentifier = getArrayIdentifier(node, indexIdentifierName);			if (!arrayIdentifier) {				return;			}			const arrayIdentifierName = arrayIdentifier.name;			const scope = context.getScope();			const staticResult = getStaticValue(arrayIdentifier, scope);			if (staticResult && !Array.isArray(staticResult.value)) {				// Bail out if we can tell that the array variable has a non-array value (i.e. we're looping through the characters of a string constant).				return;			}			if (!checkUpdateExpression(node, indexIdentifierName)) {				return;			}			if (!node.body || node.body.type !== 'BlockStatement') {				return;			}			const forScope = scopeManager.acquire(node);			const bodyScope = scopeManager.acquire(node.body);			if (!bodyScope) {				return;			}			const indexVariable = resolveIdentifierName(indexIdentifierName, bodyScope);			if (isIndexVariableAssignedToInTheLoopBody(indexVariable, bodyScope)) {				return;			}			const arrayReferences = getReferencesInChildScopes(bodyScope, arrayIdentifierName);			if (arrayReferences.length === 0) {				return;			}			if (!isOnlyArrayOfIndexVariableRead(arrayReferences, indexIdentifierName)) {				return;			}			const [start] = node.range;			const [, end] = sourceCode.getTokenBefore(node.body, isClosingParenToken).range;			const problem = {				loc: toLocation([start, end], sourceCode),				messageId: MESSAGE_ID,			};			const elementReference = arrayReferences.find(reference => {				const node = reference.identifier.parent;				if (node.parent.type !== 'VariableDeclarator') {					return false;				}				return true;			});			const elementNode = elementReference?.identifier.parent.parent;			const elementIdentifierName = elementNode?.id.name;			const elementVariable = elementIdentifierName && resolveIdentifierName(elementIdentifierName, bodyScope);			const shouldFix = !someVariablesLeakOutOfTheLoop(node, [indexVariable, elementVariable].filter(Boolean), forScope);			if (shouldFix) {				problem.fix = function * (fixer) {					const shouldGenerateIndex = isIndexVariableUsedElsewhereInTheLoopBody(indexVariable, bodyScope, arrayIdentifierName);					const index = indexIdentifierName;					const element = elementIdentifierName						|| avoidCapture(singular(arrayIdentifierName) || defaultElementName, getScopes(bodyScope));					const array = arrayIdentifierName;					let declarationElement = element;					let declarationType = 'const';					let removeDeclaration = true;					let typeAnnotation;					if (elementNode) {						if (elementNode.id.type === 'ObjectPattern' || elementNode.id.type === 'ArrayPattern') {							removeDeclaration = arrayReferences.length === 1;						}						if (removeDeclaration) {							declarationType = element.type === 'VariableDeclarator' ? elementNode.kind : elementNode.parent.kind;							if (elementNode.id.typeAnnotation && shouldGenerateIndex) {								declarationElement = sourceCodeText.slice(elementNode.id.range[0], elementNode.id.typeAnnotation.range[0]).trim();								typeAnnotation = sourceCode.getText(									elementNode.id.typeAnnotation,									-1, // Skip leading `:`								).trim();							} else {								declarationElement = sourceCode.getText(elementNode.id);							}						}					}					const parts = [declarationType];					if (shouldGenerateIndex) {						parts.push(` [${index}, ${declarationElement}]`);						if (typeAnnotation) {							parts.push(`: [number, ${typeAnnotation}]`);						}						parts.push(` of ${array}.entries()`);					} else {						parts.push(` ${declarationElement} of ${array}`);					}					const replacement = parts.join('');					yield fixer.replaceTextRange([						node.init.range[0],						node.update.range[1],					], replacement);					for (const reference of arrayReferences) {						if (reference !== elementReference) {							yield fixer.replaceText(reference.identifier.parent, element);						}					}					if (elementNode) {						yield removeDeclaration							? fixer.removeRange(getRemovalRange(elementNode, sourceCode))							: fixer.replaceText(elementNode.init, element);					}				};			}			return problem;		},	};};/** @type {import('eslint').Rule.RuleModule} */module.exports = {	create,	meta: {		type: 'suggestion',		docs: {			description: 'Do not use a `for` loop that can be replaced with a `for-of` loop.',		},		fixable: 'code',		messages,		hasSuggestion: true,	},};
 |