-
Notifications
You must be signed in to change notification settings - Fork 30
feat: add codemod for iterate-iterator
#56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this 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 } }.
|
@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 |
refactor: use [Symbol.iterator]
55e51b2 to
37a9feb
Compare
|
@zloirock Thank you for pointing that out! Added logic to use |
|
Anything I can update to help complete this PR? |
There was a problem hiding this 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...ofloops - 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.
| 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}`; |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
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).
| 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}`; |
| 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)); | ||
| } | ||
| } |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
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:
- The relationship between the two sections isn't immediately clear
- A reader must mentally track that arrow functions are handled in two different places based on their body type
- The
otherCallsname 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.

Codemod for
iterate-iterator