Skip to content

Conversation

@babs20
Copy link
Contributor

@babs20 babs20 commented Jul 22, 2024

Codemod for iterate-iterator

Copy link

@zloirock zloirock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Iterators are not always iterable, so for proper work in for-of and Array.from they should be wrapped to { [Symbol.iterator](){ return iterator } }.

@babs20
Copy link
Contributor Author

babs20 commented Jul 22, 2024

@zloirock I think that case it not intended to be handled by the package this codemod replaces. That seems like a case we would cover when we create the codemod for the iterate-value package

@zloirock
Copy link

Try something like this:
Снимок экрана 2024-07-22 в 09 57 20

refactor: use [Symbol.iterator]
@babs20 babs20 force-pushed the feature/iterate-iterator branch from 55e51b2 to 37a9feb Compare July 22, 2024 03:37
@babs20
Copy link
Contributor Author

babs20 commented Jul 22, 2024

@zloirock Thank you for pointing that out! Added logic to use [Symbol.iterator] and an additional test case based on your comment.

@babs20
Copy link
Contributor Author

babs20 commented Aug 27, 2024

Anything I can update to help complete this PR?

Copilot AI review requested due to automatic review settings December 24, 2025 15:34
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a codemod to migrate from the iterate-iterator npm package to native JavaScript alternatives. The codemod transforms calls to iterateIterator() into native for...of loops and Array.from() constructions.

Key changes:

  • Adds transformation logic to convert single-argument calls to Array.from() with a custom iterator
  • Transforms two-argument calls (with callbacks) to for...of loops
  • Handles both ES6 imports and CommonJS requires

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
codemods/iterate-iterator/index.js Main codemod implementation that transforms iterate-iterator calls to native JavaScript
types/codemods/iterate-iterator/index.d.ts TypeScript type definitions for the codemod
test/fixtures/iterate-iterator/case-1/* Test fixtures for ES6 import scenario with various transformation patterns
test/fixtures/iterate-iterator/case-2/* Test fixtures for CommonJS require scenario with various transformation patterns
test/fixtures/iterate-iterator/case-3/* Test fixtures for arrow functions with parenthesized parameters
package.json Adds package export entry for the new codemod
index.js Imports and registers the iterate-iterator codemod in the codemods registry

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +105 to +113
const needsParens =
callbackKind === 'function_expression' ||
callbackKind === 'arrow_function';

const callbackCall = needsParens
? `(${callback})(i)`
: `${callback}(i)`;

const replacement = `for (const i of {\n [Symbol.iterator]: () => ${arg1Match.text()}\n}) {\n ${callbackCall};\n}`;
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transformation uses a hardcoded variable name i for the loop variable when handling function expressions and arrow functions with block bodies. While this is semantically correct, it results in less readable code compared to extracting the parameter name from the callback function.

For example:

iterate(items, function(item) { console.log(item); })

Currently transforms to:

for (const i of {...}) {
  (function(item) { console.log(item); })(i);
}

It would be more readable to extract the parameter name item and generate:

for (const item of {...}) {
  (function(item) { console.log(item); })(item);
}

Or even better, extract the function body and simplify to:

for (const item of {...}) {
  console.log(item);
}

This would be consistent with how the arrowCalls section handles arrow functions with expression bodies (lines 63-78).

Suggested change
const needsParens =
callbackKind === 'function_expression' ||
callbackKind === 'arrow_function';
const callbackCall = needsParens
? `(${callback})(i)`
: `${callback}(i)`;
const replacement = `for (const i of {\n [Symbol.iterator]: () => ${arg1Match.text()}\n}) {\n ${callbackCall};\n}`;
let loopVar = 'i';
if (
callbackKind === 'function_expression' ||
callbackKind === 'arrow_function'
) {
const paramMatch = callback.match(
/^(?:\s*(?:async\s*)?function\b[^(]*\(\s*([^,\s()]+)\s*\)|\s*(?:async\s*)?\(?\s*([^,\s()]+)\s*\)?\s*=>)/s
);
const paramName =
paramMatch && (paramMatch[1] || paramMatch[2]);
if (paramName) {
loopVar = paramName;
}
}
const needsParens =
callbackKind === 'function_expression' ||
callbackKind === 'arrow_function';
const callbackCall = needsParens
? `(${callback})(${loopVar})`
: `${callback}(${loopVar})`;
const replacement = `for (const ${loopVar} of {\n [Symbol.iterator]: () => ${arg1Match.text()}\n}) {\n ${callbackCall};\n}`;

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +116
const arrowCalls = root.findAll({
rule: {
pattern: {
context: `${importName}($ARG1, $PARAM => $BODY)`,
strictness: 'relaxed',
},
},
constraints: {
PARAM: { regex: '^[^,]+$' },
},
});

for (const call of arrowCalls) {
const arg1Match = call.getMatch('ARG1');
const paramMatch = call.getMatch('PARAM');
const bodyMatch = call.getMatch('BODY');

if (arg1Match && paramMatch && bodyMatch) {
if (bodyMatch.kind() === 'statement_block') {
continue;
}

const paramName = paramMatch.text().replace(/^\(|\)$/g, '');
const bodyText = bodyMatch.text();

const replacement = `for (const ${paramName} of {\n [Symbol.iterator]: () => ${arg1Match.text()}\n}) {\n ${bodyText};\n}`;
edits.push(call.replace(replacement));
}
}

const otherCalls = root.findAll({
rule: {
pattern: {
context: `${importName}($ARG1, $ARG2)`,
strictness: 'relaxed',
},
},
});

for (const call of otherCalls) {
const arg1Match = call.getMatch('ARG1');
const arg2Match = call.getMatch('ARG2');

if (arg1Match && arg2Match) {
const callback = arg2Match.text();
const callbackKind = arg2Match.kind();

if (callbackKind === 'arrow_function') {
const body = arg2Match.field('body');
if (body && body.kind() !== 'statement_block') {
continue;
}
}

const needsParens =
callbackKind === 'function_expression' ||
callbackKind === 'arrow_function';

const callbackCall = needsParens
? `(${callback})(i)`
: `${callback}(i)`;

const replacement = `for (const i of {\n [Symbol.iterator]: () => ${arg1Match.text()}\n}) {\n ${callbackCall};\n}`;
edits.push(call.replace(replacement));
}
}
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code handles two-argument calls with arrow functions by splitting the logic across two sections: arrowCalls (lines 51-79) handles arrow functions with expression bodies, while otherCalls (lines 81-116) handles arrow functions with block bodies and function expressions. While this approach works correctly, it makes the code harder to understand because:

  1. The relationship between the two sections isn't immediately clear
  2. A reader must mentally track that arrow functions are handled in two different places based on their body type
  3. The otherCalls name doesn't accurately describe that it also handles a specific subset of arrow functions

Consider consolidating the two-argument callback handling into a single section with clearer conditional logic, or at minimum, add comments explaining why arrow functions are split across these two sections.

Copilot uses AI. Check for mistakes.
@43081j 43081j merged commit 6098258 into es-tooling:main Dec 24, 2025
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants