mirror of
https://github.com/millionco/react-doctor.git
synced 2026-09-14 20:00:24 +08:00
cd9ca68faa
* NEW_RULES: Reapply rule analysis foundation Reapply the complete reviewed NEW_RULES stack on top of current main after the rollback. * fix(rules): harden new diagnostics against false positives * fix(rules): preserve dynamic context providers * fix(core): resolve styled-components workspace versions * fix(rules): harden new diagnostics after deep audit
178 lines
6.1 KiB
TypeScript
178 lines
6.1 KiB
TypeScript
import * as path from "node:path";
|
|
import { describe, expect, it } from "vite-plus/test";
|
|
import type { Diagnostic } from "@react-doctor/core";
|
|
import { runOxlint } from "@react-doctor/core";
|
|
import { buildIsolatedDerivedStateRuleConfig, buildTestProject } from "./regressions/_helpers.js";
|
|
|
|
const FIXTURES_DIRECTORY = path.resolve(
|
|
import.meta.dirname,
|
|
"..",
|
|
"..",
|
|
"core",
|
|
"tests",
|
|
"fixtures",
|
|
);
|
|
const BASIC_REACT_DIRECTORY = path.join(FIXTURES_DIRECTORY, "basic-react");
|
|
|
|
const findDiagnosticsInFile = (
|
|
diagnostics: Diagnostic[],
|
|
rule: string,
|
|
fileFragment: string,
|
|
): Diagnostic[] =>
|
|
diagnostics.filter(
|
|
(diagnostic) => diagnostic.rule === rule && diagnostic.filePath.includes(fileFragment),
|
|
);
|
|
|
|
let diagnostics: Diagnostic[];
|
|
let isolatedDerivedStateEffectDiagnostics: Diagnostic[];
|
|
|
|
describe("namespace hook detection (React.useEffect, React.useState, etc.)", () => {
|
|
it("loads diagnostics from namespace-hooks fixture", async () => {
|
|
diagnostics = await runOxlint({
|
|
rootDirectory: BASIC_REACT_DIRECTORY,
|
|
project: buildTestProject({
|
|
rootDirectory: BASIC_REACT_DIRECTORY,
|
|
tanstackQueryVersion: "^5.66.0",
|
|
hasSsrDependency: true,
|
|
}),
|
|
});
|
|
isolatedDerivedStateEffectDiagnostics = await runOxlint({
|
|
rootDirectory: BASIC_REACT_DIRECTORY,
|
|
project: buildTestProject({
|
|
rootDirectory: BASIC_REACT_DIRECTORY,
|
|
hasTanStackQuery: true,
|
|
}),
|
|
userConfig: {
|
|
rules: buildIsolatedDerivedStateRuleConfig("no-derived-state-effect"),
|
|
},
|
|
});
|
|
expect(diagnostics.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("detects no-derived-state-effect with React.useEffect", () => {
|
|
const issues = findDiagnosticsInFile(
|
|
isolatedDerivedStateEffectDiagnostics,
|
|
"no-derived-state-effect",
|
|
"namespace-hooks",
|
|
);
|
|
expect(issues.length).toBeGreaterThan(0);
|
|
expect(issues[0].message).toContain("derive from other values");
|
|
});
|
|
|
|
it("detects no-fetch-in-effect with React.useEffect", () => {
|
|
const issues = findDiagnosticsInFile(diagnostics, "no-fetch-in-effect", "namespace-hooks");
|
|
expect(issues.length).toBeGreaterThan(0);
|
|
expect(issues[0].message).toContain("fetch() inside useEffect");
|
|
});
|
|
|
|
it("does not detect the retired no-cascading-set-state rule", () => {
|
|
const issues = findDiagnosticsInFile(diagnostics, "no-cascading-set-state", "namespace-hooks");
|
|
expect(issues).toHaveLength(0);
|
|
});
|
|
|
|
it("detects no-effect-event-handler with React.useEffect", () => {
|
|
const issues = findDiagnosticsInFile(diagnostics, "no-effect-event-handler", "namespace-hooks");
|
|
expect(issues.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("detects no-derived-useState with React.useState", () => {
|
|
const issues = findDiagnosticsInFile(diagnostics, "no-derived-useState", "namespace-hooks");
|
|
expect(issues.length).toBeGreaterThan(0);
|
|
// NOTE: prop name is `currentName` (NOT `initialName`) — the
|
|
// initial-only prop-name skip in `no-derived-useState` would
|
|
// suppress `initialName` as a controlled-init pattern.
|
|
expect(issues[0].message).toContain("currentName");
|
|
});
|
|
|
|
it("detects rerender-lazy-state-init with React.useState", () => {
|
|
const issues = findDiagnosticsInFile(
|
|
diagnostics,
|
|
"rerender-lazy-state-init",
|
|
"namespace-hooks",
|
|
);
|
|
expect(issues.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("detects rerender-functional-setstate with React.useState setter", () => {
|
|
const issues = findDiagnosticsInFile(
|
|
diagnostics,
|
|
"rerender-functional-setstate",
|
|
"namespace-hooks",
|
|
);
|
|
expect(issues.length).toBeGreaterThan(0);
|
|
expect(issues[0].message).toContain("reads a stale value");
|
|
});
|
|
|
|
it("detects rerender-dependencies with React.useEffect and React.useCallback", () => {
|
|
const issues = findDiagnosticsInFile(diagnostics, "rerender-dependencies", "namespace-hooks");
|
|
expect(issues.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("detects rendering-hydration-no-flicker with React.useEffect", () => {
|
|
const issues = findDiagnosticsInFile(
|
|
diagnostics,
|
|
"rendering-hydration-no-flicker",
|
|
"namespace-hooks",
|
|
);
|
|
expect(issues.length).toBeGreaterThan(0);
|
|
expect(issues[0].message).toContain("useEffect(setState, [])");
|
|
});
|
|
|
|
it("detects no-usememo-simple-expression with React.useMemo", () => {
|
|
const issues = findDiagnosticsInFile(
|
|
diagnostics,
|
|
"no-usememo-simple-expression",
|
|
"namespace-hooks",
|
|
);
|
|
expect(issues.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("detects prefer-useReducer with React.useState", () => {
|
|
const issues = findDiagnosticsInFile(diagnostics, "prefer-useReducer", "namespace-hooks");
|
|
expect(issues.length).toBeGreaterThan(0);
|
|
expect(issues[0].message).toContain("useState values");
|
|
});
|
|
|
|
it("does not flag member expression calls like localStorage.setItem as state setters", () => {
|
|
const allCleanFileIssues = diagnostics.filter((diagnostic) =>
|
|
diagnostic.filePath.includes("clean"),
|
|
);
|
|
|
|
const setterRules = new Set([
|
|
"no-derived-state-effect",
|
|
"rerender-functional-setstate",
|
|
"rendering-hydration-no-flicker",
|
|
]);
|
|
|
|
const falsePositives = allCleanFileIssues.filter((diagnostic) =>
|
|
setterRules.has(diagnostic.rule),
|
|
);
|
|
expect(
|
|
falsePositives,
|
|
"member expression calls like localStorage.setItem should not be flagged as React state setters",
|
|
).toHaveLength(0);
|
|
});
|
|
|
|
it("still detects all rules from direct-import fixtures (no regression)", () => {
|
|
const directImportRules = [
|
|
"no-derived-state-effect",
|
|
"no-fetch-in-effect",
|
|
"no-effect-event-handler",
|
|
"no-derived-useState",
|
|
"rerender-lazy-state-init",
|
|
"rerender-functional-setstate",
|
|
"rerender-dependencies",
|
|
];
|
|
|
|
for (const rule of directImportRules) {
|
|
const sourceDiagnostics =
|
|
rule === "no-derived-state-effect" ? isolatedDerivedStateEffectDiagnostics : diagnostics;
|
|
const stateIssues = findDiagnosticsInFile(sourceDiagnostics, rule, "state-issues");
|
|
expect(
|
|
stateIssues.length,
|
|
`expected ${rule} to still fire on state-issues.tsx`,
|
|
).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
});
|