Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e09dd1e
feat(react-doctor): introduce HIR data model (port from React Compiler)
cursoragent May 8, 2026
0e63a47
feat(react-doctor): add ESTree → HIR lower pass
cursoragent May 8, 2026
153c077
feat(react-doctor): add HIR type inference + debug printer
cursoragent May 8, 2026
2d144f0
feat(react-doctor): port two compiler validators to the new HIR
cursoragent May 8, 2026
76a101e
feat(react-doctor): wire HIR validators into the existing oxlint plug…
cursoragent May 8, 2026
40d7d35
test(react-doctor): regression + unit tests for the HIR port
cursoragent May 8, 2026
d5c8c09
chore: keep @typescript-eslint/parser confined to react-doctor's devDeps
cursoragent May 8, 2026
7f954a4
feat(react-doctor): thread origin ESTree node through Place
cursoragent May 8, 2026
e6d14e6
feat(react-doctor): track call-site Place in setState-in-effect findings
cursoragent May 8, 2026
8ffe483
feat(react-doctor): cache lowered HIR + report at the offending call …
cursoragent May 8, 2026
aa299e2
fix(react-doctor): tighten HIR types — drop `unknown` casts in runner
cursoragent May 8, 2026
04264e2
fix(react-doctor): use FunctionExpression captures (not body LoadLoca…
cursoragent May 8, 2026
421fe8b
fix(react-doctor): handle nested FunctionDeclaration + drop dead oute…
cursoragent May 8, 2026
c249f9a
fix(hir): round-5 deep review fixes — type-tagging, scoping, edge cases
cursoragent May 8, 2026
b2483da
fix(hir): bind catch param + drop dead inferTypes loop
cursoragent May 8, 2026
e6211a0
chore(hir): strip dead code, redundant comments, debug logging
cursoragent May 8, 2026
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
1 change: 1 addition & 0 deletions packages/react-doctor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
},
"devDependencies": {
"@types/prompts": "^2.4.9",
"@typescript-eslint/parser": "^8.59.2",
"eslint-plugin-react-hooks": "^7.1.1"
},
"peerDependencies": {
Expand Down
8 changes: 8 additions & 0 deletions packages/react-doctor/src/oxlint-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,14 @@ export const GLOBAL_REACT_DOCTOR_RULES: Record<string, RuleSeverity> = {
"react-doctor/advanced-event-handler-refs": "warn",
"react-doctor/effect-needs-cleanup": "error",

// HACK: HIR-backed rules. `hir-no-derived-computations-in-effects`
// is scoped at the validator level to defer to the AST-walker
// `no-derived-state-effect` on the simple shape, but
// `hir-no-set-state-in-effect` still overlaps with the walker on
// single-setter effects — disable either via config to dedupe.
"react-doctor/hir-no-set-state-in-effect": "warn",
"react-doctor/hir-no-derived-computations-in-effects": "warn",

"react-doctor/no-giant-component": "warn",
"react-doctor/no-render-in-render": "warn",
"react-doctor/no-many-boolean-props": "warn",
Expand Down
4 changes: 4 additions & 0 deletions packages/react-doctor/src/plugin/hir/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./types.js";
export { lowerFunction } from "./lower.js";
export { inferTypes } from "./infer-types.js";
export { hirNoSetStateInEffect, hirNoDerivedComputationsInEffects } from "./runner.js";
108 changes: 108 additions & 0 deletions packages/react-doctor/src/plugin/hir/infer-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import type { HIRFunction, Identifier, ReactType } from "./types.js";

// HACK: pared-down `inferTypes` pass — recognizes React hook callees
// (LoadGlobal name match), propagates return types to call results,
// and threads types through LoadLocal / StoreLocal so aliased setters
// stay typed as `StateSetter`.

const REACT_HOOK_NAME_TO_TYPE: Record<string, ReactType> = {
useState: "UseStateHook",
useReducer: "UseStateHook",
useEffect: "UseEffectHook",
useLayoutEffect: "UseLayoutEffectHook",
useInsertionEffect: "UseLayoutEffectHook",
useRef: "UseRefHook",
useCallback: "UseCallbackHook",
useMemo: "UseMemoHook",
useContext: "UseContextHook",
useEffectEvent: "UseEffectEventHook",
};

const setIdentifierType = (identifier: Identifier, type: ReactType): void => {
if (identifier.type === "Unknown" || identifier.type === "Function") {
identifier.type = type;
}
};

export const inferTypes = (fn: HIRFunction): void => {
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const lvalue = instr.lvalue;

switch (instr.value.kind) {
case "LoadGlobal": {
const reactType = REACT_HOOK_NAME_TO_TYPE[instr.value.name];
if (reactType && lvalue) {
setIdentifierType(lvalue.identifier, reactType);
setIdentifierType(instr.value.place.identifier, reactType);
}
break;
}
case "LoadLocal": {
if (lvalue) {
setIdentifierType(lvalue.identifier, instr.value.place.identifier.type);
}
break;
}
case "StoreLocal": {
setIdentifierType(instr.value.lvalue.identifier, instr.value.value.identifier.type);
if (lvalue) {
setIdentifierType(lvalue.identifier, instr.value.value.identifier.type);
}
break;
}
case "CallExpression": {
const calleeType = instr.value.callee.identifier.type;
if (lvalue) {
if (calleeType === "UseStateHook") {
setIdentifierType(lvalue.identifier, "StateTuple");
} else if (calleeType === "UseRefHook") {
setIdentifierType(lvalue.identifier, "RefValue");
} else if (calleeType === "UseEffectEventHook") {
setIdentifierType(lvalue.identifier, "EffectEvent");
} else if (calleeType === "UseCallbackHook") {
setIdentifierType(lvalue.identifier, "Function");
} else if (calleeType === "UseMemoHook") {
setIdentifierType(lvalue.identifier, "Object");
} else if (calleeType === "UseContextHook") {
setIdentifierType(lvalue.identifier, "Object");
}
}
break;
}
case "PropertyLoad": {
const objectType = instr.value.object.identifier.type;
if (objectType === "StateTuple" && instr.value.computed) {
if (instr.value.property === "0" && lvalue) {
setIdentifierType(lvalue.identifier, "StateValue");
} else if (instr.value.property === "1" && lvalue) {
setIdentifierType(lvalue.identifier, "StateSetter");
}
}
if (
objectType === "RefValue" &&
!instr.value.computed &&
instr.value.property === "current" &&
lvalue
) {
setIdentifierType(lvalue.identifier, "RefCurrent");
}
break;
}
case "MethodCall":
case "ArrayExpression":
case "ObjectExpression":
case "Literal":
case "Identifier":
case "BinaryExpression":
case "LogicalExpression":
case "UnaryExpression":
case "ConditionalExpression":
case "FunctionExpression":
case "JSXExpression":
case "Unsupported":
break;
}
}
}
};
Loading
Loading