Skip to content

feat: port the React Compiler's HIR + validators into react-doctor#164

Draft
aidenybai wants to merge 2 commits into
mainfrom
cursor/hir-port-7144
Draft

feat: port the React Compiler's HIR + validators into react-doctor#164
aidenybai wants to merge 2 commits into
mainfrom
cursor/hir-port-7144

Conversation

@aidenybai
Copy link
Copy Markdown
Member

@aidenybai aidenybai commented May 8, 2026

Summary

Ports the React Compiler's High-level IR (HIR) infrastructure into react-doctor as a foundation for more precise rule analysis, plus two initial validators ported from the compiler:

  • hir-no-set-state-in-effect: flags setState calls inside useEffect bodies, including when the setter is propagated through const x = setX aliasing or wrapped in a useEffectEvent / inner FunctionExpression.
  • hir-no-derived-computations-in-effects: flags effects whose captures are entirely deps + setters and whose body has at least one intermediate const (the unique path the AST walker can't see through).

What got ported

  • HIR data model (plugin/hir/types.ts): HIRFunction, BasicBlock, Instruction, Place, Identifier, ReactType (≈20 React-aware tags including the round-5 StateTuple), with originNode on Place for diagnostic precision.
  • Lowering (plugin/hir/lower.ts): ESTree → HIR, parent-chained scope environments, FunctionExpression.capturedPlaces, full statement coverage (round-5 added for/while/do-while/for-of/for-in/switch/try/throw/labeled), SpreadElement unwrapping in call args.
  • Type inference (plugin/hir/infer-types.ts): hook recognition via LoadGlobal name → ReactType, propagation through LoadLocal/StoreLocal/PropertyLoad. Round-5 introduces StateTuple so only useState returns get the indexed StateValue/StateSetter tagging — useMemo/useContext returns no longer leak into that branch.
  • Validators (plugin/hir/validators/): two ports, with shape-defer logic that lets the existing AST walker handle the simple cases.
  • Runner (plugin/hir/runner.ts): per-component WeakMap<EsTreeNode, HIRFunction> cache + originNode-based diagnostic anchoring so reports point at the offending setX(...) call site.

Round 5 — deep code review fixes

Five issues found on a fresh review of the round-4 code:

# Issue Fix
1 inferTypes conflated useState/useMemo/useContext returns as Object; the indexed-PropertyLoad branch tagged [a, b] = useMemo(...) as [StateValue, StateSetter], leading to false-positive setState detection New StateTuple ReactType gates the indexed-PropertyLoad branch to useState only; regression test for useMemo tuple destructure
2 hir-no-derived-computations-in-effects duplicated noDerivedStateEffect on the article §1 fullName example Validator now defers when the inner HIR has no StoreLocal (single-setter-call shape) — keeps the multi-statement-with-locals path that uniquely needs HIR. hir-no-set-state-in-effect left at warn with a known-overlap note in oxlint-config.ts, since reliably distinguishing direct vs aliased setters at v1 isn't workable
3 lowerExpression silently dropped SpreadElement arguments New lowerCallArguments unwraps the spread's argument so operand identity is preserved through setState propagation
4 lowerStatement ignored for/while/do-while/for-of/for-in/switch/try/throw/labeled — hooks inside any of these blocks were invisible Added recursive descent for all of them
5 hir-unit.test.ts article-§1 case used the single-setter-call shape that fix #2 now defers on Updated to the multi-statement-with-locals form to exercise the validator's unique path

Validation

  • 490 / 490 tests pass (4 new regressions added across the 5 fixes)
  • pnpm lint clean
  • pnpm typecheck clean
  • pnpm format clean

Known v1 limitations (documented in code)

  • let reassignments are not modeled as non-reactive (HIR is non-SSA).
  • JSX expression contents aren't lowered as identifier reads (renders are summarized as a placeholder).
  • import { useState as useS } is not recognized — name-based heuristic on LoadGlobal.
  • hir-no-set-state-in-effect overlaps noDerivedStateEffect on the simplest shape; users can disable either via react-doctor.config.json.

Architecture summary

ESTree (oxlint visitor)
   ↓ lowerFunction
HIR (basic blocks + instructions + Place.originNode)
   ↓ inferTypes (per-identifier ReactType tags)
Typed HIR
   ↓ validateNo*  (per-rule analyses)
Findings (with originNode for precise reporting)
   ↓ runner reports through ruleContext
oxlint diagnostics anchored at the offending call site

Future ports (no/lazy-init-in-render, no-create-ref-during-render, deeper effect-event analyses) plug in as additional validators reading the same HIR.

Open in Web Open in Cursor 

@vercel
Copy link
Copy Markdown

vercel Bot commented May 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-doctor-website Ready Ready Preview, Comment May 15, 2026 6:00am

cursor Bot pushed a commit that referenced this pull request May 8, 2026
Five issues found on a fresh deep review of the HIR port (PR #164):

1. inferTypes conflated useState/useMemo/useContext returns under
   ReactType "Object". The PropertyLoad index-0/index-1 → StateValue/
   StateSetter branch keyed off "Object", so a destructure like
   `const [n, runIt] = useMemo(...)` would tag `runIt` as a
   StateSetter and the validators would treat `runIt()` as a
   setState call. Fix: add a distinct `StateTuple` ReactType, gate
   the PropertyLoad branch on it. Regression test added.

2. The HIR rules duplicated `noDerivedStateEffect` on the canonical
   single-setter-call shape (article §1 fullName example), producing
   two diagnostics on the same line. Fix:
   `hir-no-derived-computations-in-effects` now defers when the
   effect body has no intermediate local bindings (StoreLocal in the
   inner HIR), preserving the multi-statement-with-locals path the
   AST walker can't see through. `hir-no-set-state-in-effect`
   stays unscoped because distinguishing direct vs aliased setter
   calls reliably from the HIR is not workable at v1; the duplicate
   on the simplest shape is documented as a known v1 limitation.

3. `lowerExpression` silently dropped `SpreadElement` arguments
   (`f(...args)` lowered to a CallExpression with the spread
   missing). Extracted `lowerCallArguments` that unwraps the
   spread's `argument` so the operand identity is still threaded
   through — losing only the spread shape (not modeled in v1) but
   keeping setState/alias propagation working through spread args.

4. `lowerStatement` only handled VariableDeclaration, ExpressionStatement,
   ReturnStatement, BlockStatement, IfStatement, and the function-
   declaration shapes. `for`, `while`, `do-while`, `for-of`,
   `for-in`, `switch`, `try`, `throw`, `labeled` all silently
   fell through, so a useEffect (or any hook call) inside any of
   these blocks was invisible to validators. Added recursive
   descent for all of them — control flow collapses into the
   surrounding block (no real CFG terminals yet) but the bodies
   get lowered.

5. The hir-unit test for the article §1 example used the single-
   setter-call shape that the round-2 scoping fix now defers on;
   updated to the multi-statement-with-locals form to keep the
   coverage on the validator's unique path.

All 490 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request May 8, 2026
Five issues found on a fresh deep review of the HIR port (PR #164):

1. inferTypes conflated useState/useMemo/useContext returns under
   ReactType "Object". The PropertyLoad index-0/index-1 → StateValue/
   StateSetter branch keyed off "Object", so a destructure like
   `const [n, runIt] = useMemo(...)` would tag `runIt` as a
   StateSetter and the validators would treat `runIt()` as a
   setState call. Fix: add a distinct `StateTuple` ReactType, gate
   the PropertyLoad branch on it. Regression test added.

2. The HIR rules duplicated `noDerivedStateEffect` on the canonical
   single-setter-call shape (article §1 fullName example), producing
   two diagnostics on the same line. Fix:
   `hir-no-derived-computations-in-effects` now defers when the
   effect body has no intermediate local bindings (StoreLocal in the
   inner HIR), preserving the multi-statement-with-locals path the
   AST walker can't see through. `hir-no-set-state-in-effect`
   stays unscoped because distinguishing direct vs aliased setter
   calls reliably from the HIR is not workable at v1; the duplicate
   on the simplest shape is documented as a known v1 limitation.

3. `lowerExpression` silently dropped `SpreadElement` arguments
   (`f(...args)` lowered to a CallExpression with the spread
   missing). Extracted `lowerCallArguments` that unwraps the
   spread's `argument` so the operand identity is still threaded
   through — losing only the spread shape (not modeled in v1) but
   keeping setState/alias propagation working through spread args.

4. `lowerStatement` only handled VariableDeclaration, ExpressionStatement,
   ReturnStatement, BlockStatement, IfStatement, and the function-
   declaration shapes. `for`, `while`, `do-while`, `for-of`,
   `for-in`, `switch`, `try`, `throw`, `labeled` all silently
   fell through, so a useEffect (or any hook call) inside any of
   these blocks was invisible to validators. Added recursive
   descent for all of them — control flow collapses into the
   surrounding block (no real CFG terminals yet) but the bodies
   get lowered.

5. The hir-unit test for the article §1 example used the single-
   setter-call shape that the round-2 scoping fix now defers on;
   updated to the multi-statement-with-locals form to keep the
   coverage on the validator's unique path.

All 490 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/hir-port-7144 branch from c101b26 to 29c79d0 Compare May 8, 2026 09:03
cursor Bot pushed a commit that referenced this pull request May 8, 2026
Five issues found on a fresh deep review of the HIR port (PR #164):

1. inferTypes conflated useState/useMemo/useContext returns under
   ReactType "Object". The PropertyLoad index-0/index-1 → StateValue/
   StateSetter branch keyed off "Object", so a destructure like
   `const [n, runIt] = useMemo(...)` would tag `runIt` as a
   StateSetter and the validators would treat `runIt()` as a
   setState call. Fix: add a distinct `StateTuple` ReactType, gate
   the PropertyLoad branch on it. Regression test added.

2. The HIR rules duplicated `noDerivedStateEffect` on the canonical
   single-setter-call shape (article §1 fullName example), producing
   two diagnostics on the same line. Fix:
   `hir-no-derived-computations-in-effects` now defers when the
   effect body has no intermediate local bindings (StoreLocal in the
   inner HIR), preserving the multi-statement-with-locals path the
   AST walker can't see through. `hir-no-set-state-in-effect`
   stays unscoped because distinguishing direct vs aliased setter
   calls reliably from the HIR is not workable at v1; the duplicate
   on the simplest shape is documented as a known v1 limitation.

3. `lowerExpression` silently dropped `SpreadElement` arguments
   (`f(...args)` lowered to a CallExpression with the spread
   missing). Extracted `lowerCallArguments` that unwraps the
   spread's `argument` so the operand identity is still threaded
   through — losing only the spread shape (not modeled in v1) but
   keeping setState/alias propagation working through spread args.

4. `lowerStatement` only handled VariableDeclaration, ExpressionStatement,
   ReturnStatement, BlockStatement, IfStatement, and the function-
   declaration shapes. `for`, `while`, `do-while`, `for-of`,
   `for-in`, `switch`, `try`, `throw`, `labeled` all silently
   fell through, so a useEffect (or any hook call) inside any of
   these blocks was invisible to validators. Added recursive
   descent for all of them — control flow collapses into the
   surrounding block (no real CFG terminals yet) but the bodies
   get lowered.

5. The hir-unit test for the article §1 example used the single-
   setter-call shape that the round-2 scoping fix now defers on;
   updated to the multi-statement-with-locals form to keep the
   coverage on the validator's unique path.

All 490 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/hir-port-7144 branch from 29c79d0 to e6c2397 Compare May 8, 2026 09:18
cursor Bot pushed a commit that referenced this pull request May 8, 2026
Five issues found on a fresh deep review of the HIR port (PR #164):

1. inferTypes conflated useState/useMemo/useContext returns under
   ReactType "Object". The PropertyLoad index-0/index-1 → StateValue/
   StateSetter branch keyed off "Object", so a destructure like
   `const [n, runIt] = useMemo(...)` would tag `runIt` as a
   StateSetter and the validators would treat `runIt()` as a
   setState call. Fix: add a distinct `StateTuple` ReactType, gate
   the PropertyLoad branch on it. Regression test added.

2. The HIR rules duplicated `noDerivedStateEffect` on the canonical
   single-setter-call shape (article §1 fullName example), producing
   two diagnostics on the same line. Fix:
   `hir-no-derived-computations-in-effects` now defers when the
   effect body has no intermediate local bindings (StoreLocal in the
   inner HIR), preserving the multi-statement-with-locals path the
   AST walker can't see through. `hir-no-set-state-in-effect`
   stays unscoped because distinguishing direct vs aliased setter
   calls reliably from the HIR is not workable at v1; the duplicate
   on the simplest shape is documented as a known v1 limitation.

3. `lowerExpression` silently dropped `SpreadElement` arguments
   (`f(...args)` lowered to a CallExpression with the spread
   missing). Extracted `lowerCallArguments` that unwraps the
   spread's `argument` so the operand identity is still threaded
   through — losing only the spread shape (not modeled in v1) but
   keeping setState/alias propagation working through spread args.

4. `lowerStatement` only handled VariableDeclaration, ExpressionStatement,
   ReturnStatement, BlockStatement, IfStatement, and the function-
   declaration shapes. `for`, `while`, `do-while`, `for-of`,
   `for-in`, `switch`, `try`, `throw`, `labeled` all silently
   fell through, so a useEffect (or any hook call) inside any of
   these blocks was invisible to validators. Added recursive
   descent for all of them — control flow collapses into the
   surrounding block (no real CFG terminals yet) but the bodies
   get lowered.

5. The hir-unit test for the article §1 example used the single-
   setter-call shape that the round-2 scoping fix now defers on;
   updated to the multi-statement-with-locals form to keep the
   coverage on the validator's unique path.

All 490 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/hir-port-7144 branch from e6c2397 to e6211a0 Compare May 8, 2026 09:25
cursor Bot pushed a commit that referenced this pull request May 10, 2026
Five issues found on a fresh deep review of the HIR port (PR #164):

1. inferTypes conflated useState/useMemo/useContext returns under
   ReactType "Object". The PropertyLoad index-0/index-1 → StateValue/
   StateSetter branch keyed off "Object", so a destructure like
   `const [n, runIt] = useMemo(...)` would tag `runIt` as a
   StateSetter and the validators would treat `runIt()` as a
   setState call. Fix: add a distinct `StateTuple` ReactType, gate
   the PropertyLoad branch on it. Regression test added.

2. The HIR rules duplicated `noDerivedStateEffect` on the canonical
   single-setter-call shape (article §1 fullName example), producing
   two diagnostics on the same line. Fix:
   `hir-no-derived-computations-in-effects` now defers when the
   effect body has no intermediate local bindings (StoreLocal in the
   inner HIR), preserving the multi-statement-with-locals path the
   AST walker can't see through. `hir-no-set-state-in-effect`
   stays unscoped because distinguishing direct vs aliased setter
   calls reliably from the HIR is not workable at v1; the duplicate
   on the simplest shape is documented as a known v1 limitation.

3. `lowerExpression` silently dropped `SpreadElement` arguments
   (`f(...args)` lowered to a CallExpression with the spread
   missing). Extracted `lowerCallArguments` that unwraps the
   spread's `argument` so the operand identity is still threaded
   through — losing only the spread shape (not modeled in v1) but
   keeping setState/alias propagation working through spread args.

4. `lowerStatement` only handled VariableDeclaration, ExpressionStatement,
   ReturnStatement, BlockStatement, IfStatement, and the function-
   declaration shapes. `for`, `while`, `do-while`, `for-of`,
   `for-in`, `switch`, `try`, `throw`, `labeled` all silently
   fell through, so a useEffect (or any hook call) inside any of
   these blocks was invisible to validators. Added recursive
   descent for all of them — control flow collapses into the
   surrounding block (no real CFG terminals yet) but the bodies
   get lowered.

5. The hir-unit test for the article §1 example used the single-
   setter-call shape that the round-2 scoping fix now defers on;
   updated to the multi-statement-with-locals form to keep the
   coverage on the validator's unique path.

All 490 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/hir-port-7144 branch from e6211a0 to 5359e36 Compare May 10, 2026 05:05
Comment thread packages/react-doctor/tests/regressions/hir-port.test.ts
Comment thread packages/react-doctor/src/plugin/index.ts Outdated
Comment thread packages/oxlint-plugin-react-doctor/src/plugin/hir/lower.ts
Comment thread packages/oxlint-plugin-react-doctor/src/plugin/hir/lower.ts
Comment thread packages/oxlint-plugin-react-doctor/src/plugin/hir/infer-types.ts
Comment thread packages/oxlint-plugin-react-doctor/src/plugin/hir/infer-types.ts
@reactreview
Copy link
Copy Markdown

reactreview Bot commented May 10, 2026

🔴 React Review0/100 (unchanged) · 0 ❌ errors · 10 ⚠️ warnings

Copy prompt for agent
Check if these React Review issues are valid. If so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.

Run this before and after your changes to verify the result:
npx react-doctor@latest --verbose --diff

Do not modify the react-doctor configuration unless explicitly asked.
Fix the underlying code issues instead of changing or suppressing the rules.

React Review found 0 errors and 10 warnings. This PR leaves the React health score unchanged.

<file name="packages/oxlint-plugin-react-doctor/src/plugin/hir/validators/validate-no-set-state-in-effect.ts">

<violation number="1" location="packages/oxlint-plugin-react-doctor/src/plugin/hir/validators/validate-no-set-state-in-effect.ts:42">
Severity: Warning

instr.lvalue.identifier.id is read 8 times inside this loop — hoist into a const at the top of the loop body

Hoist the deep member access into a const at the top of the loop body: `const { x, y } = obj.deeply.nested`

Rule: `js-cache-property-access`
</violation>

<violation number="2" location="packages/oxlint-plugin-react-doctor/src/plugin/hir/validators/validate-no-set-state-in-effect.ts:42">
Severity: Warning

instr.lvalue.identifier.id is read 8 times inside this loop — hoist into a const at the top of the loop body

Hoist the deep member access into a const at the top of the loop body: `const { x, y } = obj.deeply.nested`

Rule: `js-cache-property-access`
</violation>

</file>

<file name="packages/oxlint-plugin-react-doctor/src/plugin/hir/validators/validate-no-derived-computations-in-effects.ts">

<violation number="1" location="packages/oxlint-plugin-react-doctor/src/plugin/hir/validators/validate-no-derived-computations-in-effects.ts:36">
Severity: Warning

instr.value.kind is read 6 times inside this loop — hoist into a const at the top of the loop body

Hoist the deep member access into a const at the top of the loop body: `const { x, y } = obj.deeply.nested`

Rule: `js-cache-property-access`
</violation>

<violation number="2" location="packages/oxlint-plugin-react-doctor/src/plugin/hir/validators/validate-no-derived-computations-in-effects.ts:36">
Severity: Warning

instr.value.kind is read 6 times inside this loop — hoist into a const at the top of the loop body

Hoist the deep member access into a const at the top of the loop body: `const { x, y } = obj.deeply.nested`

Rule: `js-cache-property-access`
</violation>

<violation number="3" location="packages/oxlint-plugin-react-doctor/src/plugin/hir/validators/validate-no-derived-computations-in-effects.ts:90">
Severity: Warning

array.includes() in a loop is O(n) per call — convert to a Set for O(1) lookups

Use a `Set` or `Map` for repeated membership tests / keyed lookups — `Array.includes`/`find` is O(n) per call

Rule: `js-set-map-lookups`
</violation>

</file>

<file name="packages/oxlint-plugin-react-doctor/src/plugin/hir/lower.ts">

<violation number="1" location="packages/oxlint-plugin-react-doctor/src/plugin/hir/lower.ts:436">
Severity: Warning

place.identifier.id is read 3 times inside this loop — hoist into a const at the top of the loop body

Hoist the deep member access into a const at the top of the loop body: `const { x, y } = obj.deeply.nested`

Rule: `js-cache-property-access`
</violation>

<violation number="2" location="packages/oxlint-plugin-react-doctor/src/plugin/hir/lower.ts:436">
Severity: Warning

place.identifier.id is read 3 times inside this loop — hoist into a const at the top of the loop body

Hoist the deep member access into a const at the top of the loop body: `const { x, y } = obj.deeply.nested`

Rule: `js-cache-property-access`
</violation>

</file>

<file name="packages/oxlint-plugin-react-doctor/src/plugin/hir/infer-types.ts">

<violation number="1" location="packages/oxlint-plugin-react-doctor/src/plugin/hir/infer-types.ts:76">
Severity: Warning

instr.value.property is read 3 times inside this loop — hoist into a const at the top of the loop body

Hoist the deep member access into a const at the top of the loop body: `const { x, y } = obj.deeply.nested`

Rule: `js-cache-property-access`
</violation>

<violation number="2" location="packages/oxlint-plugin-react-doctor/src/plugin/hir/infer-types.ts:76">
Severity: Warning

instr.value.property is read 3 times inside this loop — hoist into a const at the top of the loop body

Hoist the deep member access into a const at the top of the loop body: `const { x, y } = obj.deeply.nested`

Rule: `js-cache-property-access`
</violation>

</file>

<file name="packages/react-doctor/tests/regressions/hir-port.test.ts">

<violation number="1" location="packages/react-doctor/tests/regressions/hir-port.test.ts:39">
Severity: Warning

.filter().map() iterates the array twice — combine into a single loop with .reduce() or for...of

Combine `.map().filter()` (or similar chains) into a single pass with `.reduce()` or a `for...of` loop to avoid iterating the array twice

Rule: `js-combine-iterations`
</violation>

</file>

Reviewed by react-review for commit 02e1bbd. Configure here.

cursor Bot pushed a commit that referenced this pull request May 12, 2026
Five issues found on a fresh deep review of the HIR port (PR #164):

1. inferTypes conflated useState/useMemo/useContext returns under
   ReactType "Object". The PropertyLoad index-0/index-1 → StateValue/
   StateSetter branch keyed off "Object", so a destructure like
   `const [n, runIt] = useMemo(...)` would tag `runIt` as a
   StateSetter and the validators would treat `runIt()` as a
   setState call. Fix: add a distinct `StateTuple` ReactType, gate
   the PropertyLoad branch on it. Regression test added.

2. The HIR rules duplicated `noDerivedStateEffect` on the canonical
   single-setter-call shape (article §1 fullName example), producing
   two diagnostics on the same line. Fix:
   `hir-no-derived-computations-in-effects` now defers when the
   effect body has no intermediate local bindings (StoreLocal in the
   inner HIR), preserving the multi-statement-with-locals path the
   AST walker can't see through. `hir-no-set-state-in-effect`
   stays unscoped because distinguishing direct vs aliased setter
   calls reliably from the HIR is not workable at v1; the duplicate
   on the simplest shape is documented as a known v1 limitation.

3. `lowerExpression` silently dropped `SpreadElement` arguments
   (`f(...args)` lowered to a CallExpression with the spread
   missing). Extracted `lowerCallArguments` that unwraps the
   spread's `argument` so the operand identity is still threaded
   through — losing only the spread shape (not modeled in v1) but
   keeping setState/alias propagation working through spread args.

4. `lowerStatement` only handled VariableDeclaration, ExpressionStatement,
   ReturnStatement, BlockStatement, IfStatement, and the function-
   declaration shapes. `for`, `while`, `do-while`, `for-of`,
   `for-in`, `switch`, `try`, `throw`, `labeled` all silently
   fell through, so a useEffect (or any hook call) inside any of
   these blocks was invisible to validators. Added recursive
   descent for all of them — control flow collapses into the
   surrounding block (no real CFG terminals yet) but the bodies
   get lowered.

5. The hir-unit test for the article §1 example used the single-
   setter-call shape that the round-2 scoping fix now defers on;
   updated to the multi-statement-with-locals form to keep the
   coverage on the validator's unique path.

All 490 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/hir-port-7144 branch from 5359e36 to d6a3ce3 Compare May 12, 2026 05:02
Comment thread packages/react-doctor/src/plugin/index.ts Outdated
Comment thread packages/react-doctor/tests/regressions/hir-port.test.ts
cursor Bot pushed a commit that referenced this pull request May 13, 2026
Five issues found on a fresh deep review of the HIR port (PR #164):

1. inferTypes conflated useState/useMemo/useContext returns under
   ReactType "Object". The PropertyLoad index-0/index-1 → StateValue/
   StateSetter branch keyed off "Object", so a destructure like
   `const [n, runIt] = useMemo(...)` would tag `runIt` as a
   StateSetter and the validators would treat `runIt()` as a
   setState call. Fix: add a distinct `StateTuple` ReactType, gate
   the PropertyLoad branch on it. Regression test added.

2. The HIR rules duplicated `noDerivedStateEffect` on the canonical
   single-setter-call shape (article §1 fullName example), producing
   two diagnostics on the same line. Fix:
   `hir-no-derived-computations-in-effects` now defers when the
   effect body has no intermediate local bindings (StoreLocal in the
   inner HIR), preserving the multi-statement-with-locals path the
   AST walker can't see through. `hir-no-set-state-in-effect`
   stays unscoped because distinguishing direct vs aliased setter
   calls reliably from the HIR is not workable at v1; the duplicate
   on the simplest shape is documented as a known v1 limitation.

3. `lowerExpression` silently dropped `SpreadElement` arguments
   (`f(...args)` lowered to a CallExpression with the spread
   missing). Extracted `lowerCallArguments` that unwraps the
   spread's `argument` so the operand identity is still threaded
   through — losing only the spread shape (not modeled in v1) but
   keeping setState/alias propagation working through spread args.

4. `lowerStatement` only handled VariableDeclaration, ExpressionStatement,
   ReturnStatement, BlockStatement, IfStatement, and the function-
   declaration shapes. `for`, `while`, `do-while`, `for-of`,
   `for-in`, `switch`, `try`, `throw`, `labeled` all silently
   fell through, so a useEffect (or any hook call) inside any of
   these blocks was invisible to validators. Added recursive
   descent for all of them — control flow collapses into the
   surrounding block (no real CFG terminals yet) but the bodies
   get lowered.

5. The hir-unit test for the article §1 example used the single-
   setter-call shape that the round-2 scoping fix now defers on;
   updated to the multi-statement-with-locals form to keep the
   coverage on the validator's unique path.

All 490 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/hir-port-7144 branch from d6a3ce3 to 2fe2042 Compare May 13, 2026 17:05
cursor Bot pushed a commit that referenced this pull request May 13, 2026
Five issues found on a fresh deep review of the HIR port (PR #164):

1. inferTypes conflated useState/useMemo/useContext returns under
   ReactType "Object". The PropertyLoad index-0/index-1 → StateValue/
   StateSetter branch keyed off "Object", so a destructure like
   `const [n, runIt] = useMemo(...)` would tag `runIt` as a
   StateSetter and the validators would treat `runIt()` as a
   setState call. Fix: add a distinct `StateTuple` ReactType, gate
   the PropertyLoad branch on it. Regression test added.

2. The HIR rules duplicated `noDerivedStateEffect` on the canonical
   single-setter-call shape (article §1 fullName example), producing
   two diagnostics on the same line. Fix:
   `hir-no-derived-computations-in-effects` now defers when the
   effect body has no intermediate local bindings (StoreLocal in the
   inner HIR), preserving the multi-statement-with-locals path the
   AST walker can't see through. `hir-no-set-state-in-effect`
   stays unscoped because distinguishing direct vs aliased setter
   calls reliably from the HIR is not workable at v1; the duplicate
   on the simplest shape is documented as a known v1 limitation.

3. `lowerExpression` silently dropped `SpreadElement` arguments
   (`f(...args)` lowered to a CallExpression with the spread
   missing). Extracted `lowerCallArguments` that unwraps the
   spread's `argument` so the operand identity is still threaded
   through — losing only the spread shape (not modeled in v1) but
   keeping setState/alias propagation working through spread args.

4. `lowerStatement` only handled VariableDeclaration, ExpressionStatement,
   ReturnStatement, BlockStatement, IfStatement, and the function-
   declaration shapes. `for`, `while`, `do-while`, `for-of`,
   `for-in`, `switch`, `try`, `throw`, `labeled` all silently
   fell through, so a useEffect (or any hook call) inside any of
   these blocks was invisible to validators. Added recursive
   descent for all of them — control flow collapses into the
   surrounding block (no real CFG terminals yet) but the bodies
   get lowered.

5. The hir-unit test for the article §1 example used the single-
   setter-call shape that the round-2 scoping fix now defers on;
   updated to the multi-statement-with-locals form to keep the
   coverage on the validator's unique path.

All 490 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/hir-port-7144 branch from 2fe2042 to be5aabb Compare May 13, 2026 17:53
cursor Bot pushed a commit that referenced this pull request May 13, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
#221, #223, #224, #226, #227) split the monolithic src/utils +
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/hir-port-7144 branch from be5aabb to ebd49c9 Compare May 13, 2026 18:37
cursor Bot pushed a commit that referenced this pull request May 13, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/hir-port-7144 branch from ebd49c9 to 3edfd31 Compare May 13, 2026 19:10
cursor Bot pushed a commit that referenced this pull request May 14, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/hir-port-7144 branch from a6c2c8e to c189609 Compare May 14, 2026 03:14
cursor Bot pushed a commit that referenced this pull request May 14, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/hir-port-7144 branch from c189609 to aa13731 Compare May 14, 2026 03:45
cursor Bot pushed a commit that referenced this pull request May 14, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/hir-port-7144 branch from aa13731 to b56e4c2 Compare May 14, 2026 05:33
cursor Bot pushed a commit that referenced this pull request May 14, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/hir-port-7144 branch from b56e4c2 to c1c6323 Compare May 14, 2026 06:11
cursor Bot pushed a commit that referenced this pull request May 15, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/hir-port-7144 branch from c1c6323 to 4b03893 Compare May 15, 2026 00:08
cursor Bot pushed a commit that referenced this pull request May 15, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/hir-port-7144 branch from 4b03893 to 0d12d1e Compare May 15, 2026 02:21
cursoragent and others added 2 commits May 15, 2026 05:50
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
PR #235 dropped the [key: string]: any escape hatch from EsTreeNode,
so the HIR lower pass — which walks ESTree as a generic graph and
reads properties dynamically — no longer type-checks against the
narrowed Node type.

Two changes:
1. lower.ts: introduce a local `AnyNode = EsTreeNode & Record<string,
   any>` alias and use it throughout the lowering passes. Runtime
   `node.type === "X"` checks still gate every branch — only the
   compiler-time access is widened.
2. runner.ts: tighten the visitor signatures to EsTreeNodeOfType<"...">
   so the per-visitor calls compile against the narrowed shape.

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants