eslint-plugin-unicorn
Version:
More than 300 powerful ESLint rules
986 lines (800 loc) • 29.5 kB
JavaScript
import {findVariable, getStaticValue, isClosingParenToken} from '@eslint-community/eslint-utils';
import {
getAvailableVariableName,
getConstVariableInitializer,
getScopes,
singular,
toLocation,
getReferences,
getStaticValueIfNoSideEffects,
isBoolean,
isNumber,
isArray,
isNullishType,
isUnknownType,
hasPotentiallyMutableMemberAccess,
hasCommentInRange,
getParenthesizedRange,
withTypeInformation,
} from './utils/index.js';
import {
isCallExpression,
isIdentifierNamed,
isLiteral,
} from './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 entriesSupported = 'supported';
const entriesUnsupported = 'unsupported';
const entriesUnknown = 'unknown';
const noEntriesTypeNames = new Set([
'HTMLAllCollection',
'HTMLCollection',
'HTMLCollectionOf',
'HTMLFormControlsCollection',
'HTMLFormElement',
'HTMLOptionsCollection',
'HTMLSelectElement',
'String',
]);
// These return an `HTMLCollection`, which has no `.entries()`. A `NodeList` (`querySelectorAll()`, `getElementsByName()`) has one.
const noEntriesDomCollectionMethods = new Set([
'getElementsByClassName',
'getElementsByTagName',
'getElementsByTagNameNS',
]);
const noEntriesTypeAnnotationTypes = new Set([
'TSBigIntKeyword',
'TSBooleanKeyword',
'TSNeverKeyword',
'TSNullKeyword',
'TSNumberKeyword',
'TSStringKeyword',
'TSSymbolKeyword',
'TSUndefinedKeyword',
'TSVoidKeyword',
'TSLiteralType',
]);
const isLiteralZero = node => isLiteral(node, 0);
const isLiteralOne = node => isLiteral(node, 1);
const getArrayIdentifierFromLengthMemberExpression = node => {
if (
node?.type !== 'MemberExpression'
|| node.computed
|| node.object.type !== 'Identifier'
|| node.property.type !== 'Identifier'
|| node.property.name !== 'length'
) {
return;
}
return node.object;
};
const combineUnionEntriesSupport = entriesSupports => {
if (entriesSupports.every(entriesSupport => entriesSupport === entriesSupported)) {
return entriesSupported;
}
return entriesSupports.includes(entriesUnsupported) ? entriesUnsupported : entriesUnknown;
};
const combineIntersectionEntriesSupport = entriesSupports => {
if (entriesSupports.includes(entriesSupported)) {
return entriesSupported;
}
return entriesSupports.every(entriesSupport => entriesSupport === entriesUnsupported) ? entriesUnsupported : entriesUnknown;
};
const getTypeReferenceEntriesSupport = (node, scope, visitedTypeVariables) => {
// A qualified name like `Foo.Bar` cannot be resolved by name
if (node.typeName.type !== 'Identifier') {
return entriesUnknown;
}
const typeReferenceName = node.typeName.name;
if (typeReferenceName === 'Array' || typeReferenceName === 'ReadonlyArray') {
return entriesSupported;
}
if (noEntriesTypeNames.has(typeReferenceName)) {
return entriesUnsupported;
}
const typeVariable = scope && findVariable(scope, typeReferenceName);
if (visitedTypeVariables.has(typeVariable)) {
return entriesUnknown;
}
visitedTypeVariables.add(typeVariable);
const [definition] = typeVariable?.defs ?? [];
if (!definition || definition.type !== 'Type') {
visitedTypeVariables.delete(typeVariable);
return entriesUnknown;
}
let entriesSupport = entriesUnknown;
if (definition.node.type === 'TSTypeAliasDeclaration') {
entriesSupport = getTypeAnnotationEntriesSupport(definition.node.typeAnnotation, typeVariable.scope, visitedTypeVariables);
} else if (definition.node.type === 'TSTypeParameter') {
entriesSupport = getTypeAnnotationEntriesSupport(definition.node.constraint, typeVariable.scope, visitedTypeVariables);
}
visitedTypeVariables.delete(typeVariable);
return entriesSupport;
};
const getTypeAnnotationEntriesSupport = (node, scope, visitedTypeVariables = new Set()) => {
switch (node?.type) {
case 'TSTypeAnnotation':
case 'TSParenthesizedType': {
return getTypeAnnotationEntriesSupport(node.typeAnnotation, scope, visitedTypeVariables);
}
case 'TSArrayType':
case 'TSTupleType': {
return entriesSupported;
}
case 'TSTypeOperator': {
return node.operator === 'readonly'
? getTypeAnnotationEntriesSupport(node.typeAnnotation, scope, visitedTypeVariables)
: entriesUnknown;
}
case 'TSTypeReference': {
return getTypeReferenceEntriesSupport(node, scope, visitedTypeVariables);
}
case 'TSUnionType': {
return combineUnionEntriesSupport(node.types.map(type => getTypeAnnotationEntriesSupport(type, scope, visitedTypeVariables)));
}
case 'TSIntersectionType': {
return combineIntersectionEntriesSupport(node.types.map(type => getTypeAnnotationEntriesSupport(type, scope, visitedTypeVariables)));
}
default: {
return noEntriesTypeAnnotationTypes.has(node?.type) ? entriesUnsupported : entriesUnknown;
}
}
};
const getEntryTypeSupport = (type, checker) => {
if (!type || isUnknownType(type)) {
return entriesUnknown;
}
if (type.isUnion()) {
const types = type.types.filter(type => !isNullishType(type));
return types.length === 0
? entriesUnknown
: combineUnionEntriesSupport(types.map(type => getEntryTypeSupport(type, checker)));
}
return checker.isArrayType(type) || checker.isTupleType(type)
? entriesSupported
: entriesUnsupported;
};
// Only called for an iterator type, which has a callable `next()`
const getIteratorResultValueType = (type, checker) => {
const [nextSignature] = checker.getTypeOfPropertyOfType(type, 'next').getCallSignatures();
return checker.getTypeOfPropertyOfType(checker.getReturnTypeOfSignature(nextSignature), 'value');
};
const getEntriesReturnSupport = (type, checker) => {
if (isUnknownType(type)) {
return entriesUnknown;
}
if (type.isUnion()) {
return combineUnionEntriesSupport(type.types.map(type => getEntriesReturnSupport(type, checker)));
}
const isArrayReturn = checker.isArrayType(type);
const next = checker.getTypeOfPropertyOfType(type, 'next');
const hasCallableNext = next?.getCallSignatures().length > 0;
if (!isArrayReturn && !hasCallableNext) {
return entriesUnsupported;
}
const [entryType] = checker.getTypeArguments(type);
return getEntryTypeSupport(entryType ?? getIteratorResultValueType(type, checker), checker);
};
const getCallableEntriesSupport = (type, checker) => {
const entriesSupports = (type?.getCallSignatures() ?? [])
.filter(signature => signature.parameters.length === 0)
.map(signature => getEntriesReturnSupport(checker.getReturnTypeOfSignature(signature), checker));
return entriesSupports.length === 0
? entriesUnsupported
: combineIntersectionEntriesSupport(entriesSupports);
};
const getTypeEntriesSupport = (type, checker) => {
if (isUnknownType(type)) {
return entriesUnknown;
}
if (type.isTypeParameter?.()) {
const constraint = type.getConstraint();
return constraint ? getTypeEntriesSupport(constraint, checker) : entriesUnknown;
}
if (type.isUnion()) {
return combineUnionEntriesSupport(type.types.map(type => getTypeEntriesSupport(type, checker)));
}
if (type.isIntersection()) {
return combineIntersectionEntriesSupport(type.types.map(type => getTypeEntriesSupport(type, checker)));
}
if (checker.isArrayType(type) || checker.isTupleType(type)) {
return entriesSupported;
}
const constraint = checker.getBaseConstraintOfType(type);
if (constraint && constraint !== type) {
return getTypeEntriesSupport(constraint, checker);
}
const entries = checker.getTypeOfPropertyOfType(type, 'entries');
return getCallableEntriesSupport(entries, checker);
};
const getEntriesSupportFromTypeInformation = (node, context) =>
withTypeInformation(node, context, ({type, checker}) => getTypeEntriesSupport(type, checker)) ?? entriesUnknown;
const isNoEntriesDomCollection = (node, context) =>
isCallExpression(node, {
optional: false,
})
&& node.callee.type === 'MemberExpression'
&& !node.callee.computed
&& node.callee.object.type === 'Identifier'
&& node.callee.object.name === 'document'
&& context.sourceCode.isGlobalReference(node.callee.object)
&& node.callee.property.type === 'Identifier'
&& noEntriesDomCollectionMethods.has(node.callee.property.name);
const getEntriesSupportFromVariable = (node, context, visitedVariables) => {
const variable = findVariable(context.sourceCode.getScope(node), node.name);
if (
!variable
|| visitedVariables.has(variable)
|| variable.defs.length !== 1
) {
return entriesUnknown;
}
visitedVariables.add(variable);
const [definition] = variable.defs;
const definitionScope = context.sourceCode.getScope(definition.name);
const entriesSupportFromAnnotation = getTypeAnnotationEntriesSupport(definition.name.typeAnnotation, definitionScope);
let entriesSupport = entriesSupportFromAnnotation;
// The declaration kind does not matter, a known no-entries collection has no `.entries()`
if (
entriesSupport === entriesUnknown
&& definition.type === 'Variable'
&& definition.node.id === definition.name
&& definition.node.init
) {
entriesSupport = getEntriesSupport(definition.node.init, context, visitedVariables);
}
visitedVariables.delete(variable);
return entriesSupport;
};
function getEntriesSupportFromSyntax(node, context, visitedVariables) {
switch (node.type) {
case 'Identifier': {
return getEntriesSupportFromVariable(node, context, visitedVariables);
}
case 'TSAsExpression':
case 'TSTypeAssertion': {
const entriesSupportFromAnnotation = getTypeAnnotationEntriesSupport(node.typeAnnotation, context.sourceCode.getScope(node));
return entriesSupportFromAnnotation === entriesUnknown
? getEntriesSupport(node.expression, context, visitedVariables)
: entriesSupportFromAnnotation;
}
case 'TSSatisfiesExpression':
case 'TSNonNullExpression': {
return getEntriesSupport(node.expression, context, visitedVariables);
}
case 'SequenceExpression': {
return getEntriesSupport(node.expressions.at(-1), context, visitedVariables);
}
case 'ConditionalExpression': {
return combineUnionEntriesSupport([
getEntriesSupport(node.consequent, context, visitedVariables),
getEntriesSupport(node.alternate, context, visitedVariables),
]);
}
default: {
return isNoEntriesDomCollection(node, context) ? entriesUnsupported : entriesUnknown;
}
}
}
function getEntriesSupport(node, context, visitedVariables = new Set()) {
if (isArray(node, context)) {
return entriesSupported;
}
const entriesSupportFromTypeInformation = getEntriesSupportFromTypeInformation(node, context);
return entriesSupportFromTypeInformation === entriesUnknown ? getEntriesSupportFromSyntax(node, context, visitedVariables) : entriesSupportFromTypeInformation;
}
const getLoopInfoFromSingleDeclarator = forStatement => {
const {init: variableDeclaration} = forStatement;
if (
!variableDeclaration
|| variableDeclaration.type !== 'VariableDeclaration'
|| variableDeclaration.declarations.length !== 1
) {
return;
}
const [variableDeclarator] = variableDeclaration.declarations;
const {id, init} = variableDeclarator;
if (
id.type !== 'Identifier'
|| !isLiteralZero(init)
) {
return;
}
const arrayIdentifier = getArrayIdentifierFromBinaryExpression(forStatement.test, id.name);
if (!arrayIdentifier) {
return;
}
return {
arrayIdentifier,
indexIdentifierName: 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) => {
if (binaryExpression?.type !== 'BinaryExpression') {
return;
}
const operands = getStrictComparisonOperands(binaryExpression);
if (!operands) {
return;
}
const {lesser, greater} = operands;
if (!isIdentifierNamed(lesser, indexIdentifierName)) {
return;
}
return getArrayIdentifierFromLengthMemberExpression(greater);
};
const getLoopInfoFromCachedLengthDeclarator = forStatement => {
const {init: variableDeclaration, test} = forStatement;
if (
!variableDeclaration
|| variableDeclaration.type !== 'VariableDeclaration'
|| variableDeclaration.declarations.length !== 2
|| test?.type !== 'BinaryExpression'
) {
return;
}
const [indexDeclarator, cachedLengthDeclarator] = variableDeclaration.declarations;
const {id: indexIdentifier, init: indexInitializer} = indexDeclarator;
const {id: cachedLengthIdentifier, init: cachedLengthInitializer} = cachedLengthDeclarator;
if (
indexIdentifier.type !== 'Identifier'
|| !isLiteralZero(indexInitializer)
|| cachedLengthIdentifier.type !== 'Identifier'
) {
return;
}
const arrayIdentifier = getArrayIdentifierFromLengthMemberExpression(cachedLengthInitializer);
if (!arrayIdentifier) {
return;
}
const operands = getStrictComparisonOperands(test);
if (
!operands
|| !isIdentifierNamed(operands.lesser, indexIdentifier.name)
|| !isIdentifierNamed(operands.greater, cachedLengthIdentifier.name)
) {
return;
}
return {
arrayIdentifier,
cachedLengthIdentifier,
indexIdentifierName: indexIdentifier.name,
};
};
const getLoopInfo = forStatement =>
getLoopInfoFromSingleDeclarator(forStatement)
?? getLoopInfoFromCachedLengthDeclarator(forStatement);
const isLiteralOnePlusIdentifierWithName = (node, identifierName) => {
if (node?.type === 'BinaryExpression' && node.operator === '+') {
return (isIdentifierNamed(node.left, identifierName) && isLiteralOne(node.right))
|| (isIdentifierNamed(node.right, identifierName) && isLiteralOne(node.left));
}
return false;
};
const isUpdateExpressionIncrementingIndex = (forStatement, indexIdentifierName) => {
const {update} = forStatement;
if (!update) {
return false;
}
if (update.type === 'UpdateExpression') {
return update.operator === '++' && isIdentifierNamed(update.argument, indexIdentifierName);
}
if (
update.type === 'AssignmentExpression'
&& isIdentifierNamed(update.left, indexIdentifierName)
) {
if (update.operator === '+=') {
return isLiteralOne(update.right);
}
if (update.operator === '=') {
return isLiteralOnePlusIdentifierWithName(update.right, indexIdentifierName);
}
}
return false;
};
const isSequenceUpdateExpressionIncrementingIndexAndReadingCachedLength = (update, indexIdentifierName, cachedLengthIdentifierName) => {
if (
update?.type !== 'SequenceExpression'
|| update.expressions.length !== 2
) {
return false;
}
const [firstExpression, secondExpression] = update.expressions;
return (
isUpdateExpressionIncrementingIndex({update: firstExpression}, indexIdentifierName)
&& isIdentifierNamed(secondExpression, cachedLengthIdentifierName)
) || (
isUpdateExpressionIncrementingIndex({update: secondExpression}, indexIdentifierName)
&& isIdentifierNamed(firstExpression, cachedLengthIdentifierName)
);
};
// `[arr[i]]`, `{key: arr[i]}`, and `[...arr[i]]` pass the write through to `arr[i]`
const isDestructuringElement = node => {
const {parent} = node;
switch (parent.type) {
case 'ArrayExpression':
case 'ArrayPattern': {
return parent.elements.includes(node);
}
case 'ObjectExpression':
case 'ObjectPattern': {
return parent.properties.includes(node);
}
case 'Property': {
return parent.value === node;
}
case 'RestElement': {
return parent.argument === node;
}
default: {
return false;
}
}
};
const writeTargetParentTypes = new Set([
'AssignmentExpression',
'AssignmentPattern',
'ForOfStatement',
'ForInStatement',
]);
// `arr[i]` is a write when it is an assignment target, a destructuring target inside one, or the left of a `for…of`/`for…in` head
const isWriteTarget = node => {
let current = node;
while (isDestructuringElement(current)) {
current = current.parent;
}
const {parent} = current;
return writeTargetParentTypes.has(parent.type) && parent.left === current;
};
const isMemberExpressionChanged = node =>
node.parent.type === 'UpdateExpression'
|| (node.parent.type === 'UnaryExpression' && node.parent.operator === 'delete')
|| isWriteTarget(node);
const isOnlyArrayOfIndexVariableRead = (arrayReferences, arrayVariable, indexVariable) => arrayReferences.every(reference => {
const node = reference.identifier.parent;
if (node.type !== 'MemberExpression') {
return false;
}
const referencedArrayVariable = findVariable(reference.from, reference.identifier.name);
if (
referencedArrayVariable !== arrayVariable
|| !node.computed
|| node.property.type !== 'Identifier'
|| findVariable(reference.from, node.property.name) !== indexVariable
) {
return false;
}
return !isMemberExpressionChanged(node);
});
const getRemovalRange = (node, sourceCode) => {
const declarationNode = node.parent;
if (declarationNode.declarations.length === 1) {
const {line} = sourceCode.getLoc(declarationNode).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}),
]
: sourceCode.getRange(declarationNode);
}
const index = declarationNode.declarations.indexOf(node);
if (index === 0) {
return [
sourceCode.getRange(node)[0],
sourceCode.getRange(declarationNode.declarations[1])[0],
];
}
return [
sourceCode.getRange(declarationNode.declarations[index - 1])[1],
sourceCode.getRange(node)[1],
];
};
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 isCachedLengthVariableUsedOutsideTest = (forStatement, cachedLengthVariable, cachedLengthIdentifier) =>
cachedLengthVariable.references.some(reference =>
reference.identifier !== cachedLengthIdentifier
&& !nodeContains(forStatement.test, reference.identifier));
const isCachedLengthVariableWrittenInsideLoopOutsideTest = (forStatement, cachedLengthVariable, cachedLengthIdentifier) =>
cachedLengthVariable.references.some(reference =>
reference.identifier !== cachedLengthIdentifier
&& nodeContains(forStatement, reference.identifier)
&& !nodeContains(forStatement.test, reference.identifier)
&& reference.isWrite());
// The whole `for (…)` header is replaced, so a comment between the clauses is destroyed too
const hasCommentsInsideLoopHeader = (forStatement, context) => {
const {sourceCode} = context;
const openingParenthesisToken = sourceCode.getTokenAfter(sourceCode.getFirstToken(forStatement));
const closingParenthesisToken = sourceCode.getTokenBefore(forStatement.body);
const [start] = sourceCode.getRange(openingParenthesisToken);
const [, end] = sourceCode.getRange(closingParenthesisToken);
return hasCommentInRange(context, [start, end]);
};
const canRemoveCachedLengthVariable = ({
forStatement,
cachedLengthVariable,
cachedLengthIdentifier,
}) =>
// The cached length variable is declared in the loop head, so it always resolves when `cachedLengthIdentifier` exists
!cachedLengthIdentifier
|| !isCachedLengthVariableUsedOutsideTest(forStatement, cachedLengthVariable, cachedLengthIdentifier);
// `let element = array[index]; element = 1; use(array[index])` would read the reassigned value after the rewrite
const isElementReassignedWhileArrayIsRead = ({elementNode, elementVariable, arrayReferences}) =>
arrayReferences.length > 1
&& Boolean(elementVariable?.references.some(reference => reference.isWrite() && reference.identifier !== elementNode.id));
const shouldFixProblem = ({
forStatement,
context,
forScope,
indexVariable,
elementNode,
elementVariable,
cachedLengthVariable,
cachedLengthIdentifier,
isStandardUpdateExpression,
entriesSupport,
shouldGenerateIndex,
isElementNameShadowed,
arrayReferences,
}) =>
isStandardUpdateExpression
&& !isElementNameShadowed
&& !isElementReassignedWhileArrayIsRead({elementNode, elementVariable, arrayReferences})
&& !hasCommentsInsideLoopHeader(forStatement, context)
&& !someVariablesLeakOutOfTheLoop(forStatement, [indexVariable, elementVariable, cachedLengthVariable].filter(Boolean), forScope)
&& canRemoveCachedLengthVariable({
forStatement,
cachedLengthVariable,
cachedLengthIdentifier,
})
&& !elementNode?.id.typeAnnotation
&& !(shouldGenerateIndex && entriesSupport === entriesUnsupported);
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;
return node.type !== 'MemberExpression' || node.object.name !== arrayIdentifierName;
});
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.some(reference => !(scopeContains(forScope, reference.from) || nodeContains(forStatement, reference.identifier))));
const getReferencesInChildScopes = (scope, name) =>
getReferences(scope).filter(reference => reference.identifier.name === name);
const isStaticNonArray = (node, context) => {
const initializer = getConstVariableInitializer(node, context);
if (initializer && (isNumber(initializer, context) || isBoolean(initializer, context))) {
return true;
}
const staticResult = getStaticValueIfNoSideEffects(node, context);
if (staticResult) {
return !Array.isArray(staticResult.value);
}
const fallbackStaticResult = getStaticValue(node, context.sourceCode.getScope(node));
if (!fallbackStaticResult) {
return false;
}
if (hasPotentiallyMutableMemberAccess(node, context)) {
// A static string may be produced by a method call. Keep this guard because reporting it would be a false positive.
return typeof fallbackStaticResult.value === 'string';
}
return !Array.isArray(fallbackStaticResult.value);
};
const getUpdateExpressionInfo = (forStatement, indexIdentifierName, cachedLengthIdentifier) => {
const isStandardUpdateExpression = isUpdateExpressionIncrementingIndex(forStatement, indexIdentifierName);
const isReportOnlyCachedLengthUpdate = cachedLengthIdentifier
&& isSequenceUpdateExpressionIncrementingIndexAndReadingCachedLength(forStatement.update, indexIdentifierName, cachedLengthIdentifier.name);
return {
isStandardUpdateExpression,
isReportableUpdateExpression: isStandardUpdateExpression || isReportOnlyCachedLengthUpdate,
};
};
const getBlockStatementScope = (forStatement, scopeManager) => {
if (forStatement.body?.type !== 'BlockStatement') {
return;
}
return scopeManager.acquire(forStatement.body);
};
/**
@param {import('eslint').Rule.RuleContext} context
*/
const create = context => {
const {sourceCode} = context;
const {scopeManager} = sourceCode;
context.on('ForStatement', node => {
const loopInfo = getLoopInfo(node);
if (!loopInfo) {
return;
}
const {arrayIdentifier, cachedLengthIdentifier, indexIdentifierName} = loopInfo;
if (isStaticNonArray(arrayIdentifier, context)) {
// 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;
}
const scope = sourceCode.getScope(node);
const arrayVariable = findVariable(scope, arrayIdentifier.name);
const {
isStandardUpdateExpression,
isReportableUpdateExpression,
} = getUpdateExpressionInfo(node, indexIdentifierName, cachedLengthIdentifier);
if (!isReportableUpdateExpression) {
return;
}
const bodyScope = getBlockStatementScope(node, scopeManager);
if (!bodyScope) {
return;
}
const arrayIdentifierName = arrayIdentifier.name;
const indexVariable = findVariable(bodyScope, indexIdentifierName);
if (!indexVariable || isIndexVariableAssignedToInTheLoopBody(indexVariable, bodyScope)) {
return;
}
const arrayReferences = getReferencesInChildScopes(bodyScope, arrayIdentifierName);
if (
arrayReferences.length === 0
|| !isOnlyArrayOfIndexVariableRead(arrayReferences, arrayVariable, indexVariable)
) {
return;
}
const forScope = scopeManager.acquire(node);
const cachedLengthVariable = cachedLengthIdentifier && (
findVariable(forScope, cachedLengthIdentifier.name)
?? findVariable(scope, cachedLengthIdentifier.name)
);
if (
cachedLengthVariable
&& isCachedLengthVariableWrittenInsideLoopOutsideTest(node, cachedLengthVariable, cachedLengthIdentifier)
) {
return;
}
const [start] = sourceCode.getRange(node);
const closingParenthesisToken = sourceCode.getTokenBefore(node.body, isClosingParenToken);
const [, end] = sourceCode.getRange(closingParenthesisToken);
const problem = {
loc: toLocation([start, end], context),
messageId: MESSAGE_ID,
};
const elementReference = arrayReferences.find(reference => {
const node = reference.identifier.parent;
return node.parent.type === 'VariableDeclarator';
});
const elementNode = elementReference?.identifier.parent.parent;
const elementIdentifierName = elementNode?.id.name;
const elementVariable = elementIdentifierName && findVariable(bodyScope, elementIdentifierName);
// A nested block can already declare the element name, and the loop head would shadow it, or a rewritten `array[index]` would resolve to it (`{ const element = array[index]; }` in two blocks would become `const element = element`)
const isElementNameShadowed = Boolean(elementIdentifierName)
&& getScopes(bodyScope)
.some(scope => scope !== bodyScope && findVariable(scope, elementIdentifierName) !== elementVariable);
const shouldGenerateIndex = isIndexVariableUsedElsewhereInTheLoopBody(indexVariable, bodyScope, arrayIdentifierName);
const entriesSupport = shouldGenerateIndex ? getEntriesSupport(arrayIdentifier, context) : entriesUnknown;
const shouldFix = shouldFixProblem({
forStatement: node,
context,
forScope,
indexVariable,
elementNode,
elementVariable,
cachedLengthVariable,
cachedLengthIdentifier,
isStandardUpdateExpression,
entriesSupport,
shouldGenerateIndex,
isElementNameShadowed,
arrayReferences,
});
if (shouldFix) {
problem.fix = function * (fixer, {abort}) {
const element = elementIdentifierName
|| getAvailableVariableName(singular(arrayIdentifierName) || defaultElementName, getScopes(bodyScope));
let declarationElement = element;
let declarationType = 'const';
let isRemoveDeclaration = true;
if (elementNode) {
if (elementNode.id.type === 'ObjectPattern' || elementNode.id.type === 'ArrayPattern') {
isRemoveDeclaration = arrayReferences.length === 1;
}
if (isRemoveDeclaration) {
declarationType = elementNode.parent.kind;
declarationElement = sourceCode.getText(elementNode.id);
}
}
const parts = [declarationType];
const index = indexIdentifierName;
const array = arrayIdentifierName;
if (shouldGenerateIndex) {
parts.push(` [${index}, ${declarationElement}] of ${array}.entries()`);
} else {
parts.push(` ${declarationElement} of ${array}`);
}
const replacement = parts.join('');
const [start] = sourceCode.getRange(node.init);
// The parentheses around the update are not part of its range, they would be left dangling after the new `for…of` head
const [, end] = getParenthesizedRange(node.update, context);
yield fixer.replaceTextRange([start, end], replacement);
for (const reference of arrayReferences) {
if (reference === elementReference) {
continue;
}
// `array[/* comment */ index]` is replaced as a whole
if (sourceCode.getCommentsInside(reference.identifier.parent).length > 0) {
return abort();
}
yield fixer.replaceText(reference.identifier.parent, element);
}
if (!elementNode) {
return;
}
// The element declaration is removed or its `array[index]` initializer is rewritten, a comment inside the rewritten range would be lost. The removal range also covers the `var`/`let`/`const` keyword and the trailing semicolon.
const rewrittenRange = isRemoveDeclaration
? getRemovalRange(elementNode, sourceCode)
: sourceCode.getRange(elementNode.init);
if (hasCommentInRange(context, rewrittenRange)) {
return abort();
}
yield isRemoveDeclaration
? fixer.removeRange(rewrittenRange)
: fixer.replaceText(elementNode.init, element);
};
}
return problem;
});
};
/**
@type {import('eslint').Rule.RuleModule}
*/
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Do not use a `for` loop that can be replaced with a `for-of` loop.',
recommended: true,
},
fixable: 'code',
messages,
languages: [
'js/js',
],
},
};
export default config;