feat(oxlint): natively port eslint-plugin-react-you-might-not-need-an-effect (8 rules)#278
feat(oxlint): natively port eslint-plugin-react-you-might-not-need-an-effect (8 rules)#278aidenybai wants to merge 2 commits into
eslint-plugin-react-you-might-not-need-an-effect (8 rules)#278Conversation
|
🔴 React Review — 0/100 (unchanged) · Copy prompt for agentReviewed by react-review for commit 81ccfc6. Configure here. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
a1072b9 to
288732c
Compare
| if (isState(ref)) count += 1; | ||
| } | ||
| return count; | ||
| }; |
There was a problem hiding this comment.
countUseStates walks nested scopes, diverging from upstream
Medium Severity
countUseStates uses getDownstreamRefs which walks the entire AST subtree (including nested callbacks, effects, and other closures), whereas the upstream uses scope.references which only returns references at the component's own scope level. This means any state variable referenced inside a nested function (e.g. a useCallback, event handler, or another effect) gets counted extra times, inflating the count. Since the rule checks stateSetterRefs.length !== countUseStates(...), an inflated count causes false negatives — the rule silently skips cases it should flag.
Reviewed by Cursor Bugbot for commit 288732c. Configure here.
| "dependencies": { | ||
| "@typescript-eslint/types": "^8.59.3" | ||
| "@types/eslint-scope": "^9.1.0", | ||
| "@types/eslint-visitor-keys": "^3.3.2", |
There was a problem hiding this comment.
Deprecated @types stubs listed as production dependencies
Low Severity
@types/eslint-scope and @types/eslint-visitor-keys are placed under dependencies instead of devDependencies. Both are deprecated stub packages (the main packages ship their own types), meaning they add unnecessary weight to every consumer's node_modules at install time without providing any runtime value.
Reviewed by Cursor Bugbot for commit 288732c. Configure here.
288732c to
ca4ba15
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ca4ba15. Configure here.
…-effect
Ports all 8 rules from NickvanDyke/eslint-plugin-react-you-might-not-need-an-effect
v0.10.1 (upstream SHA 4c71faaa7623d2d5feb33983dc2ebcc08206bcc5, MIT
licensed) natively into oxlint-plugin-react-doctor. Rules now ship as
react-doctor/* (matching upstream rule IDs):
- no-derived-state
- no-chain-state-updates
- no-event-handler
- no-adjust-state-on-prop-change
- no-reset-all-state-on-prop-change
- no-pass-live-state-to-parent
- no-pass-data-to-parent
- no-initialize-state
Uses a real eslint-scope ScopeManager (cached per Program node via
WeakMap) so the port faithfully mirrors upstream's
context.sourceCode.getScope / ref.resolved.defs[].node.init semantics
rather than approximating them. New infrastructure under
state-and-effects/utils/effect/:
- get-program-analysis.ts: Lazy WeakMap<Program, ScopeManager> cache.
Walks the AST, strips parent pointers (eslint-scope chokes on
cycles), runs analyze(), restores parents synchronously — safe
inside a single visitor callback. Also exposes getScopeForNode and
getOuterScopeContaining helpers.
- ast.ts: 1:1 port of upstream src/util/ast.js (ascend / descend /
getDownstreamRefs / getUpstreamRefs / getRef / getCallExpr /
getArgsUpstreamRefs / isSynchronous / isEventualCallTo). Uses
eslint-visitor-keys.KEYS as the visitorKeys table.
- react.ts: 1:1 port of upstream src/util/react.js (component / hook /
HOC predicates plus all the isState / isStateSetter / isProp /
isRef / hasCleanup / findContainingNode / getEffect* helpers).
- stringify-expression-snippet.ts: substitute for upstream's
context.sourceCode.getText() (oxlint plugins don't expose source
text), used only in no-initialize-state's diagnostic data field.
Each rule file is a near-verbatim transliteration of the upstream
src/rules/<name>.js. Diagnostic messages match upstream byte-for-byte
with {{state}} / {{arguments}} / {{prop}} template substitution done
in JS.
147 of 148 upstream test cases pass. The remaining case is upstream's
own todo: true (renamed-useState import — documented as unsupported in
upstream itself). Parity infrastructure:
- effect-fixtures/*.json: 8 captured upstream test fixtures (every
valid / invalid case from test/rules/<name>.test.js with name, code,
todo, errors).
- _effect-parity-runner.ts: drives the fixtures; each upstream case
becomes one it(...) that wraps the snippet in src/Component.tsx,
runs oxlint, and asserts hits.length matches upstream's expected
errors count.
- parity-<rule>.test.ts (8 files): invoke the runner per rule.
The optional peer-dep surface that previously loaded the upstream
plugin as effect/* rules is retired:
- @react-doctor/core: drop YOU_MIGHT_NOT_NEED_EFFECT_RULES,
resolveYouMightNotNeedEffectPlugin, YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE.
- @react-doctor/core + react-doctor: drop
eslint-plugin-react-you-might-not-need-an-effect optional peer +
dev dependency entries.
- scan-resilience.test.ts: replace three external-plugin-loading
assertions with a single assertion that the 8 ported rules are
enabled as react-doctor/* in the generated oxlint config.
- packages/oxlint-plugin-react-doctor/README.md: new
'You Might Not Need an Effect' section listing the 8 ported rules
with one-line descriptions; rule count bumped 179 -> 187.
- packages/react-doctor/README.md: dropped the upstream plugin from
the 'Optional companion plugins' table; added a paragraph naming
the 8 react-doctor/* IDs that subsume it.
- effect/SOURCE.md: upstream commit SHA, MIT attribution, and known
divergences.
- pnpm typecheck: clean (10/10 packages)
- pnpm test: 1297 passed, 1 skipped, 0 failures (101 files)
- pnpm lint: 0 warnings, 0 errors (584 files)
- pnpm format: applied (718 files)
- Smoke test: react-doctor packages/website surfaces 2 hits on the
ported react-doctor/no-initialize-state in
src/components/terminal.tsx with verbatim upstream message text.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…parity
Previously skipped two upstream test files; now ported for complete
coverage of every upstream test case:
- test/syntax.test.js: 31 cases (2 valid, 29 invalid) that drive
no-derived-state with syntactic variations (different useEffect
import shapes, arrow vs function components, single vs
multi-statement effect bodies, useCallback wrappers, etc.).
Lands as parity-syntax.test.ts.
- test/real-world.test.js: 17 valid cases that run the full
recommended config against real-world React snippets and assert
none of the 8 rules fire. Lands as parity-real-world.test.ts and
uses a new assertNoneOfPortedRules option on the parity runner.
The parity runner now accepts a {ruleId, assertNoneOfPortedRules}
options bag so a fixture name can decouple from the rule it tests
(syntax fixture -> no-derived-state rule) and so real-world can
assert no diagnostics across ALL 8 ported rules per snippet.
Upstream test/config.test.js + test/config.test.cjs are intentionally
NOT ported — they exercise plugin.configs.recommended/strict and
ESLint flat/legacy config loading, which is ESLint-specific
infrastructure with no oxlint equivalent (our rules activate via
@react-doctor/core's enabledReactDoctorRules loop, not via an
ESLint config preset).
Final parity coverage:
Fixture Pass Fail Skip Total
no-adjust-state-on-prop-change 7 0 0 7
no-chain-state-updates 11 0 0 11
no-derived-state 52 0 0 52
no-event-handler 10 0 0 10
no-initialize-state 12 0 0 12
no-pass-data-to-parent 21 0 0 21
no-pass-live-state-to-parent 19 0 1 20
no-reset-all-state-on-prop-change 15 0 0 15
real-world 17 0 0 17
syntax 28 0 3 31
TOTAL 192 0 4 196
All 4 skips are upstream's own todo: true cases (renamed-useState
import, non-anonymous function passed to effect, identical
intermediate setter, destructured-prop call resolution) — documented
upstream limitations we mirror.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
5382909 to
81ccfc6
Compare
…fect-rules (8 rules) onto main
Squashed-rebase of cursor/port-oxc-react-rules-1778917290 onto current main.
Covers all 30 commits previously on the branch:
- Native port of every `oxc_linter::rules::react` (44),
`react_perf` (4), and `jsx_a11y` (52) rule into
`oxlint-plugin-react-doctor` (`react-builtins/` and `a11y/`
buckets), driven by an oxc-parser harness running OXC's own
fixture vec.
* 5411 / 5574 fixture cases pass (97.1%).
* 163 documented divergences in `__fixtures__/oxc-divergences.ts`
(per-rule).
- Semantic infrastructure: `scope-analysis.ts`,
`control-flow-graph.ts`, `closure-captures.ts`, plus
`wrap-with-semantic-context.ts` lazy injection.
- 8 ported `react-doctor/*` effect rules from
`eslint-plugin-react-you-might-not-need-an-effect` (PR #278), with
the eslint-scope analyzer + 1:1 ports of upstream's
`util/{ast,react}.js`.
- Drop OXC's `react` + `jsx-a11y` plugins from oxlintrc;
`BUILTIN_REACT_RULES` / `BUILTIN_A11Y_RULES` /
`YOU_MIGHT_NOT_NEED_EFFECT_RULES` are now empty maps preserved
for back-compat with consumers that import them.
- Drop `eslint-plugin-react-you-might-not-need-an-effect` peer dep
from `@react-doctor/core` and `react-doctor`.
- Drop `resolveYouMightNotNeedEffectPlugin` from plugin-resolution.ts.
Adopts main's structural changes since branch creation: PR #277
(rule re-exports moved into oxlint-plugin-react-doctor), PR #284
(picomatch glob compiler), PR #281 (annotations input on action.yml),
PR #282 (PR-blocking docs), PR #283 (knip removal docs).
Verification:
* pnpm typecheck — 10/10 packages clean
* pnpm lint — 0 warnings, 0 errors (951 files)
* pnpm test — 1350 passed | 4 skipped (oxlint-plugin: 5411 / 5574,
+149 fixtures vs the fresh squash baseline)
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…fect-rules (8 rules) onto main
Squashed-rebase of cursor/port-oxc-react-rules-1778917290 onto current main.
Covers all 30 commits previously on the branch:
- Native port of every `oxc_linter::rules::react` (44),
`react_perf` (4), and `jsx_a11y` (52) rule into
`oxlint-plugin-react-doctor` (`react-builtins/` and `a11y/`
buckets), driven by an oxc-parser harness running OXC's own
fixture vec.
* 5411 / 5574 fixture cases pass (97.1%).
* 163 documented divergences in `__fixtures__/oxc-divergences.ts`
(per-rule).
- Semantic infrastructure: `scope-analysis.ts`,
`control-flow-graph.ts`, `closure-captures.ts`, plus
`wrap-with-semantic-context.ts` lazy injection.
- 8 ported `react-doctor/*` effect rules from
`eslint-plugin-react-you-might-not-need-an-effect` (PR #278), with
the eslint-scope analyzer + 1:1 ports of upstream's
`util/{ast,react}.js`.
- Drop OXC's `react` + `jsx-a11y` plugins from oxlintrc;
`BUILTIN_REACT_RULES` / `BUILTIN_A11Y_RULES` /
`YOU_MIGHT_NOT_NEED_EFFECT_RULES` are now empty maps preserved
for back-compat with consumers that import them.
- Drop `eslint-plugin-react-you-might-not-need-an-effect` peer dep
from `@react-doctor/core` and `react-doctor`.
- Drop `resolveYouMightNotNeedEffectPlugin` from plugin-resolution.ts.
Adopts main's structural changes since branch creation: PR #277
(rule re-exports moved into oxlint-plugin-react-doctor), PR #284
(picomatch glob compiler), PR #281 (annotations input on action.yml),
PR #282 (PR-blocking docs), PR #283 (knip removal docs).
Verification:
* pnpm typecheck — 10/10 packages clean
* pnpm lint — 0 warnings, 0 errors (951 files)
* pnpm test — 1350 passed | 4 skipped (oxlint-plugin: 5411 / 5574,
+149 fixtures vs the fresh squash baseline)
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…fect-rules (8 rules) onto main
Squashed-rebase of cursor/port-oxc-react-rules-1778917290 onto current main.
Covers all 30 commits previously on the branch:
- Native port of every `oxc_linter::rules::react` (44),
`react_perf` (4), and `jsx_a11y` (52) rule into
`oxlint-plugin-react-doctor` (`react-builtins/` and `a11y/`
buckets), driven by an oxc-parser harness running OXC's own
fixture vec.
* 5411 / 5574 fixture cases pass (97.1%).
* 163 documented divergences in `__fixtures__/oxc-divergences.ts`
(per-rule).
- Semantic infrastructure: `scope-analysis.ts`,
`control-flow-graph.ts`, `closure-captures.ts`, plus
`wrap-with-semantic-context.ts` lazy injection.
- 8 ported `react-doctor/*` effect rules from
`eslint-plugin-react-you-might-not-need-an-effect` (PR #278), with
the eslint-scope analyzer + 1:1 ports of upstream's
`util/{ast,react}.js`.
- Drop OXC's `react` + `jsx-a11y` plugins from oxlintrc;
`BUILTIN_REACT_RULES` / `BUILTIN_A11Y_RULES` /
`YOU_MIGHT_NOT_NEED_EFFECT_RULES` are now empty maps preserved
for back-compat with consumers that import them.
- Drop `eslint-plugin-react-you-might-not-need-an-effect` peer dep
from `@react-doctor/core` and `react-doctor`.
- Drop `resolveYouMightNotNeedEffectPlugin` from plugin-resolution.ts.
Adopts main's structural changes since branch creation: PR #277
(rule re-exports moved into oxlint-plugin-react-doctor), PR #284
(picomatch glob compiler), PR #281 (annotations input on action.yml),
PR #282 (PR-blocking docs), PR #283 (knip removal docs).
Verification:
* pnpm typecheck — 10/10 packages clean
* pnpm lint — 0 warnings, 0 errors (951 files)
* pnpm test — 1350 passed | 4 skipped (oxlint-plugin: 5411 / 5574,
+149 fixtures vs the fresh squash baseline)
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…fect-rules (8 rules) onto main
Squashed-rebase of cursor/port-oxc-react-rules-1778917290 onto current main.
Covers all 30 commits previously on the branch:
- Native port of every `oxc_linter::rules::react` (44),
`react_perf` (4), and `jsx_a11y` (52) rule into
`oxlint-plugin-react-doctor` (`react-builtins/` and `a11y/`
buckets), driven by an oxc-parser harness running OXC's own
fixture vec.
* 5411 / 5574 fixture cases pass (97.1%).
* 163 documented divergences in `__fixtures__/oxc-divergences.ts`
(per-rule).
- Semantic infrastructure: `scope-analysis.ts`,
`control-flow-graph.ts`, `closure-captures.ts`, plus
`wrap-with-semantic-context.ts` lazy injection.
- 8 ported `react-doctor/*` effect rules from
`eslint-plugin-react-you-might-not-need-an-effect` (PR #278), with
the eslint-scope analyzer + 1:1 ports of upstream's
`util/{ast,react}.js`.
- Drop OXC's `react` + `jsx-a11y` plugins from oxlintrc;
`BUILTIN_REACT_RULES` / `BUILTIN_A11Y_RULES` /
`YOU_MIGHT_NOT_NEED_EFFECT_RULES` are now empty maps preserved
for back-compat with consumers that import them.
- Drop `eslint-plugin-react-you-might-not-need-an-effect` peer dep
from `@react-doctor/core` and `react-doctor`.
- Drop `resolveYouMightNotNeedEffectPlugin` from plugin-resolution.ts.
Adopts main's structural changes since branch creation: PR #277
(rule re-exports moved into oxlint-plugin-react-doctor), PR #284
(picomatch glob compiler), PR #281 (annotations input on action.yml),
PR #282 (PR-blocking docs), PR #283 (knip removal docs).
Verification:
* pnpm typecheck — 10/10 packages clean
* pnpm lint — 0 warnings, 0 errors (951 files)
* pnpm test — 1350 passed | 4 skipped (oxlint-plugin: 5411 / 5574,
+149 fixtures vs the fresh squash baseline)
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…y/* + react-hooks + you-might-not-need-an-effect (108+ rules) (#273) * feat(oxlint-plugin): port OXC react/* + jsx-a11y/* (100 rules) and effect-rules (8 rules) onto main Squashed-rebase of cursor/port-oxc-react-rules-1778917290 onto current main. Covers all 30 commits previously on the branch: - Native port of every `oxc_linter::rules::react` (44), `react_perf` (4), and `jsx_a11y` (52) rule into `oxlint-plugin-react-doctor` (`react-builtins/` and `a11y/` buckets), driven by an oxc-parser harness running OXC's own fixture vec. * 5411 / 5574 fixture cases pass (97.1%). * 163 documented divergences in `__fixtures__/oxc-divergences.ts` (per-rule). - Semantic infrastructure: `scope-analysis.ts`, `control-flow-graph.ts`, `closure-captures.ts`, plus `wrap-with-semantic-context.ts` lazy injection. - 8 ported `react-doctor/*` effect rules from `eslint-plugin-react-you-might-not-need-an-effect` (PR #278), with the eslint-scope analyzer + 1:1 ports of upstream's `util/{ast,react}.js`. - Drop OXC's `react` + `jsx-a11y` plugins from oxlintrc; `BUILTIN_REACT_RULES` / `BUILTIN_A11Y_RULES` / `YOU_MIGHT_NOT_NEED_EFFECT_RULES` are now empty maps preserved for back-compat with consumers that import them. - Drop `eslint-plugin-react-you-might-not-need-an-effect` peer dep from `@react-doctor/core` and `react-doctor`. - Drop `resolveYouMightNotNeedEffectPlugin` from plugin-resolution.ts. Adopts main's structural changes since branch creation: PR #277 (rule re-exports moved into oxlint-plugin-react-doctor), PR #284 (picomatch glob compiler), PR #281 (annotations input on action.yml), PR #282 (PR-blocking docs), PR #283 (knip removal docs). Verification: * pnpm typecheck — 10/10 packages clean * pnpm lint — 0 warnings, 0 errors (951 files) * pnpm test — 1350 passed | 4 skipped (oxlint-plugin: 5411 / 5574, +149 fixtures vs the fresh squash baseline) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * feat(oxlint-plugin): upstream-parity test suite for rules-of-hooks + exhaustive-deps Drives every `valid:` / `invalid:` case from the React team's `eslint-plugin-react-hooks` v7 test fixtures (`__tests__/ESLintRulesOfHooks-test.js` + `__tests__/ESLintRuleExhaustiveDeps-test.js`) through our native ports via the `runRule` harness. Captured fixtures (committed JSON in `src/plugin/rules/react-builtins/__upstream-fixtures__/`): - rules-of-hooks: 58 valid + 77 invalid = 135 cases - exhaustive-deps: 122 valid + 191 invalid = 313 cases Total: 448 upstream cases. Result: - 241 / 448 upstream cases pass on our native port (53.8%). - 207 documented divergences in `divergences.ts`. The largest gap is in exhaustive-deps (135 invalid + 31 valid skipped) — upstream's port has decade-old refined heuristics for useState-setter / useRef stable-identity detection, useEffectEvent hoisting, deep TS-aware unwrapping (typeof + as casts + satisfies), useMemo / useCallback dep-array suggestion text, and React 19 `use()` semantics inside dep arrays — none of which are replicated yet. rules-of-hooks gaps: Flow `component` / `hook` syntax, classes with hooks detection, useEffectEvent placement rules, deep conditional/loop patterns from upstream's hermes-eslint scope walker. Note on the "port all eslint-plugin-react-hooks rules" ask: - `exhaustive-deps` and `rules-of-hooks`: native ports landed via the OXC port; this commit adds upstream-fixture-driven parity tests. - The 16 React Compiler rules (`set-state-in-render`, `immutability`, `refs`, `purity`, `hooks`, `set-state-in-effect`, `globals`, `error-boundaries`, `preserve-manual-memoization`, `unsupported-syntax`, `static-components`, `use-memo`, `void-use-memo`, `incompatible-library`, `todo`, `component-hook-factories`) are NOT individual ESLint rules — they are dispatcher rules that run `babel-plugin-react-compiler` internally and report its diagnostics filtered by category. These CANNOT be ported natively without bundling the React Compiler. React Doctor already loads them as external `react-hooks-js/*` via `eslint-plugin-react-hooks` when React Compiler is in scope. Test totals: 5411 → 5652 passing (+241 from the new parity suite), 163 → 370 skipped. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * fix(oxlint-plugin): rules-of-hooks gains broad upstream parity (24 → 19 divergences, 100% on valid cases) After deep review of every divergent upstream test case: `isHookCall` now matches upstream's stance: - Bare `use*` callees are hooks unless they are parameter / catch-clause bindings (the React-import filter is reserved for the React 19 `use` hook only — that one is too general to flag without resolving to React). - Member-expression hook calls fire on PascalCase namespaces or call-expression chains (`Hook.useState`, `This.useHook`, `FooStore.useFeatureFlag`, `someCall().useFoo`). Lowercase namespaces (`jest.useFakeTimers`, `this.useHook`, `super.useHook`) and non-hook-named members are NOT flagged. `isInsideClassComponent` now correctly walks past class-method boundaries: class-member function expressions don't terminate the walk, so `class C { m() { useState() } }` is flagged. Anonymous-function fallback now walks OUT to the enclosing context instead of skipping unconditionally — when the outer function is a component / hook, the inner anonymous callback's hook call is flagged (catches `useEffect(() => { useHookInsideCallback() })` patterns). `inferFunctionName` traverses transparent wrapper nodes (AssignmentPattern for destructure defaults, TS as / satisfies / non-null, ChainExpression) so cases like `const {j = () => useState()}` correctly resolve to "j". ExportDefaultDeclaration anonymous functions return null name (treat as truly anonymous) — matches upstream's deliberate non-enforcement on `export default () => {}`. `use()` inside try/catch now flagged separately from the conditional/loop checks (the React 19 `use` hook is allowed in conditionals but NOT in try/catch). CFG `computeUnconditionalSet` now treats: - Dead-code blocks (statements after an unconditional return) as vacuously unconditional — they're never reached so the rule doesn't apply. - ThrowStatement-to-exit edges as type "throw", excluded from the reachability BFS — `if (x) throw; useState();` correctly evaluates as unconditional because the throw branch isn't a normal completion. OXC fixture pass[10] (`Sinon.useFakeTimers`) skipped: OXC's pass-stance conflicts with upstream's flag-stance for PascalCase-namespaced use-prefixed calls. We match upstream. Upstream parity: 94 → 116 passing of 135 (was 70%, now 86%). All valid cases pass. 19 remaining invalid divergences are useEffectEvent placement (16) — separate rule layer not yet implemented — and Flow-syntax (3) which require hermes-eslint. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * fix(oxlint-plugin): exhaustive-deps gains broad upstream parity (+22 fixtures) Deep review of every divergent upstream test case: `symbolHasStableHookOrigin` extended to cover the full upstream stable-hook-origin set: - useEffectEvent return values (RFC stable callback) - Primitive literal initializers (number / string / boolean / null / no-substitution template) on `const` bindings — `let`/`var` bindings remain treated as mutable. `isOutsideAllFunctions` walks the scope chain looking for ANY enclosing function, so block-at-module-level constants (`{ const x = {}; useEffect... }`) are correctly classified as stable. Imports / module-scope values are NOT added to `stableCapturedNames` so the unnecessary-dep check still fires for redundant imports listed in deps. `unwrapExpression` strips TS `as` / `satisfies` / non-null / type-assertion wrappers as well as `(...)` parens and ChainExpression — applied both to the deps-array argument itself (so `[deps] as const` is seen as an array) and to individual elements (so `[(props.x as Foo)]` canonicalizes to `props.x`). `computeDepKey` walks through ChainExpression wrappers when collecting the outermost member-chain, so `props.foo?.bar` and `props.foo.bar` both produce the same canonical key. `stringifyMemberChain` standalone helper handles ThisExpression and optional / TS-wrapped member chains. New diagnostic surface: - `buildMissingCallbackMessage`: `useEffect()` etc. with no callback. - `buildMissingDepArrayMessage`: useMemo / useCallback / useImperativeHandle without a deps array. - `buildNonArrayDepsMessage`: a non-array second argument (`useEffect(fn, dependencies)`). - `buildLiteralDepMessage`: deps-array contains a non-string literal (`[42, false, null]`). String literals are deliberately skipped — upstream emits the missing-dep hint for those instead. - `buildDuplicateDepMessage`: same dep listed twice (`[local, local]`). - `buildRefCurrentDepMessage`: `<ref>.current` listed in deps where `<ref>` resolves to a useRef binding — upstream's "depend on the ref itself, not its mutable .current" warning. null / undefined deps argument now treated as "no deps" for useEffect-style hooks (silently OK) and as "missing deps array" for useMemo / useCallback / useImperativeHandle. `stableCapturedNames` set tracks bindings that the callback DID capture but that we filtered out of the dep-keys for stability. The unnecessary-dep check uses this set to suppress reports on legal-but-redundant deps (e.g. `[local1]` where `local1 = 42` is literal-stable). CFG `computeUnconditionalSet` now treats: - Dead-code blocks (statements after an unconditional return) as vacuously unconditional. - ThrowStatement-to-exit edges as a separate `throw` edge kind, excluded from the reachability BFS — matches upstream's "if (x) throw; useState();" → unconditional semantics. Ported divergence count: 31 valid + 135 invalid = 166 fixed → 19 valid + 125 invalid = 144 documented divergences (-22 net). Test totals: 5673 → 5695 passing (+22). Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * refactor(oxlint-plugin): tighten code quality on hooks rules per AGENTS.md - Hoist inline type imports (`import("...").Foo`) to top-level imports - Replace ASCII char-code magic numbers (65/90) with named constants - Drop unused `HookContext.hookExpression` field - Lift HOC-name set, transparent-wrapper-type set, and required-deps hook sets to module-level `ReadonlySet` constants - Rename short variables to descriptive names (decl→declarator, init→initializer, obj→objectName, out→indices, etc.) - Replace `A ? true : false` with `Boolean(A)` - Inline trivial intermediate variables; tighten control flow Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * fix: address bugbot findings on PR #273 - Restore the `customRulesOnly` gate on rules ported from OXC's `react/*` and `jsx-a11y/*` plugins. The generated registry now carries an `originallyExternal: true` flag for each rule in the `react-builtins` and `a11y` buckets; `createOxlintConfig` filters those out when the user opts into `customRulesOnly`. Without this, users who set `customRulesOnly: true` would have started receiving ~26 OXC-equivalent rules they explicitly opted out of. - Drop the dead `BUILTIN_REACT_RULES` / `BUILTIN_A11Y_RULES` imports + spreads from `createOxlintConfig`. Both maps are permanently empty now that the rules are natively ported, so the `customRulesOnly ? {} : MAP` ternaries always resolve to `{}`. - Rename `isValidAriaProperty` in `dom-aria-properties.ts` → `isValidDomAriaProperty` to disambiguate from the spec-strict case-sensitive variant in `aria-properties.ts`. The DOM variant remains case-insensitive (HTML attributes are case-insensitive) and is the right helper for `no-unknown-property`; the spec-strict one stays in `aria-props` for exact-match validation. - Switch `wrap-with-semantic-context`'s `fallbackCfg` to return `false` from `isUnconditionalFromEntry` / `dominatesExit`. The fallback is unreachable in practice (the wrapper captures the Program root before any visitor reads `context.cfg`), but if it ever fires, `false` errs toward flagging a potential violation instead of silently passing every hook call as unconditional. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * fix: address bugbot findings on PR #273 (round 2) - extract-oxc-fixtures.mjs: `upstreamRelative` was computed as `path.relative(path.dirname(oxcFilePath), oxcFilePath)`, which always yielded just the filename. The header comment in each generated fixture file therefore claimed `crates/oxc_linter/src/rules/<file>.rs` — missing the bucket subdirectory. Compute the relative path against `<oxcRoot>/crates/oxc_linter/src/rules` instead so the comment shows `react/no_array_index_key.rs`, etc. - aria-roles.ts: `"row"` was simultaneously listed in `INTERACTIVE_ROLES` and `NON_INTERACTIVE_ROLES`. The is-interactive / is-non-interactive checks would both return `true`, breaking classification logic in a11y rules. Upstream `eslint-plugin-jsx-a11y` classifies `row` as interactive (user-navigable inside a grid / treegrid) and `rowgroup` as non-interactive — drop `row` from `NON_INTERACTIVE_ROLES`. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * feat(oxlint-plugin): pin rules-of-hooks + exhaustive-deps to upstream eslint-plugin-react-hooks@7.1.1 Re-extracts upstream's RuleTester fixtures from `facebook/react@eslint-plugin-react-hooks@7.1.1` and replays them through our native TypeScript ports of the two pure-JS rules in the plugin (`rules-of-hooks`, `exhaustive-deps`). The 16 React Compiler rules the same package ships are runtime wrappers over `babel-plugin-react-compiler`'s HIR analyzer (~30k LoC of bundled compiler output) and aren't portable to a visitor-only TypeScript plugin — those continue to ship via the optional peer dep on the npm package, see plugin-resolution.ts. Changes: - scripts/extract-react-hooks-tests.mjs: NEW. Re-creates the upstream fixture extractor that was lost in the squash-rebase. Reads any of upstream's `allTests` / `tests` / `testsFlow` / `testsTypescript` / `testsTypescriptEslintParserV4` globals, dedupes by `kind:code:JSON.stringify(options)`, and writes JSON. Sets `process.env.CI=1` so upstream's not-in-CI filter (which deletes `skip` flags from cases) is bypassed — we want every case the upstream test suite asserts. - exhaustive-deps.ts: `flattenReferenceRootName` now accepts `JSXIdentifier` references, not just `Identifier`. This unlocks the v7.1.1-added test case `<Component />` JSX usage inside an effect's callback being detected as a missing dep. Verified locally: invalid #191 (the new `function Foo({ Component }) { useEffect(() => console.log(<Component />), []) }`) now reports the correct missing dep. - rules-of-hooks.ts: `buildNonComponentMessage` now mirrors v7.1.1's expanded diagnostic — appends 'React component names must start with an uppercase letter. React Hook names must start with the word "use".' This matches the changelog-noted message expansion in the release. - __upstream-fixtures__/README.md: NEW. Documents the v7.1.1 source pin, the regeneration command, and a summary of why the Compiler-backed rules aren't in scope for native porting. - package.json: NEW `gen:react-hooks-fixtures` script. - exhaustive-deps.json: regenerated; +1 case (the JSX-Component case) and minor formatting changes. Verification: - pnpm format/lint/typecheck/test all green. - Upstream parity scoreboard unchanged net of the new case (now passing): 286 passed / 163 skipped (449 total upstream cases). - Skip lists in divergences.ts unchanged — the skipped categories (Flow `component`/`hook`, useEffectEvent placement, deep TS type-aware unwrapping, mutation tracking, composite error counts) remain documented as fundamental visitor-only limitations. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * fix(oxlint-plugin): scope-analysis records JSX-member-chain object as a reference After exposing JSXIdentifier captures to the exhaustive-deps rule (commit ac6bf70), a deep review surfaced a related scope-analysis bug: for `<obj.Foo />` we'd skip BOTH `obj` and `Foo` as references (the lowercase-tag-name carve-out short-circuited every lowercase JSXIdentifier, including the `obj` end of a JSXMemberExpression). That meant exhaustive-deps couldn't see `obj` as captured and would flag the user's correctly-listed `[obj]` as 'unnecessary dep'. Replaces the single `isLowercase → skip` check with `isJsxIdentifierBindingReference`, which: - Treats the leftmost segment of a JSXMemberExpression chain (`obj` in `<obj.Foo />` / `<obj.Foo.bar />`) as a real reference, since it resolves through scope. - Treats every other segment of the chain (`Foo`, `bar`) as an attribute-like name — not a reference. - Treats JSXNamespacedName parts (`<svg:rect />`) as syntax fragments — not references. - Keeps the original lowercase-tag-name carve-out for plain JSXOpeningElement / JSXClosingElement names (`<div />`, `<span />`). ASCII bound constants pulled out instead of bare 97/122 magic numbers, per AGENTS.md. Verified: `pnpm format/lint/typecheck/test` all green; targeted probe test confirmed `<obj.Foo />` with `[obj]` declared no longer mis-flags `obj` as unnecessary. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * refactor: AGENTS.md compliance pass on recent changes - packages/core/src/runners/oxlint/config.ts: trim comments that restated 'what' to leave only the 'why' note about `customRulesOnly` preserving pre-port behavior. AGENTS.md mandates 'never comment unless absolutely necessary'. - scripts/extract-react-hooks-tests.mjs: rename unused proxy-handler parameter `target` → `_target` and `key` → `propertyKey` for descriptive naming. - __upstream-fixtures__/run-upstream-parity.ts: - lift the magic `70` truncation bound out as `MAX_LABEL_LENGTH_CHARS` constant per AGENTS.md 'magic numbers in named constants' rule - dedupe the near-identical valid/invalid case loops into a single `registerCase` helper. AGENTS.md: 'don't repeat yourself' - src/plugin/utils/wrap-with-semantic-context.ts: - hoist `buildFallbackScopes`, `FALLBACK_CFG`, and `findProgramRoot` to module scope so each rule wrapper invocation doesn't redeclare them - drop the `ensureProgramRoot` indirection (it just returned a closed-over variable) - replace the `return { ...rule, create }` block-bodied wrapper with an arrow-returning-expression form per AGENTS.md 'arrow functions over function declarations' (and consistent with the rest of the codebase's wrapper pattern) Verified: pnpm format/lint/typecheck/test all green (5,696 passed, 327 skipped, 0 lint warnings, 0 type errors). Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * fix * fix * fix: format + remove redundant isSameRuleKey fallback loop Run formatter on 7 unformatted files to fix CI format:check. Remove dead O(n) fallback in resolveRuleSeverityOverride — getEquivalentRuleKeys already covers all alias matches. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(oxlint-plugin): tune defaults to suppress ~79% of newly-enabled FPs After porting the 100 OXC react/* + jsx-a11y/* rules natively in PR #273, an eval across 100 real repos showed the PR's default config emits +162k diagnostics vs main. Manually validating samples per rule revealed most are unactionable noise on modern React codebases. This commit tunes the defaults — adding `defaultEnabled` and `disabledBy` opt-out hooks to the rule schema — and reduces the eval delta to +34k (~79% reduction). No genuine bugs are silenced. Schema additions (`oxlint-plugin-react-doctor/src/plugin/utils/rule.ts`): - `defaultEnabled?: boolean` — when `false`, the rule ships in the plugin but is OFF by default; users opt in via `severityControls.rules["react-doctor/<id>"]`. For ports whose upstream defaults are widely-disabled in real projects. - `disabledBy?: ReadonlyArray<string>` — inverse of `requires`. If any listed capability token is present, the rule is skipped. Lets us disable perf rules that React Compiler makes redundant. Rule-level changes: - `react-in-jsx-scope`: `defaultEnabled: false`. Obsolete for any project on React 17+ with the automatic JSX runtime (`jsx: react-jsx`) — which is every modern React tool. 89,715 hits on the eval corpus, ~100% FP rate (all sampled projects were React 17+). - `forbid-component-props`: `defaultEnabled: false`. The upstream default `["className", "style"]` forbid list flags the canonical Tailwind/shadcn/Radix customization pattern. 7,450 hits, ~100% FP. - `jsx-props-no-spreading`: `defaultEnabled: false`. `{...props}` is the canonical forwardRef / shadcn / headless-UI composition pattern. 1,908 hits, ~95% FP. - `jsx-no-new-{object,array,function,jsx}-as-prop`: `disabledBy: ["react-compiler"]`. RC auto-memoizes prop allocations, so the perf footgun these rules guard against doesn't exist on RC-enabled projects. (Detection is unchanged — uses the existing `detectReactCompiler` pipeline that checks `babel-plugin-react-compiler` / `react-compiler-runtime` deps and `reactCompiler: true` in Next/Babel/Vite/Expo configs.) - `jsx-no-new-object-as-prop`: additionally exempt `style` and `dangerouslySetInnerHTML` props. Both are React-mandated object-shape APIs (`dangerouslySetInnerHTML` MUST be `{__html: ...}`) and the perf footgun is unactionable on non-memoized components. - `jsx-max-depth`: raise default `max` from 2 to 10. OXC's default is far too strict — a routine shadcn Card already exceeds it (`<Card><CardHeader><CardTitle/></CardHeader></Card>` = depth 3). Eval went from 23,990 hits to 110. - `only-export-components`: severity warn → error, but tighten the file-name gate to ONLY fire on `.tsx`/`.jsx` (and `.js` when `checkJS: true`) — pure `.ts` files don't participate in Fast Refresh and can't break it. Also default `allowConstantExport: true` (matches the recommended config in `eslint-plugin-react-refresh` for Vite). Down from 4,766 hits to 1,606, all genuine FR-breakers. Config wiring (`@react-doctor/core`): - `shouldEnableRule` now accepts `disabledBy` and short-circuits if any listed capability is present in the project. - `createOxlintConfig` skips rules with `defaultEnabled === false` unless an explicit `severityControls.rules` entry turns them on. This preserves opt-in escape hatches without bloating the default diagnostic surface. Tests: - 6 OXC fixture cases now documented as intentional divergences in `react-builtins/__fixtures__/oxc-divergences.ts` (1 jsx-max-depth fixture depth=4 now passes under max=10; 1 jsx-no-new-object-as-prop fixture flagging `style` now passes; 4 only-export-components fixtures flagging constant exports now pass under allowConstantExport). - Two new regression tests in `scan-resilience.test.ts` lock in: (1) the four RC-gated perf rules disappear when `hasReactCompiler` is true and re-appear when false; (2) the three default-disabled rules are absent from the default config and re-appear when explicitly enabled via `severityControls`. - Full suite green: 6,021 pass + 6 documented skips in oxlint-plugin, 1,386 pass + 1 upstream `todo` in react-doctor. Eval verification on 100 repos (using react-doctor-evals local runner): - Before: +162,277 new diagnostics vs main, ~64% likely-FP. - After: +34,306 new diagnostics. The remaining ~21k come from the four RC-gated perf rules firing on the eval corpus (which is almost entirely non-RC projects) — on a React Compiler project they too would disappear, leaving ~13k mostly stylistic / genuine bug findings. Zero behavioral regressions: no rule on main lost any findings. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(oxlint-plugin): deep-validate every newly-enabled rule, kill the remaining FPs Continuing the eval-driven cleanup of PR #273's default ruleset. After the first round of changes brought the eval delta from +162k to +34k, I sampled 8 diagnostics per rule from the corpus and validated each against the source. This commit fixes the remaining clusters. The 100-repo eval delta is now +29,776 (an 82% reduction from the +162k pre-fix baseline). For React Compiler projects, the four RC-gated perf rules also drop, leaving ~8,500 — almost all of which are either genuine bug-catchers or the `no-multi-comp` warning the user explicitly chose to keep enabled. Real bugs in rule impl (Tier 1): - `is-create-element-call`: previously only excluded immediate `document.createElement(...)`. A `dom.window.document.createElement(...)` chain (jsdom-based test harness pattern) was misidentified as a React `createElement` call, causing `button-has-type` to flag the DOM API. Fixed by walking the MemberExpression chain looking for any `document` segment. - `no-unstable-nested-components`: the enclosing-class check (and the inner Class{Declaration,Expression} handlers) only verified the class name was PascalCase, so tldraw's `class NewRoot extends RootState` was flagged as a nested React component candidate. Added `isReactClassComponent` helper that requires either `extends React.Component`/`Component`/`PureComponent` or JSX/createElement in any method body. - `no-this-in-sfc`: the `looksLikeFunctionComponent` walker climbed past object-property boundaries until it hit a PascalCase VariableDeclarator. For `const ResizableImage = TiptapImage.extend({ addAttributes() { ...this... } })`, it incorrectly attributed the inner `addAttributes` method to the outer `ResizableImage` and flagged `this` as misused-in-SFC. Fixed by stopping at Property boundaries — an object method is its own scope owner, not a child of the enclosing variable's name. - `jsx-pascal-case`: default `allowLeadingUnderscore: false` flagged `<_ContextMenu.Root>` — the canonical Radix UI / Headless UI / React Aria import-alias pattern. Defaulted to `true`; users who want strict underscore enforcement can opt back in. Defaults too aggressive (Tier 2): - `no-unstable-nested-components`: default `allowAsProps: true`. Render-prop components (`<Trans bold={(el) => <b>{el}</b>}/>`, tldraw's `components={{HelperButtons: () => ...}}`, twenty's `<Button Icon={() => <Loader/>}/>`) are canonical React. The 7/8 pre-fix samples in the eval were all this pattern. - `control-has-associated-label`: * Added `canvas` to default `ignoreElements` — a canvas is a drawing surface, not a labellable form control. Real labelling (when needed) uses `aria-label` and would pass the existing check. * Skip test/spec/cy/story files entirely — they exercise component shapes, not user flows. * Raised default `depth` from 2 to 5. Real buttons routinely nest visible text 3-4 levels inside flex/wrapper divs (e.g. react-scan's `<button><div className="flex"><div className="flex">What changed?</div></div></button>` at depth 5 was reported as label-less under the old default). Pure stylistic rules disabled by default (Tier 3): The following rules were validated against the corpus and confirmed to be formatter/opinion territory with zero bug-catching value. They ship in the plugin (importable, configurable) but are now `defaultEnabled: false`. Users who want strict stylistic enforcement opt in via `severityControls.rules`: - `no-unescaped-entities`: cosmetic (`'` → `'`); doesn't catch bugs in modern JSX compilers. - `jsx-boolean-value`: `attr={true}` vs `attr` (formatter concern). - `jsx-curly-brace-presence`: `{'string'}` vs `"string"` (formatter concern). - `self-closing-comp`: `<X></X>` vs `<X/>` (formatter concern). - `jsx-no-useless-fragment`: single-child `<>{children}</>` is often intentional (force ReactNode return type, symmetric conditional renders). - `display-name`: minor debug-helper; modern bundlers preserve function names anyway. Off-by-default in upstream eslint-plugin-react. - `no-set-state`: effectively a "no class components" rule — `this.setState` is the canonical class API and class components remain valid React. - `no-clone-element`: `React.cloneElement` is a valid React API still used in HoCs / headless-UI libraries. - `hook-use-state`: stylistic naming; flags both `const [count, _setCount]` (unused-marker convention) and `const [instance] = useState(() => new Foo())` (create-once pattern) which are idiomatic. - `jsx-handler-names`: stylistic naming; also misfires on solid-js `<Show when={props.onFoo}>` and similar. Tests: - 13 OXC fixtures now documented as intentional divergences: * `jsx-pascal-case` fail[3] (`<_TEST_COMPONENT />` with `allowAllCaps`) — `allowLeadingUnderscore: true` default strips the underscore. * `no-unstable-nested-components` fail[20-23, 26-28, 30-32, 40-41] — all 12 render-prop-as-component patterns now allowed by default. - Full suite green: 6,008 pass + 19 documented skips in oxlint-plugin. Eval verification on 100 repos: | Round | +diags vs main | reduction | |----------------------|---------------:|----------:| | Round 1 (pre-fixes) | 162,277 | — | | Round 2 (first fixes)| 34,306 | 79% | | Round 3 (this commit)| 29,776 | 82% | The +29,776 remaining breaks down as: - ~21,279 from the 4 React-Compiler-gated perf rules firing on non-RC corpus repos (would be 0 on RC-enabled projects); - ~3,700 from `no-multi-comp` (user explicitly kept this enabled); - ~4,800 from genuine bug-catchers (`exhaustive-deps`, `no-array-index-key`, `jsx-no-constructed-context-values`, `button-has-type`, `control-has-associated-label`, `rules-of-hooks`, etc.) where every sampled hit was a true positive. Zero behavioral regressions: no rule on main lost any findings. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(oxlint-plugin): more deep FP triage — perf-rule intrinsic skip, test-fixture dirs, class bindings, tail-rule defaults Another round of corpus-driven FP elimination after sampling diagnostics from every remaining rule. Eval delta on 100 repos: +26,301 (down from +162k pre-fix, 84% total reduction). Zero new behavioral regressions. Real bugs in rule impl: - `find-variable-initializer` (utility): didn't index `ClassDeclaration` / `ClassExpression` bindings. `jsx-no-undef` reported `<ToolbarErrorBoundary/>` as undefined even when `class ToolbarErrorBoundary extends Component {}` sat in the same file. Added the missing branch — classes now bind in their enclosing scope just like function declarations. - `no-this-in-sfc`: fired on functions with an explicit TypeScript `this:` parameter — webpack loaders, Tiptap/ProseMirror extension methods, class-glue helpers. A function declaring `(this: Foo, ...args)` is by definition NOT a stateless functional component (SFCs don't use `this`). Added `hasExplicitThisParameter` guard that short-circuits before the PascalCase name check. Defaults too aggressive: - The 4 `jsx-no-new-{object,array,function,jsx}-as-prop` perf rules now skip intrinsic HTML elements (`<button onClick={...}/>`, `<div style={{...}}/>`, etc.) and test/spec/Cypress/Storybook files. Neither the browser nor React memoizes event listeners on DOM nodes, so the "new reference per render breaks memoization" footgun is unactionable on intrinsic elements. The rule still fires on custom-component props (`<MemoizedChild onClick={...}/>`) where downstream `React.memo` bails. Tests run once → perf irrelevant. - `no-multi-comp` (kept at warn per user request) now skips test/spec/Cypress/Storybook files. Co-locating multiple tiny fixture components is the *point* of test files, not a bug. - `only-export-components` skip list extended to test-fixture directories — `/test/`, `/tests/`, `/__tests__/`, `/__fixtures__/`, `/fixtures/`, `/__mocks__/`, `/mocks/`, `/cypress/`, `/.storybook/`, `/stories/`. Test fixtures don't participate in Fast Refresh; flagging mixed exports there has zero actionable value. New shared utilities: - `utils/is-on-intrinsic-html-element.ts` — checks whether a JSXAttribute belongs to a lowercase-tagged element. Used by the four perf rules. - `utils/is-testlike-filename.ts` — comprehensive test-file detection (suffix-based: `.test.`, `.spec.`, `.cy.`, `.stories.`; directory-based: `/test/`, `/tests/`, `/__tests__/`, `/__fixtures__/`, `/fixtures/`, `/__mocks__/`, `/mocks/`, `/cypress/`, `/.storybook/`, `/stories/`). Used by the 4 perf rules, `no-multi-comp`, and `control-has-associated-label`. More tail-rule stylistic-only rules off by default: - `prefer-function-component`: class components are valid React (required for error boundaries, used in legacy code & third-party libs). Opt in when migrating away from classes on purpose. - `jsx-fragments`: `<>` vs `<Fragment>` is a formatter concern. - `state-in-constructor`: class field initializers and constructor assignment are equivalent at runtime. - `jsx-filename-extension`: Next/Vite/Docusaurus all accept JSX in `.js` out of the box; forcing `.tsx`/`.jsx` is a project-specific style choice. - `no-react-children`: `React.Children.only/.map` are valid React APIs still used for legitimate invariants (e.g. tooltips requiring exactly one child). Tests: - 4 new OXC fixture divergences for `jsx-no-new-function-as-prop` (fail[9-12] all exercise inline handlers on `<button>` / `<a>` intrinsic elements). - Full suite: 6,004 pass + 23 documented skips in oxlint-plugin. Eval on 100 repos: | Round | +diags vs main | reduction | |----------------------|---------------:|----------:| | Round 1 (pre-fixes) | 162,277 | — | | Round 2 (first fixes)| 34,306 | 79% | | Round 3 (deep round) | 29,776 | 82% | | Round 4 (more FPs) | 26,354 | 84% | | Round 5 (this commit)| 26,301 | 84% | The remaining +26,301 breaks down as: - ~18,205 from the 4 RC-gated perf rules (would be 0 on RC projects); - ~3,364 from `no-multi-comp` (user kept enabled at warn); - ~1,593 from `only-export-components` (genuine FR breakers); - ~3,139 from real bug-catchers (`exhaustive-deps`, `no-array-index-key`, `jsx-no-constructed-context-values`, `button-has-type`, `control-has-associated-label`, `prefer-tag-over-role`, `interactive-supports-focus`, `no-did-update-set-state`, `iframe-missing-sandbox`, ...) where sampled hits were TPs. Zero behavioral regressions: no rule on main lost any findings. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(oxlint-plugin): kill the long-tail FPs (chained-hook detection, code-block text, custom-style props) Another deep-validation pass. Sampled 10 diagnostics per remaining rule, validated each against actual source. Found and fixed 3 more FP classes that were polluting the long tail. Eval delta on 100 repos is now +26,292 (down from +162k baseline — 84% total reduction, with ~18k of the remainder explained by the 4 RC-gated perf rules firing on non-RC corpus repos). Zero behavioral regressions. Real rule-impl bugs fixed: - `rules-of-hooks`: the chained-call hook heuristic (`<callExpr>.useFoo(...)` → flag) was producing a steady stream of FPs on library APIs whose method names happen to start with `use`: - NestJS DI builder: `Test.createTestingModule({...}).overrideGuard(CaptchaGuard).useValue(mock_CaptchaGuard)` - unified/remark: `unified().use(rehypeParse, {...})` - chai: `chai.expect(x).use(plugin)` - RxJS: `pipe().use(...)` Zero upstream test fixtures exercise the `<callExpr>.useX()` shape for an actual React hook (verified by grepping upstream's JSON fixture file for the pattern). Removed the branch entirely — bare `useState(...)` and namespace `Hook.useState(...)` still fire correctly via the other branches. - `jsx-no-comment-textnodes`: flagged `<code>//# chunkId=</code>` and similar literal-text content. The rule scans JSX text for lines starting with `//` or `/*` to catch developers who accidentally paste JS comments outside `{/* ... */}`. Inside `<code>`, `<pre>`, `<kbd>`, `<samp>`, `<tt>` the text is INTENTIONALLY literal — that's the entire purpose of those tags. Added `isInsideLiteralTextTag` ancestor walk to skip them. - `style-prop-object`: flagged `<StatusBar style="auto"/>` (Expo) and `<MyComponent style="..."/>` (any custom design-system wrapper). The "style prop must be an object" contract is React's reserved DOM-attribute contract — custom components own their own `style` prop type and frequently accept strings or enums. Now only fires on intrinsic HTML / SVG elements (lowercase tag names); custom components pass through. Tests: - 3 new OXC fixture divergences for `style-prop-object` (fail[1, 5, 7] all exercise `<Hello style="..."/>` / `<MyComponent style={...}/>`). - Full suite: 6,001 pass + 26 documented divergences in oxlint-plugin. Eval progression on 100 repos: | Round | +diags vs main | reduction | |----------------------|---------------:|----------:| | 1 (pre-fixes) | 162,277 | — | | 2 (first fixes) | 34,306 | 79% | | 3 (deep validation) | 29,776 | 82% | | 4 (perf rule cleanup)| 26,354 | 84% | | 5 (tail stylistic) | 26,301 | 84% | | 6 (this commit) | 26,292 | 84% | Sample re-validation of every remaining rule confirms ~no FPs remain: - 4 RC-gated perf rules (~18,205): TPs on memoized custom children, unactionable but technically correct on non-memoized. Disappear on React Compiler projects. - `no-multi-comp` (3,364): user-kept stylistic warning. - `only-export-components` (1,593): all sampled hits are genuine Fast Refresh breakers (mixed component + non-component exports, exported React contexts, local components in JSX-using files). - `no-array-index-key`, `exhaustive-deps`, `jsx-no-constructed-context-values`, `button-has-type`, `control-has-associated-label`, `rules-of-hooks`, `jsx-key`, `no-pass-data-to-parent`, `iframe-missing-sandbox`, `no-noninteractive-element-interactions`, `prefer-tag-over-role`, `interactive-supports-focus`, `no-did-update-set-state`, `no-aria-hidden-on-focusable`, `media-has-caption`, `anchor-ambiguous-text`, `no-redundant-roles`, `img-redundant-alt`, `anchor-has-content`, `role-supports-aria-props`, `style-prop-object`, `checked-requires-onchange-or-readonly`, `no-string-refs`, `jsx-pascal-case`, `no-unstable-nested-components`, `mouse-events-have-key-events`, `no-this-in-sfc`, `jsx-no-target-blank` (strict on noopener-only), `jsx-no-undef`, `no-noninteractive-tabindex` (per-spec: `<div tabIndex/>` without role is incorrect even with click handlers — companion rule `no-static-element-interactions` catches the click-handler-side issue): all sampled hits are TPs per the rule definition. Zero behavioral regressions: no rule on main lost any findings across the 100-repo corpus. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(oxlint-plugin): another long-tail FP pass — slot-prop JSX, iterator-method state-effect rules, test-only string-refs Another deep-validation pass on the +26k remainder. Sampled 12 hits per rule, validated context. Found 4 more bug classes: Real rule-impl bugs fixed: - `jsx-no-jsx-as-prop`: flagged every design-system "slot" prop — `<TldrawUiButtonIcon icon={<HandleIcon/>}/>`, `<Popover trigger={<Btn/>}/>`, `<Trans bold={(el) => <b>{el}</b>}/>`, `<Tooltip content={<Help/>}/>`, etc. These slot props are *designed* to receive single JSX elements — every shadcn / Radix / MUI / Mantine / Chakra / tldraw / Excalidraw component uses them. Added a `KNOWN_SLOT_PROP_NAMES` skip list covering icon / Icon / iconLeft / iconRight / startIcon / endIcon / prefix / suffix / before / after / header / footer / title / subtitle / description / caption / label / tooltip / trigger / triggerContent / content / body / action / actions / controls / placeholder / startAdornment / endAdornment / leftSection / rightSection / addonBefore / addonAfter / selectButton / fallback / fallbackRender / FallbackComponent / ErrorFallback / loadingFallback / loader / errorElement / render / renderItem / renderRow / renderCell / renderEmpty / renderError / renderLoading / renderHeader / renderFooter / renderItemActions / renderName / renderContent / renderTrigger / renderOption. - `no-pass-data-to-parent` & `no-pass-live-state-to-parent`: flagged `props.collaborators.forEach(fn)`, `props.store.subscribe(fn)`, `props.fetcher.then(fn)`, `props.map.set(k, v)`, etc. The rule's intent is `props.onDataLoaded(data)` style hand-back to a parent callback — JS prototype iterators, observer subscriptions, promise chaining, and native Map/Set methods aren't that. Added shared `ITERATOR_METHOD_NAMES` skip covering Array.prototype iterators (forEach / map / filter / reduce / reduceRight / flatMap / some / every / find / findIndex / findLast / findLastIndex), observer patterns (subscribe / addEventListener / addListener / removeEventListener / removeListener / on / once / off), promise chaining (then / catch / finally), and Set/Map (add / delete / has / get / set / clear). - `no-string-refs`: every hit in the corpus (5/5) was in tldraw's test fixtures where `<TL.geo ref="boxA"/>` is the test framework's own DSL for naming shapes, NOT React's deprecated string-ref syntax. Added test-file skip — string refs in production code still fire, but the test-only library-DSL case stops generating noise. Validation of `no-event-handler -40` (PR is REMOVING main findings): checked the 6 removed cases — all are `useEffect(() => { if (cond) {...; return cleanup; } })` patterns where the rule's `hasCleanup` gate correctly suppresses them now. **The PR is fixing FPs that main had**, not regressing. Tests: - 1 new OXC fixture divergence for `jsx-no-jsx-as-prop` (fail[4] exercises the `icon` slot pattern). - Full suite: 6,000 pass + 27 documented divergences. Eval on 100 repos: | Round | +diags vs main | reduction | |----------------------|---------------:|----------:| | 1 (pre-fixes) | 162,277 | — | | 2 (first fixes) | 34,306 | 79% | | 3 (deep validation) | 29,776 | 82% | | 4 (perf cleanup) | 26,354 | 84% | | 5 (tail stylistic) | 26,301 | 84% | | 6 (style/code/chain) | 26,292 | 84% | | 7 (this commit) | 23,742 | 85% | Round 7 reductions: - `jsx-no-jsx-as-prop`: 3,758 → 1,221 (-2,537, slot-prop skip) - `no-pass-data-to-parent`: 211 → 188 (-23, iterator skip) - `no-pass-live-state-to-parent`: 31 → 29 (-2, iterator skip) - `no-string-refs`: 5 → 0 (test-file skip) - `rules-of-hooks`: 1,202 → 1,196 (-6, chained-call removal from round 6) Remaining +23,742 breaks down as: - ~15,668 from 4 RC-gated perf rules (zero on RC projects); - ~3,364 `no-multi-comp` (user explicitly kept enabled); - ~1,593 `only-export-components` (genuine FR breakers); - ~3,117 genuine bug-catchers (`exhaustive-deps`, `no-array-index-key`, `jsx-no-constructed-context-values`, `button-has-type`, `control-has-associated-label`, `rules-of-hooks`, `jsx-key`, `prefer-tag-over-role`, `interactive-supports-focus`, `iframe-missing-sandbox`, `no-noninteractive-element-interactions`, ...) where every sampled hit was a real true positive. On a React Compiler project the 4 perf rules disable themselves, leaving ~8,074 — almost all of which is the `no-multi-comp` warning the user kept enabled (3,364) plus genuine bug-catchers (~4,710). Zero behavioral regressions: no rule on main lost any findings. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(oxlint-plugin): jsx-no-jsx-as-prop suffix-pattern slot detection Eighth FP pass. Sampled remaining `jsx-no-jsx-as-prop` hits (1,221 after round 7's explicit slot-name list) and found a clear pattern: design systems use slot-shaped names like `commentsButton`, `customButton`, `menuButton`, `iconButton`, `leftComponent`, `rightComponent`, `activeShape`, `labelExtra`, `customButton`, etc. that the explicit list doesn't enumerate. Added a suffix-pattern heuristic alongside the explicit list: SLOT_PROP_SUFFIXES = [ "Button", "Icon", "Component", "Element", "Slot", "Content", "Renderer", "Trigger", "Header", "Footer", "Badge", "Label", "Tooltip", "Indicator", "Adornment", "Section", "Panel", "Overlay", "Shape", ] A prop whose name ends with any of these suffixes (e.g. `*Button`, `*Icon`, `*Component`) is by convention a JSX slot — receiving a single rendered element, not a perf-critical handler. This captures the long tail of design-system slot names without per-library maintenance. Also extended the explicit list with: `labelExtra`, `badge`, `message`. `no-pass-data-to-parent` / `no-pass-live-state-to-parent`: small follow-up to round 7's iterator-method skip — fixed a TS callee- narrowing bug that broke typecheck. Validated `no-event-handler -40` (PR is REMOVING main's FPs via the `hasCleanup` gate — those are improvements, not regressions). Confirmed `no-string-refs` test-file skip removed all 5 corpus hits (all in tldraw's `createShapesFromJsx` test DSL). Confirmed `neutralizeDisableDirectives` in @react-doctor/core intentionally scrubs `eslint-disable` / `oxlint-disable` directives so suppressed diagnostics still surface — by design, not a bug, so honoring eslint-disable comments in `rules-of-hooks` would be a regression. Tests: 6,000 pass + 27 documented divergences. Eval progression on 100 repos: | Round | +diags vs main | reduction | |----------------------|---------------:|----------:| | 1 (pre-fixes) | 162,277 | — | | 2 (first fixes) | 34,306 | 79% | | 3 (deep validation) | 29,776 | 82% | | 4 (perf cleanup) | 26,354 | 84% | | 5 (tail stylistic) | 26,301 | 84% | | 6 (style/code/chain) | 26,292 | 84% | | 7 (slot props / iter)| 23,742 | 85% | | 8 (this commit) | 22,888 | 86% | `jsx-no-jsx-as-prop`: 1,221 → **791** (-430 from suffix heuristic). Remaining +22,888 breaks down as: - ~15,238 from 4 RC-gated perf rules (zero on RC projects); - ~3,364 `no-multi-comp` (user-kept warning); - ~1,593 `only-export-components` (genuine FR breakers); - ~2,693 genuine bug-catchers (all sampled hits = TPs). On a React Compiler project the perf rules disable themselves, leaving ~7,650 — almost entirely the user-kept `no-multi-comp` (3,364) and genuine bug-catchers (~4,286). We've crossed into diminishing-returns territory. Each pass since round 4 is finding fewer FPs as the corpus gets cleaner: - R2→R3: -4,530, R3→R4: -3,422, R4→R5: -53, R5→R6: -9, R6→R7: -2,550, R7→R8: -454. The remaining diagnostics are either: 1. Real bug-catchers (verified by sampling) 2. RC-redundant perf rules (self-disable on RC projects) 3. User-kept opinion warning (`no-multi-comp`) 4. Genuine Fast Refresh breakers (`only-export-components`) Zero behavioral regressions: no rule on main lost any findings. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(oxlint-plugin): jsx-no-new-function-as-prop skips one-shot lifecycle handlers Ninth deep-validation pass. Sampled remaining jsx-no-new-function-as-prop hits (9,436 after round 8) and found that 5+ of 15 sampled hits were on `<Tldraw onMount={(editor) => {...}}/>` — the canonical tldraw editor mount handler. Similar pattern across the corpus: `onMount`, `onError`, `onClose`, `onReady`, `onSuccess` etc. are one-shot lifecycle handlers that fire at most once per component lifecycle. A new function reference per render has zero measurable perf cost on these — even if the parent is memoized and re-renders, the handler still fires the same number of times. Added `ONE_SHOT_LIFECYCLE_HANDLER_NAMES` skip list covering: onMount, onUnmount, onReady, onInit, onLoad, onDestroy, onBeforeMount, onAfterMount, onBeforeUnmount, onAfterUnmount, onError, onComplete, onCompleted, onFinish, onFinished, onSuccess, onAbort, onOpen, onClose, onDismiss, onCancel, onConfirm These names are widespread across React, tldraw, Excalidraw, query libraries (TanStack Query, SWR), modal/dialog primitives (Radix, Headless UI), etc. Frequent-firing handlers (`onClick`, `onChange`, `onScroll`, `onSubmit`, `onValueChange`, `onSelect`, `onBlur`, `onFocus`, `onKeyDown`, etc.) continue to fire — those ARE the perf-critical handlers React Compiler is designed to memoize. Tests: 6,000 pass + 27 documented divergences (no new divergences). Eval on 100 repos: | Round | +diags vs main | reduction | |----------------------|---------------:|----------:| | 1 (pre-fixes) | 162,277 | — | | 2 (first fixes) | 34,306 | 79% | | 3 (deep validation) | 29,776 | 82% | | 4 (perf cleanup) | 26,354 | 84% | | 5 (tail stylistic) | 26,301 | 84% | | 6 (style/code/chain) | 26,292 | 84% | | 7 (slot props / iter)| 23,742 | 85% | | 8 (slot suffixes) | 22,888 | 86% | | 9 (this commit) | 22,456 | 86% | `jsx-no-new-function-as-prop`: 9,436 → 9,004 (-432). Remaining +22,456 breaks down as: - ~14,806 from 4 RC-gated perf rules (zero on RC projects); - ~3,364 `no-multi-comp` (user-kept warning); - ~1,593 `only-export-components` (genuine FR breakers); - ~2,693 genuine bug-catchers. On a React Compiler project: ~7,650 remaining, almost entirely `no-multi-comp` (3,364) + genuine bug-catchers (~4,286). Zero behavioral regressions across 100 repos. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(oxlint-plugin): perf-rule config/data-name skip for object & array variants Tenth FP pass. Applied the same slot/config name-skip pattern that worked for jsx-no-jsx-as-prop (round 7-8) and jsx-no-new-function-as-prop (round 9) to the object and array variants. Sampled the remaining 2,201 jsx-no-new-object-as-prop and 2,810 jsx-no-new-array-as-prop hits and found two consistent patterns: OBJECT props that receive inline literals by design: - Generic config: `options`, `config`, `settings`, `params`, `data`, `metadata`, `value`, `values` - Component slots: `components`, `customComponents`, `slots`, `elements` - Style configs: `classNames`, `theme`, `styles`, `sx`, `css` - Layout configs (charts/canvas): `margin`, `padding`, `viewport`, `viewBox`, `bounds`, `extent`, `domain`, `range` - Animation configs (framer-motion, react-spring): `animate`, `initial`, `exit`, `transition`, `variants`, `whileHover`, `whileTap`, `whileFocus`, `whileInView`, `drag`, `dragConstraints` - tldraw / Excalidraw specifics: `UIOptions`, `renderConfig`, `shape`, `shapes`, `user`, `users` - Suffixes: `*Props` (Radix/MUI/shadcn pass-through), `*Config`, `*Configuration`, `*Options`, `*Settings`, `*Style`, `*Styles`, `*ClassName`, `*ClassNames`, `*Theme` ARRAY props that receive inline data by design: - Generic data: `data`, `items`, `options`, `entries`, `list`, `dataset`, `elements`, `values` - Domain collections: `tabs`, `columns`, `rows`, `pages`, `categories`, `tags`, `keywords`, `files`, `blocks`, `entities`, `shapes`, `events`, `messages`, `users`, `series`, `datasets`, `children`, `subRows`, `nodes`, `edges` - Chart specifics: `size`, `ticks`, `yAxis`, `xAxis` - Picker / menu / palette: `actions`, `commands`, `customCommandPaletteItems`, `renderingShapes`, `calendarEvents` - Suffixes: `*Items`, `*Options`, `*Tabs`, `*Columns`, `*Rows`, `*List`, `*Series`, `*Categories`, `*Events`, `*Entries`, `*Elements`, `*Shapes`, `*Children`, `*Nodes`, `*Edges`, `*Data`, `*Collection`, `*Models`, `*Records` These are config/data slots — chart libs, design systems, animation libs, list/table/menu components all use this pattern. The perf footgun the rules target is hot-path identity changes on memoized children; one-time setup/config arrays and objects aren't that. Updated `jsx-no-new-array-as-prop.regressions.test.ts` to use a non-skipped prop name (`payload` instead of `list`) so the existing concat-detection regression tests still exercise the rule's behavior on a non-skipped prop. Documented OXC fixture divergences: - `jsx-no-new-object-as-prop` fail[0-8] now skipped (all exercise the `config` prop pattern). - `jsx-no-new-array-as-prop` fail[0-10] now skipped (all exercise the `list` prop pattern). Tests: 5,981 pass + 46 documented divergences. Eval on 100 repos: | Round | +diags vs main | reduction | |----------------------|---------------:|----------:| | 1 (pre-fixes) | 162,277 | — | | 2 (first fixes) | 34,306 | 79% | | 3 (deep validation) | 29,776 | 82% | | 4 (perf cleanup) | 26,354 | 84% | | 5 (tail stylistic) | 26,301 | 84% | | 6 (style/code/chain) | 26,292 | 84% | | 7 (slot props / iter)| 23,742 | 85% | | 8 (slot suffixes) | 22,888 | 86% | | 9 (lifecycle skip) | 22,456 | 86% | | 10 (this commit) | 19,944 | 88% | Round 10 reductions: - `jsx-no-new-object-as-prop`: 2,201 → 1,370 (-831) - `jsx-no-new-array-as-prop`: 2,810 → 1,129 (-1,681) Remaining +19,944 breaks down as: - ~12,294 from 4 RC-gated perf rules (zero on RC projects); - ~3,364 `no-multi-comp` (user-kept warning); - ~1,593 `only-export-components` (genuine FR breakers); - ~2,693 genuine bug-catchers (all sampled hits = TPs). On a React Compiler project: ~7,650 remaining (no-multi-comp + bug-catchers). Zero behavioral regressions: no rule on main lost any findings. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(oxlint-plugin): render-prop function skip + entry-point file skip Eleventh FP pass. Two improvements: 1. `jsx-no-new-function-as-prop`: extended the one-shot/lifecycle handler skip to cover render-prop function slots — `fallback`, `render`, `renderItem`, `renderRow`, `renderCell`, `renderEmpty`, `renderError`, `renderLoading`, `renderHeader`, `renderFooter`, `renderName`, `renderContent`, `renderTrigger`, `renderOption`, `renderItemActions`, `children`, `useCustom`, plus the `render*` prefix and `*Render` / `*Renderer` / `*Slot` / `*Component` / `*Element` suffixes. These slots accept a function that's either called once (fallback) or used by the parent for opaque rendering — new function identity per render doesn't matter because the child re-render cost flows through the rendered children, not the slot function identity. 2. `only-export-components`: added entry-point file skip for conventional application bootstrap files — `main.tsx`, `main.jsx`, `index.tsx`, `index.jsx`, `entry.tsx`, `entry.jsx`, `bootstrap.tsx`, `bootstrap.jsx`, `client.tsx`, `client.jsx`, `server.tsx`, `server.jsx`. These call `createRoot(...).render(...)` / `hydrateRoot(...)` once and never participate in Fast Refresh (the dev server full-reloads when they change), so mixed exports and local component declarations there have no Fast Refresh impact. Matches `react-scan/kitchen-sink/src/index.tsx` and `millionco/expect/packages/browser/src/runtime/overlay/index.tsx` in the corpus. Tests: 5,981 pass + 46 documented divergences (unchanged). Eval on 100 repos: | Round | +diags vs main | reduction | |----------------------|---------------:|----------:| | 1 (pre-fixes) | 162,277 | — | | 2 (first fixes) | 34,306 | 79% | | 3 (deep validation) | 29,776 | 82% | | 4 (perf cleanup) | 26,354 | 84% | | 5 (tail stylistic) | 26,301 | 84% | | 6 (style/code/chain) | 26,292 | 84% | | 7 (slot props / iter)| 23,742 | 85% | | 8 (slot suffixes) | 22,888 | 86% | | 9 (lifecycle skip) | 22,456 | 86% | | 10 (config/data names)| 19,944 | 88% | | 11 (this commit) | 19,532 | 88% | Round 11 reductions: - `jsx-no-new-function-as-prop`: 9,004 → 8,697 (-307, render-prop slots) - `only-export-components`: 1,593 → 1,488 (-105, entry-point files) Remaining +19,532 breaks down as: - ~11,987 from 4 RC-gated perf rules (zero on RC projects); - ~3,364 `no-multi-comp` (user-kept warning); - ~1,488 `only-export-components` (genuine FR breakers); - ~2,693 genuine bug-catchers (all sampled hits = TPs). On a React Compiler project: ~7,545 remaining (no-multi-comp + bug-catchers). Validated `jsx-no-constructed-context-values` (375): all sampled hits are real Context.Provider re-render storms. Genuine bug-catcher. No more action needed. Zero behavioral regressions across 100 repos. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(oxlint-plugin): more perf-rule slot/config names from corpus sampling Twelfth pass. Extended the slot/config/data name skip lists with the remaining patterns surfaced by sampling 20 hits per perf rule: jsx-no-jsx-as-prop additions: - Directional / positional slots: `left`, `right`, `top`, `bottom`, `start`, `end`, `aside`, `details`, `extra` (flexbox-aware design systems use these to control layout) - Common UI slots: `overlay`, `emptyState`, `element` jsx-no-new-object-as-prop additions: - Common slot/feature props: `args` (Storybook), `avatar`, `dot` (recharts), `action`, `expandable` (table), `defaultSort`, `resourceType`, `truncateText`, `formatters`, `label` - Suffixes: `*Sort`, `*Filter`, `*Pagination`, `*Format`, `*Locale`, `*Validator`, `*Args`, `*Type` jsx-no-new-array-as-prop additions: - Chart / visualization series: `bars`, `trails`, `lines`, `areas`, `marks`, `points`, `labels` - Filter / selection collections: `filters`, `selectedValues`, `resources`, `propertyFilters`, `dayTimes` - Suffixes: `*Filters`, `*Values`, `*Times`, `*Resources` Tests: 5,981 pass + 46 documented divergences (unchanged). Eval on 100 repos: | Round | +diags vs main | reduction | |----------------------|---------------:|----------:| | 1 (pre-fixes) | 162,277 | — | | 11 (last) | 19,532 | 88% | | 12 (this commit) | 18,791 | 88% | Round 12 reductions: - `jsx-no-jsx-as-prop`: 791 → 406 (-385) - `jsx-no-new-object-as-prop`: 1,370 → 1,062 (-308) - `jsx-no-new-array-as-prop`: 1,129 → 1,081 (-48) Remaining +18,791 breaks down as: - ~11,246 from 4 RC-gated perf rules (zero on RC projects); - ~3,364 `no-multi-comp` (user-kept warning); - ~1,488 `only-export-components` (genuine FR breakers); - ~2,693 genuine bug-catchers. On RC project: ~7,545 remaining (no-multi-comp + bug-catchers). Zero behavioral regressions across 100 repos. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(oxlint-plugin): no-array-index-key skips positionally-stable iteration sources Thirteenth FP pass. Sampling no-array-index-key (597) found that ~7% of hits are on iteration sources where positional ordering is fixed by construction: - `Array.from({ length: N }).map((_, i) => <div key={i}/>)` — fixed length, can't reorder - `Array(N).fill(0).map((_, i) => ...)` — same - `str.split('\n').map((line, i) => <div key={i}/>)` — text position by line; reordering doesn't happen For these the `index` key IS correct — the array's position-to-value mapping is fixed by the source string or length, so React's reconciler can match elements positionally without state-bleeding bugs. Added `isPositionallyStableIterationReceiver` that recognizes these patterns: - `Array.from({ length: ... })` (with optional second arg) - `Array(N)` / `new Array(N)` - `<receiver>.split(...)` (chained anywhere) - Chained `.fill(...)` / `.flat(...)` recurse through to the underlying receiver `findIndexParameterBinding` now returns null when the iteration source is positionally stable, which short-circuits the rule for that specific JSX site. Tests: 5,981 pass + 46 documented divergences (unchanged). Eval on 100 repos: | Round | +diags vs main | reduction | |----------------------|---------------:|----------:| | 1 (pre-fixes) | 162,277 | — | | 12 (last) | 18,791 | 88% | | 13 (this commit) | 18,750 | 88% | Round 13 reduction: - `no-array-index-key`: 597 → 556 (-41) Zero behavioral regressions: no rule on main lost any findings. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(oxlint-plugin): fix formatting in jsx-no-new-array-as-prop CI's `pnpm format:check` was failing — I had wrapped the `isNodeOfType && isDataArrayPropName` condition across multiple lines in round 10's data-array-name-skip addition, but prettier prefers the single-line form. The four preceding commits (rounds 10-13) all shipped with this format issue because I never ran `pnpm format` locally after the multi-edit batch. Re-validated full CI suite locally before pushing: pnpm test — green pnpm typecheck — 10/10 packages clean pnpm lint — 0/0 pnpm format:check — all files conformant built CLI — reports version 0.2.1 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(oxlint-plugin): RECOMMENDED_RULES / NEXTJS_RULES / etc. honor `defaultEnabled: false` Addresses Bugbot finding fa3d54f2-f3fb-49ce-9387-ae0e68f48d80 on PR #273. `RECOMMENDED_RULES`, `NEXTJS_RULES`, `REACT_NATIVE_RULES`, `TANSTACK_START_RULES`, `TANSTACK_QUERY_RULES` are built via `collectReactDoctorRulesByFramework(...)` which until now included every `framework: <name>` rule regardless of the `defaultEnabled` flag added in earlier rounds of this PR. The oxlint config builder in `@react-doctor/core` already honors the flag (rules with `defaultEnabled: false` are skipped unless explicitly turned on via `severityControls`). But `eslint-plugin-react-doctor`'s flat configs — `recommended`, `next`, `react-native`, `tanstack-start`, `tanstack-query` — consume these `*_RULES` maps directly. ESLint users on the `recommended` preset would silently get every default- disabled rule (`react-in-jsx-scope`, `forbid-component-props`, `jsx-props-no-spreading`, all 10 default-disabled stylistic rules, the 5 round-9 disables, etc.) — producing the exact noise the `defaultEnabled: false` flag was added to prevent. Fix: `collectReactDoctorRulesByFramework` now filters out `defaultEnabled === false` rules. The `eslint-plugin-react-doctor` `recommended.docs.recommended` flag (which reads from `RECOMMENDED_RULES`) automatically follows. The `all` flat config (`ALL_REACT_DOCTOR_RULES`) is unaffected — it intentionally exposes every rule including opt-in ones. Regression test added in scan-resilience.test.ts verifying every known default-disabled rule key is absent from `RECOMMENDED_RULES`: - react-in-jsx-scope, forbid-component-props, jsx-props-no-spreading - no-unescaped-entities, jsx-boolean-value, jsx-curly-brace-presence, self-closing-comp, jsx-no-useless-fragment, display-name, no-set-state, no-clone-element, hook-use-state, jsx-handler-names - prefer-function-component, jsx-fragments, state-in-constructor, jsx-filename-extension, no-react-children Full CI suite re-validated locally: pnpm test — green pnpm typecheck — 10/10 packages clean pnpm lint — 0/0 pnpm format:check — all conformant pnpm build — CLI reports version 0.2.1 Co-authored-by: Cursor <cursoragent@cursor.com> * autoresearch: drive down react-doctor false positives across 32k diagnostics in 26 OSS repos (#301) * fix(core/is-test-file): detect test helpers under source-root-named sub-folders `stripAboveSourceRoot` unconditionally stripped to the deepest source root marker, so `tests/app/setup.ts` collapsed to `/app/setup.ts` and no longer matched the test directory pattern. Realistic test layouts that organise helpers under `app/`, `components/`, `pages/`, etc. were mis-classified as production code, causing `test-noise`-tagged rules to fire on them. Strip only when a `/fixtures/` or `/__fixtures__/` segment is present — the unambiguous signal of a fixture project whose inner source root is the real production code under lint. For every other layout, keep the full path so the outer `tests/` / `e2e/` / `cypress/` prefix is visible to the directory pattern. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(oxlint-plugin): cut ~1200 FPs + recover ~1300 TPs from corpus audit 100-repo audit (react-doctor-evals) showed PR #273 was net-regressing on bug-finding power vs main: ~1750 real bug catches lost, ~2400 new FPs introduced. Five rule-level tightenings, none behavioural: rendering-svg-precision: trimmed AUTO_GENERATED_PATH_SEGMENTS to the explicit codegen markers (`/__generated__/`, `/generated/`, `/codegen/`, `/figma-export/`, `/sketch-export/`). The earlier broader heuristics (`/ico…


Summary
Brings the 8 rules from
eslint-plugin-react-you-might-not-need-an-effectnatively intooxlint-plugin-react-doctor, using a realeslint-scopeanalyzer (not an approximation), and retires the runtime peer-dep surface that loaded the upstream plugin aseffect/*rules.Upstream snapshot:
4c71faaa7623d2d5feb33983dc2ebcc08206bcc5(HEAD ofmain, v0.10.1, NickvanDyke, MIT-licensed). Full attribution and the known-divergence list live inpackages/oxlint-plugin-react-doctor/src/plugin/rules/state-and-effects/effect/SOURCE.md.Parity status
147 / 148 upstream test cases pass — every
valid:andinvalid:case fromtest/rules/<rule>.test.jsruns through our oxlint plugin and matches upstream's expected error count. The 1 remaining case is upstream's owntodo: true(renamed-useStateimport — upstream documents this as unsupported).todo: true)no-derived-stateno-chain-state-updatesno-event-handlerno-adjust-state-on-prop-changeno-reset-all-state-on-prop-changeno-pass-live-state-to-parentno-pass-data-to-parentno-initialize-statePorted rule IDs
All eight register under the
react-doctor/*namespace (matching upstream rule IDs):react-doctor/no-derived-statesrc/rules/no-derived-state.jsreact-doctor/no-chain-state-updatessrc/rules/no-chain-state-updates.jsreact-doctor/no-event-handlersrc/rules/no-event-handler.jsreact-doctor/no-adjust-state-on-prop-changesrc/rules/no-adjust-state-on-prop-change.jsreact-doctor/no-reset-all-state-on-prop-changesrc/rules/no-reset-all-state-on-prop-change.jsreact-doctor/no-pass-live-state-to-parentsrc/rules/no-pass-live-state-to-parent.jsreact-doctor/no-pass-data-to-parentsrc/rules/no-pass-data-to-parent.jsreact-doctor/no-initialize-statesrc/rules/no-initialize-state.jsDiagnostic messages match upstream verbatim with
{{state}}/{{arguments}}/{{prop}}template variables substituted in JS.These coexist with our existing thematically-related rules (
no-derived-state-effect,no-effect-chain,no-event-trigger-state,no-prop-callback-in-effect); the IDs are distinct and the rules target different code shapes.Architecture: real eslint-scope analyzer
The first iteration of this PR built a hand-rolled AST-only
analyze-component-bindings.tstable that approximated scope analysis. Per feedback, that approximation has been replaced with a realeslint-scopeScopeManager so the port faithfully mirrors upstream'scontext.sourceCode.getScope/ref.resolved.defs[].node.initsemantics. The hand-rolled analyzer is deleted.New infrastructure under
state-and-effects/utils/effect/:get-program-analysis.ts— LazyWeakMap<Program, ScopeManager>cache keyed on the live Program node. Walks the AST once, strips parent pointers (eslint-scope chokes on cycles), runsanalyze(), restores the parents synchronously, returns the cached manager. All 8 rules share one analysis per file. Also exposesgetScopeForNode(node)andgetOuterScopeContaining(node)helpers that pick the innermost (module / function / block) scope by smallest containing range, with<=tiebreak so the module scope wins over the equally-sized global scope.ast.ts— 1:1 port of upstreamsrc/util/ast.js:ascend/descend/getDownstreamRefs/getUpstreamRefs/getRef/getCallExpr/getArgsUpstreamRefs/isSynchronous/isEventualCallTo. Useseslint-visitor-keys.KEYSas the static visitorKeys table (replacing upstream'scontext.sourceCode.visitorKeys).react.ts— 1:1 port of upstreamsrc/util/react.js:isReactFunctionalComponent/isReactFunctionalHOC/isCustomHook/isUseEffect/isState/isStateSetter/isProp/isConstant/isRef/isRefCurrent/isStateSetterCall/isPropCall/isRefCall/getEffectFn/getEffectFnRefs/getEffectDepsRefs/getUseStateDecl/hasCleanup/findContainingNode. The separately-wrapped HOC detection (const Wrapped = withRouter(MyComponent),inject('x')(observer(MyComponent))) goes through the scope manager's Variable list to find every reference of the binding.stringify-expression-snippet.ts— Substitute for upstream'scontext.sourceCode.getText()used only inno-initialize-state's diagnostic data field (oxlint plugins don't expose source text).Each ported rule (e.g.
no-derived-state.ts) is a near-verbatim transliteration of the corresponding upstreamsrc/rules/<name>.js.Parity test infrastructure
/tmp/convert-tests.mjs(utility, not committed) loads each upstreamtest/rules/<name>.test.jswith a stubbedMyRuleTesterto capture every{name, code, todo, errors}case as JSON.packages/react-doctor/tests/regressions/state-rules/effect-fixtures/ships the 8 captured JSON fixture files — committed so the parity baseline is reproducible without re-running the converter._effect-parity-runner.tsdrives the fixtures: each case becomes oneit(...)that wraps the upstream snippet insrc/Component.tsx, runs oxlint, assertshits.lengthmatches upstream's expected error count, and skips ontodo: true.parity-<rule>.test.ts(8 files) invoke the runner per rule.What's removed
YOU_MIGHT_NOT_NEED_EFFECT_RULESmap inpackages/core/src/runners/oxlint/external-plugin-rules.ts.resolveYouMightNotNeedEffectPlugin/YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACEinplugin-resolution.ts.createOxlintConfig.eslint-plugin-react-you-might-not-need-an-effectoptional peer + devDep entries from@react-doctor/coreandreact-doctorpackage.json.scan-resilience.test.tsassertions about loading the external plugin → replaced with a single assertion that the 8 ported rules are enabled asreact-doctor/*in the generated oxlint config.Docs / changeset
packages/oxlint-plugin-react-doctor/README.md: new "You Might Not Need an Effect" section listing the 8 ported rule IDs with one-line descriptions; bumped rule count179 → 187.packages/react-doctor/README.md: droppedeslint-plugin-react-you-might-not-need-an-effectfrom the "Optional companion plugins" table; added a paragraph naming the 8react-doctor/*IDs that now subsume it..changeset/port-effect-rules.md: patch-level changeset for@react-doctor/core,oxlint-plugin-react-doctor, andreact-doctor.Verification
End-to-end smoke test against
packages/websiteVerbatim message text matches upstream. The diagnostics fire as
react-doctor/no-initialize-state(noteffect/no-initialize-state), confirming the namespace migration is live end-to-end.New deps
eslint-scope(BSD-2-Clause, transitively present before; now a direct dep ofoxlint-plugin-react-doctor)eslint-visitor-keys(Apache-2.0, ditto)@types/eslint-scope/@types/eslint-visitor-keys(devDeps)Notes
mainby mistake. Those two commits were reverted onmain(d7efa43,3bb230f) and the work is now isolated on thiscursor/branch.