Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fast-status-validator-membership.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) ||
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof import("../../utils/walk-ast.js")>();
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,
Expand All @@ -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", () => {
Expand All @@ -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);
});
}
});
Loading