Skip to content
Merged
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
17 changes: 17 additions & 0 deletions .changeset/feat-cfg-formal-verification-stack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"oxlint-plugin-react-doctor": patch
---

Add a formal-verification stack to the control-flow graph and three path-sensitive rules.

`@react-doctor/cfg` gains four layers on top of its CFG/SSA engine, all pure-TS, bundled at build time, lazy (a rule that never reads a layer pays nothing), and run once per scan:

- **Dataflow framework** — `solveDataflow`, a generic monotone worklist fixpoint over a `Lattice<Fact>` (one solver subsumes many analyses), and `analyzeDefiniteAssignment` built on it: a forward must-analysis over the SSA occurrence stream answering _is this read reached unassigned on some path?_ (a `declare` like `let x;` is neither read nor write, so a bare declaration never counts as an assignment).
- **Typestate engine** — `verifyTypestate(cfg, { automaton, classifier })` generalizes resource-protocol checking into a reusable automaton verified over the CFG, reporting error transitions (an illegal event) and leaked resources (a resource left non-accepting on a normal-completion path). Events are attributed to their real block and deduplicated, so the whole-body implicit-return never double-counts a call.
- **Path feasibility** — a bounded, dependency-free checker (`isPathFeasible` + `lowerGuard` / `pathConditionFacts`) that lowers a path's branch guards into facts over SSA values and refutes correlated-branch counterexamples via union-find congruence closure. It only ever _suppresses_ a diagnostic when the path search is complete and every counterexample is provably infeasible (e.g. `if (x) open(); … if (x) close();`), so it strictly removes false positives and is never unsound for bug-finding.

Three new rules consume them:

- `correctness/no-use-before-define` — a block-scoped binding (`let` / `const` / `class` / `using`) used lexically before its declaration runs, in the same synchronous execution, which always throws a `ReferenceError` from the Temporal Dead Zone. Sound by construction: quiet for hoisted `var` / function declarations, params, globals, and any access nested in a closure or class body that may run after the declaration. A declared-but-unassigned `let` read (`let x; if (c) x = 1; use(x)`) is `undefined`, not a TDZ crash, so it is deliberately not reported.
- `state-and-effects/no-stale-closure-capture` — a `useMemo` / `useCallback` closure that captures a `let` binding reassigned later in the same render, so the memoised value/function sees a stale value (the deps array signals the author intended the value at creation time). Quiet for `const` and bindings never reassigned after capture, and for the deferred effect hooks (`useEffect` / `useLayoutEffect`), whose callbacks run after render where reading the binding's final value is the intended pattern, not a stale capture.
- `state-and-effects/no-unreleased-resource` — a resource opened inside a React effect callback (timer, subscription, event listener, `AbortController`) and released INLINE on some paths but leaked on an early return. Scoped to `useEffect` / `useLayoutEffect` / `useInsertionEffect` (including the namespaced `React.useEffect` form): the returned-cleanup contract stays owned by `effect-cleanup-not-on-every-path`, a `finally`-based release counts as run-on-every-path, and non-effect functions (class lifecycle methods, non-React frameworks like Solid's `createEffect`/`onCleanup`) are left alone.
9 changes: 9 additions & 0 deletions .changeset/feat-cfg-native-ssa.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"oxlint-plugin-react-doctor": patch
---

Add native SSA to the control-flow graph and a path-sensitive dead-assignment rule.

`@react-doctor/cfg` now builds variable-level **static single assignment** form over its oxc-native CFG via the Braun, Buchwald, Hack et al. (2013) on-the-fly sealed-block algorithm — the same algorithm the React Compiler's `EnterSSA` implements — followed by their redundant-φ elimination pass. It is a clean-room port (no Babel, MIT attribution): a minimal value model (`SsaIdentifier` / `Place` / `Phi`), per-instruction read/write extraction, a self-contained lexical binding resolver with an injectable seam (the plugin feeds in its own scope analyzer's binding identities), and an `analyzeSsa` query API (`versionAt`, `reachingDefinition`, `isLiveValue`, `isRedefinedBetween`, `bindingOf`, per-function φ + def blocks). The parity suite asserts the Braun φ placement equals the iterated dominance frontier of each binding's definitions (Cytron et al.), and `toDot` renders φ-functions.

New `no-dead-assignment` rule uses it: it flags a write to a reassignable local whose value is never read because every path overwrites it first (`let total = expensive(); total = cheap(); return total;`). This is a value-flow question pure control flow can't answer — it complements `no-unused-vars` (which only sees wholly-unused bindings) and stays quiet for `const`, compound assignments, closure-captured bindings, and any write whose value is read on some path.
7 changes: 7 additions & 0 deletions .changeset/feat-cfg-structural-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"oxlint-plugin-react-doctor": patch
---

Upgrade `@react-doctor/cfg` to a full structural control-flow graph.

Each basic block is now a typed instruction list ending in a first-class `Terminal` modeled on the React Compiler HIR taxonomy (`goto` / `if` / `switch` / loops / `logical` / `ternary` / `optional` / `try` / `return` / `throw`), with `fallthrough` join blocks and explicit `goto` lowering of `break` / `continue`. Dominance now uses the Cooper–Harvey–Kennedy immediate-dominator tree over reverse-postorder (plus the Cytron dominance frontier as the SSA seam). New analysis surface: `dominanceFrontier`, `isInfiniteLoopStart` (oxc-parity constant folding), and a Graphviz `toDot` export. The builder is split into `ir/` + `build/` + `analysis/` modules, and curated parity corpora from oxc (`no-fallthrough`, `no-unsafe-finally`, `getter-return`), ESLint code-path analysis, and React Compiler `BuildHIR` are ported as tests. The published plugin behavior is unchanged (all rule tests pass); this is an internal engine upgrade bundled at build time.
8 changes: 8 additions & 0 deletions .changeset/feat-cfg-verifier-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"oxlint-plugin-react-doctor": patch
---

Add 2 new rules that use the structural control-flow graph as a verifier:

- `no-unreachable-code` (Bugs): flags code that never runs because every path above it returns, throws, breaks, continues, or loops forever (via the CFG's `isUnreachable`). Hoisted function declarations, type-only TS declarations, and a bare `var x;` are left alone, matching ESLint's `no-unreachable` carve-outs. Global rule (runs on all JS/TS), so the defensive trailing `throw` after a switch whose every case returns is reported as dead code, consistent with `no-unreachable`.
- `no-set-state-in-render-loop` (Bugs): flags a `useState` setter called inside a render-phase loop (via the CFG's `isInsideLoop`), which fires every iteration and restarts rendering ("Too many re-renders"). Complements `no-set-state-in-render`, which only catches setters that run unconditionally; the two partition cleanly on `isUnconditionalFromEntry`, so an unconditional `for (;;)` / `while (true)` setter is owned by `no-set-state-in-render` and never double-reported. Setters in `.map()` / event-handler / effect callbacks (separate functions) stay quiet.
7 changes: 7 additions & 0 deletions .changeset/refactor-extract-cfg-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"oxlint-plugin-react-doctor": patch
---

Extract the control-flow graph into a dedicated internal `@react-doctor/cfg` package.

The per-function CFG builder and its dominance / reachability analyses now live in their own self-contained package (bundled into the plugin at build time, so the published surface is unchanged). The package ships a typed `analyzeControlFlow` API, a README documenting the modeled terminal taxonomy, and a full port of oxc's `eslint/no-unreachable` `pass` / `fail` corpus asserted directly against the graph's `isUnreachable`.
Loading
Loading