securityos/node_modules/eslint-plugin-unicorn/rules/no-for-loop.js

428 lines
11 KiB
JavaScript
Raw Permalink Normal View History

2024-09-06 15:32:35 +00:00
'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,
},
};