diff --git a/.changeset/fast-status-validator-membership.md b/.changeset/fast-status-validator-membership.md new file mode 100644 index 0000000000..df55b9dbae --- /dev/null +++ b/.changeset/fast-status-validator-membership.md @@ -0,0 +1,7 @@ +--- +"eslint-plugin-react-doctor": patch +"oxlint-plugin-react-doctor": patch +"react-doctor": patch +--- + +Use a Set for repeated status-aware return membership checks in the fetch response status rule. diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/correctness/no-fetch-response-used-without-status-check.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/correctness/no-fetch-response-used-without-status-check.ts index f990dedcde..67888cc285 100644 --- a/packages/oxlint-plugin-react-doctor/src/plugin/rules/correctness/no-fetch-response-used-without-status-check.ts +++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/correctness/no-fetch-response-used-without-status-check.ts @@ -525,8 +525,10 @@ const localValidatorChecksResponseStatus = ( } }); } - const statusAwareReturns = returnedExpressions.filter((returnedExpression) => - expressionReadsStatusProperty(returnedExpression, parameterSymbol.id, context), + const statusAwareReturns = new Set( + returnedExpressions.filter((returnedExpression) => + expressionReadsStatusProperty(returnedExpression, parameterSymbol.id, context), + ), ); const booleanReturnIsStatusGuarded = (returnedExpression: EsTreeNode): boolean => { const fallback = stripGroupingParens(returnedExpression); @@ -545,9 +547,9 @@ const localValidatorChecksResponseStatus = ( return false; }; return ( - statusAwareReturns.length > 0 && + statusAwareReturns.size > 0 && returnedExpressions.every((returnedExpression) => { - if (statusAwareReturns.includes(returnedExpression)) return true; + if (statusAwareReturns.has(returnedExpression)) return true; const fallback = stripGroupingParens(returnedExpression); return ( booleanReturnIsStatusGuarded(returnedExpression) || diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/state-and-effects/effect-needs-cleanup.performance.test.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/state-and-effects/effect-needs-cleanup.performance.test.ts index 94c1049865..5ab7cc6bfe 100644 --- a/packages/oxlint-plugin-react-doctor/src/plugin/rules/state-and-effects/effect-needs-cleanup.performance.test.ts +++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/state-and-effects/effect-needs-cleanup.performance.test.ts @@ -1,12 +1,25 @@ -import { describe, expect, it } from "vite-plus/test"; +import { describe, expect, it, vi } from "vite-plus/test"; import { runRule } from "../../../test-utils/run-rule.js"; import { effectNeedsCleanup } from "./effect-needs-cleanup.js"; const SMALL_HANDLER_COUNT = 500; const LARGE_HANDLER_COUNT = 2500; -const MEASUREMENT_SAMPLE_COUNT = 7; const MAXIMUM_SCALING_MULTIPLIER = 15; +const traversalCounter = vi.hoisted(() => vi.fn()); + +vi.mock("../../utils/walk-ast.js", async (importOriginal) => { + const originalModule = await importOriginal(); + return { + ...originalModule, + walkAst: ((node, visitor) => + originalModule.walkAst(node, (candidate) => { + traversalCounter(); + return visitor(candidate); + })) satisfies typeof originalModule.walkAst, + }; +}); + const buildRetainedHandlersSource = ( handlerCount: number, buildRegistration: (handlerIndex: number) => string, @@ -23,20 +36,16 @@ const buildRetainedHandlersSource = ( return `const Component = () => {\n${handlers}\nreturn <>${buttons};\n};`; }; -const measureDuration = ( +const measureTraversalWork = ( handlerCount: number, buildRegistration: (handlerIndex: number) => string, ): number => { const source = buildRetainedHandlersSource(handlerCount, buildRegistration); - const sampleDurations = Array.from({ length: MEASUREMENT_SAMPLE_COUNT }, () => { - const startedAt = process.hrtime.bigint(); - const result = runRule(effectNeedsCleanup, source); - expect(result.parseErrors).toEqual([]); - expect(result.diagnostics).toHaveLength(handlerCount); - return Number(process.hrtime.bigint() - startedAt); - }); - sampleDurations.sort((firstDuration, secondDuration) => firstDuration - secondDuration); - return sampleDurations[Math.floor(sampleDurations.length / 2)] ?? Number.POSITIVE_INFINITY; + traversalCounter.mockClear(); + const result = runRule(effectNeedsCleanup, source); + expect(result.parseErrors).toEqual([]); + expect(result.diagnostics).toHaveLength(handlerCount); + return traversalCounter.mock.calls.length; }; describe("effect-needs-cleanup performance", () => { @@ -52,11 +61,17 @@ describe("effect-needs-cleanup performance", () => { }, ]) { it(`scales near-linearly across retained ${performanceCase.name}`, () => { - measureDuration(SMALL_HANDLER_COUNT, performanceCase.buildRegistration); - measureDuration(LARGE_HANDLER_COUNT, performanceCase.buildRegistration); - const smallDuration = measureDuration(SMALL_HANDLER_COUNT, performanceCase.buildRegistration); - const largeDuration = measureDuration(LARGE_HANDLER_COUNT, performanceCase.buildRegistration); - expect(largeDuration).toBeLessThan(smallDuration * MAXIMUM_SCALING_MULTIPLIER); + const smallTraversalCount = measureTraversalWork( + SMALL_HANDLER_COUNT, + performanceCase.buildRegistration, + ); + const largeTraversalCount = measureTraversalWork( + LARGE_HANDLER_COUNT, + performanceCase.buildRegistration, + ); + expect(smallTraversalCount).toBeGreaterThan(SMALL_HANDLER_COUNT); + expect(largeTraversalCount).toBeGreaterThan(smallTraversalCount); + expect(largeTraversalCount).toBeLessThan(smallTraversalCount * MAXIMUM_SCALING_MULTIPLIER); }); } });