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,
- },
- };
|