Skip to content

Commit 37461a8

Browse files
fix: apply sleep/dependency heuristics to callback-based iteration
The loopBodyHasOnlySleepLikeAwaits and hasLoopCarriedDependency checks were only applied in inspectLoopBody (for/while/do-while) but skipped for callback-based iteration (.forEach, .map, etc.), causing false positives on patterns like arr.forEach(async item => { await sleep(500) }). Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b084dbe commit 37461a8

2 files changed

Lines changed: 26 additions & 3 deletions

File tree

packages/react-doctor/src/plugin/rules/js-performance.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -904,9 +904,8 @@ export const asyncAwaitInLoop: Rule = {
904904
) {
905905
return;
906906
}
907-
// `body` is either a BlockStatement (block body) or any
908-
// expression (concise body, e.g. `async x => fetch(x)`); walkAst
909-
// handles both, so we just walk `body` directly.
907+
if (loopBodyHasOnlySleepLikeAwaits(body)) return;
908+
if (hasLoopCarriedDependency(body)) return;
910909
const firstAwait = findFirstAwaitOutsideNestedFunctions(body);
911910
if (firstAwait) {
912911
const message =

packages/react-doctor/tests/fixtures/basic-react/src/async-and-handler-issues.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,30 @@ export const fetchAllUsersParallel = async (ids: string[]) => {
4747
);
4848
};
4949

50+
// NEGATIVE case for async-await-in-loop: forEach with only sleep-like awaits.
51+
// The heuristic must suppress the report just as it does for traditional loops.
52+
declare const sleep: (ms: number) => Promise<void>;
53+
declare const process: (item: string) => void;
54+
55+
export const throttledForEach = (items: string[]) => {
56+
items.forEach(async (item) => {
57+
await sleep(500);
58+
process(item);
59+
});
60+
};
61+
62+
// NEGATIVE case for async-await-in-loop: forEach with a loop-carried
63+
// dependency pattern (assign + read in awaited arg).
64+
declare const fetchPage: (cursor: string) => Promise<{ next: string; data: string[] }>;
65+
66+
export const paginatedForEach = async (cursors: string[]) => {
67+
let token = "start";
68+
cursors.forEach(async () => {
69+
const page = await fetchPage(token);
70+
token = page.next;
71+
});
72+
};
73+
5074
// advanced-event-handler-refs: useEffect re-subscribes when handler prop
5175
// identity changes.
5276
export const Ticker = ({ onTick }: { onTick: () => void }) => {

0 commit comments

Comments
 (0)