Files
millionco__react-doctor/packages/react-doctor/tests/namespace-hooks.test.ts
Aiden Bai cd9ca68faa NEW_RULES: Add rule analysis foundation and 56 rules (#1402)
* 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
2026-07-20 00:54:55 -07:00

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);
}
});
});