diff --git a/.changeset/soften-await-messages.md b/.changeset/soften-await-messages.md new file mode 100644 index 000000000..b689f29c5 --- /dev/null +++ b/.changeset/soften-await-messages.md @@ -0,0 +1,16 @@ +--- +"oxlint-plugin-react-doctor": patch +"react-doctor": patch +--- + +Soften await parallelization messaging to acknowledge uncertainty + +The `server-sequential-independent-await` and `async-await-in-loop` rules now use conditional language that acknowledges parallelization only helps when work is truly independent. The messages explicitly mention the requirements: no shared queues, transactions, ordering constraints, or synchronous work behind async facades. + +**Changed messages:** +- `server-sequential-independent-await`: Now says "These awaits appear independent, but parallelization only improves performance when work doesn't share queues, transactions, or resources. Verify independence, then consider \`Promise.all([...])\`." +- `async-await-in-loop`: Now says "This loop runs awaits sequentially. Parallelization may improve performance if work is truly independent (no shared queues, transactions, or ordering requirements). Verify before applying..." + +**Detection unchanged:** The rules still fire on the same patterns. No new exemptions were added. The fix addresses messaging accuracy, not detection scope. + +Fixes #1840 diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/core-rule-registry-data.json b/packages/oxlint-plugin-react-doctor/src/plugin/core-rule-registry-data.json index 37926d0d2..51e9158af 100644 --- a/packages/oxlint-plugin-react-doctor/src/plugin/core-rule-registry-data.json +++ b/packages/oxlint-plugin-react-doctor/src/plugin/core-rule-registry-data.json @@ -310,7 +310,7 @@ "id": "async-await-in-loop", "title": "await inside a loop", "severity": "warn", - "recommendation": "Collect the items, then use `await Promise.all(items.map(...))` so independent work runs at the same time", + "recommendation": "If the work is truly independent (no shared queues, transactions, ordering requirements, or synchronous facades), consider parallelizing with `await Promise.all(items.map(...))`. Verify independence before applying.", "category": "Performance", "framework": "global", "tags": ["test-noise"], @@ -12388,7 +12388,7 @@ "id": "server-sequential-independent-await", "title": "Sequential independent awaits", "severity": "warn", - "recommendation": "These two awaits don't depend on each other. Wrap them in `Promise.all([...])` so they run at the same time.", + "recommendation": "If these awaits perform truly independent asynchronous work (no shared queues, transactions, or resources), wrap them in `Promise.all([...])` to run them concurrently. Verify independence before applying.", "category": "Bugs", "framework": "global", "tags": ["server-action", "test-noise"], diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/js-performance/async-await-in-loop.regressions.test.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/js-performance/async-await-in-loop.regressions.test.ts index 95da74794..de32e0f17 100644 --- a/packages/oxlint-plugin-react-doctor/src/plugin/rules/js-performance/async-await-in-loop.regressions.test.ts +++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/js-performance/async-await-in-loop.regressions.test.ts @@ -871,4 +871,46 @@ describe("js-performance/async-await-in-loop — regressions", () => { expect(result.parseErrors).toEqual([]); expect(result.diagnostics.length).toBeGreaterThan(0); }); + + it("flags serialized-queue method calls where syntactic independence cannot prove runtime serialization (#1840)", () => { + const result = runRule( + asyncAwaitInLoop, + `class Repository { + private tail: Promise = Promise.resolve(); + private serialize(operation: () => Promise): Promise { + const result = this.tail.then(operation, operation); + this.tail = result.then(() => undefined, () => undefined); + return result; + } + runAsync(id: string): Promise { + return this.serialize(async () => { this.writes.push(id); }); + } +} +async function writeBatch(repository: Repository, ids: string[]): Promise { + for (const id of ids) { + await repository.runAsync(id); + } +}`, + ); + expect(result.parseErrors).toEqual([]); + expect(result.diagnostics.length).toBeGreaterThan(0); + }); + + it("flags async facades over synchronous work where syntactic await cannot prove asynchrony (#1840)", () => { + const result = runRule( + asyncAwaitInLoop, + `const files = new Set(['a', 'b']); +const cache = { exists: async (path: string) => files.has(path) }; +async function readExisting(paths: string[]): Promise { + const found: string[] = []; + for (const path of paths) { + if (!(await cache.exists(path))) continue; + found.push(path); + } + return found; +}`, + ); + expect(result.parseErrors).toEqual([]); + expect(result.diagnostics.length).toBeGreaterThan(0); + }); }); diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/js-performance/async-await-in-loop.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/js-performance/async-await-in-loop.ts index 021017914..0c3f3177e 100644 --- a/packages/oxlint-plugin-react-doctor/src/plugin/rules/js-performance/async-await-in-loop.ts +++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/js-performance/async-await-in-loop.ts @@ -1313,7 +1313,7 @@ export const asyncAwaitInLoop = defineRule({ severity: "warn", tags: ["test-noise"], recommendation: - "Collect the items, then use `await Promise.all(items.map(...))` so independent work runs at the same time", + "If the work is truly independent (no shared queues, transactions, ordering requirements, or synchronous facades), consider parallelizing with `await Promise.all(items.map(...))`. Verify independence before applying.", create: (context: RuleContext) => { let hasTestLibraryImport = false; const inspectLoop = ( @@ -1345,7 +1345,7 @@ export const asyncAwaitInLoop = defineRule({ if (firstAwait) { context.report({ node: firstAwait, - message: `This makes the ${label} slow because each await runs one after another, so collect the independent calls & run them together with \`await Promise.all(items.map(...))\``, + message: `This ${label} runs awaits sequentially. Parallelization may improve performance if work is truly independent (no shared queues, transactions, or ordering requirements). Verify before applying \`await Promise.all(items.map(...))\`.`, }); } }; @@ -1399,7 +1399,7 @@ export const asyncAwaitInLoop = defineRule({ const message = methodName === "forEach" ? "Async callback in .forEach silently drops every await, so the work never finishes before the loop moves on. Use a `for…of` loop, or `await Promise.all(items.map(async (item) => {...}))`" - : `Async callback in .${methodName} runs the awaits one after another, so it is slow. Use \`await Promise.all(items.map(async (item) => {...}))\` to run them at the same time`; + : `Async callback in .${methodName} runs awaits sequentially. Parallelization may improve performance if work is truly independent (no shared queues, transactions, or ordering requirements). Consider \`await Promise.all(items.map(async (item) => {...}))\`.`; context.report({ node: firstAwait, message }); } }, diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/server/server-sequential-independent-await.regressions.test.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/server/server-sequential-independent-await.regressions.test.ts index 134c396ee..db3a80f91 100644 --- a/packages/oxlint-plugin-react-doctor/src/plugin/rules/server/server-sequential-independent-await.regressions.test.ts +++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/server/server-sequential-independent-await.regressions.test.ts @@ -318,6 +318,45 @@ export default async function Page() { const user = await fetchUser(); const posts = await fetchPosts(); return null; +}`, + ); + expect(result.parseErrors).toEqual([]); + expect(result.diagnostics.length).toBeGreaterThan(0); + }); + + it("flags serialized-queue method calls where syntactic independence cannot prove runtime independence (#1840)", () => { + const result = runRule( + serverSequentialIndependentAwait, + `class Repository { + private tail: Promise = Promise.resolve(); + private serialize(operation: () => Promise): Promise { + const result = this.tail.then(operation, operation); + this.tail = result.then(() => undefined, () => undefined); + return result; + } + private runSqliteRead(kind: string): Promise { + return this.serialize(async () => [kind]); + } + async readSnapshot(): Promise { + const collectionRows = await this.runSqliteRead('collection'); + const binderRows = await this.runSqliteRead('binders'); + return [...collectionRows, ...binderRows]; + } +}`, + ); + expect(result.parseErrors).toEqual([]); + expect(result.diagnostics.length).toBeGreaterThan(0); + }); + + it("flags async facades over synchronous work where syntactic await cannot prove asynchrony (#1840)", () => { + const result = runRule( + serverSequentialIndependentAwait, + `const files = new Set(['a', 'b']); +const cache = { exists: async (path: string) => files.has(path) }; +async function readMetadata(): Promise { + const hasConfig = await cache.exists('config.json'); + const hasManifest = await cache.exists('manifest.json'); + return [hasConfig ? 'config' : '', hasManifest ? 'manifest' : ''].filter(Boolean); }`, ); expect(result.parseErrors).toEqual([]); diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/server/server-sequential-independent-await.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/server/server-sequential-independent-await.ts index 4376f55ef..abae6b213 100644 --- a/packages/oxlint-plugin-react-doctor/src/plugin/rules/server/server-sequential-independent-await.ts +++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/server/server-sequential-independent-await.ts @@ -202,7 +202,7 @@ export const serverSequentialIndependentAwait = defineRule({ severity: "warn", tags: ["test-noise"], recommendation: - "These two awaits don't depend on each other. Wrap them in `Promise.all([...])` so they run at the same time.", + "If these awaits perform truly independent asynchronous work (no shared queues, transactions, or resources), wrap them in `Promise.all([...])` to run them concurrently. Verify independence before applying.", create: (context: RuleContext) => { const inspectStatements = (statements: EsTreeNode[]): void => { for (let statementIndex = 0; statementIndex < statements.length - 1; statementIndex++) { @@ -242,7 +242,7 @@ export const serverSequentialIndependentAwait = defineRule({ context.report({ node: nextStatement, message: - "This await doesn't use the previous result, so your users wait twice as long for nothing.", + "These awaits appear independent, but parallelization only improves performance when work doesn't share queues, transactions, or resources. Verify independence, then consider `Promise.all([...])`.", }); // Skip past the next so we don't double-report a chain. statementIndex++;