Commit Graph

43 Commits

Author SHA1 Message Date
Aiden Bai 6416370836 feat(rules): add UI-library composition and correctness rules behind capability gates (#1654) 2026-08-16 23:29:04 -07:00
Aiden Bai 8c2f03aea9 feat: make React cleanup first-class (#1624)
* feat: make React cleanup first-class

* refactor: remove editor integrations

* fix: harden React cleanup analysis

* fix: detect default export duplication roots

* fix: unwrap typed duplication roots

* feat: add opt-in project analysis rules

* fix: canonicalize project analysis paths

* fix: harden project analysis precision

* fix: recognize cross-platform project entries

* fix: eliminate project analysis false positives

* fix: harden project analysis reachability

* fix: canonicalize project analysis inputs

* fix: resolve project analysis review findings

* fix: eliminate residual project analysis false positives

* fix: ignore commented registry previews

* fix: eliminate project analysis false positives

* fix: normalize project analysis paths across platforms

* fix: normalize Nextra theme path identity

* test: canonicalize convention fixture paths

* fix: preserve project analysis provenance

* fix: harden project analysis precision

* fix: honor project analysis boundaries

* fix: recognize conditional config plugins

* fix: recognize executable project references

* fix: recognize Stencil tool contracts

* fix: recognize nested tool references

* fix: recognize project setup contracts

* fix: recognize generated and local package consumers

* fix: recognize static template package references

* fix: recognize nested package runtime contracts

* fix: close project analysis parser gaps

* fix: parse project conventions structurally

* refactor: replace structural scanners with parsers

* fix: recognize functional Next CSS config

* fix: close remaining project analysis gaps

* fix: apply tag filters to project analysis

* fix: preserve embedded source positions

* fix: validate static config helper bindings

* fix: bound runtime directory discovery

* fix: close final dependency analysis gaps

* fix: preserve declaration dependency references

* chore: refresh generated rule metadata

* fix: make project analysis portable and bounded

* test: stabilize cleanup scaling guard

* refactor: parse project syntax with oxc

* fix: normalize native filesystem paths

* fix: separate path identity from report paths

* fix: match project files by filesystem identity

* fix: match build glob files by package identity

* fix: use native path keys for file identity

* fix: canonicalize Windows file identities

* fix: canonicalize package ownership paths

* test: inspect Windows path identities

* test: trace Windows package ownership

* fix: keep Windows path identities consistent

* fix: classify test contracts by normalized path

* fix: scope test contracts by canonical package path

* fix: keep test package graphs conservative

* test: keep React complexity advisory
2026-08-13 16:26:08 -07:00
Aiden Bai 7b7bfe7c1e fix: faithfully port upstream React rules (#1642) 2026-08-12 18:41:05 -07:00
Aiden Bai b49f499840 fix(rules): port no-multi-comp faithfully (#1641)
* fix(rules): port no-multi-comp faithfully

* refactor(rules): preserve component rule ordering
2026-08-12 16:51:20 -07:00
Aiden Bai 8dfb013067 Add comprehensive Three.js and R3F diagnostics (#1629) 2026-08-11 15:40:53 -07:00
Aiden Bai 13138a4af5 refactor: simplify internals across the workspace (#1590) 2026-08-08 00:20:14 -07:00
Aiden Bai 44922d6782 FPFNFIX82 Fix form and fragment diagnostics (#1561)
* fix(rules): correct form and fragment diagnostics

* fix(rules): handle fragment review edge cases

* fix: handle document-top fragment links

* fix(a11y): parse ids from html tags only

* fix(a11y): resolve static fragment aliases

* fix(a11y): retain known fragment targets

* fix(a11y): tokenize html id attributes

* fix: preserve known fragment IDs in mixed spreads

* fix: preserve known fragment ID defaults
2026-08-03 03:03:00 -07:00
Aiden Bai 2db2a97283 perf: reduce scan startup and workspace contention (#1533) 2026-07-30 19:42:52 -07:00
Aiden Bai 846c2df844 feat: add parsed Three.js shader lint rules (#1455)
* feat: add parsed Three.js shader lint rules

* fix: tighten shader rule precision

* fix: address shader rule review findings

* fix: harden parsed shader edge cases

* fix: harden shader variant analysis

* fix: complete parsed shader edge coverage

* fix: close shader control-flow gaps

* fix: tighten shader interface analysis

* fix: recognize GLSL3 integer samplers

* fix: close shader control-flow gaps

* fix: close remaining shader rule gaps

* fix: close shader review gaps

* fix: track shader cache key invalidation

* fix: handle shader material review edge cases

* fix: close remaining shader review gaps

* fix: skip uncertain shader position writes

* fix: harden shader position analysis

* fix: track divergent shader loop controls

* fix: track parameter shader variants

* fix: complete shader variant detection

* fix: identify and clean up parity sandboxes

* fix: tolerate Daytona cleanup races

* fix: restore complete parity corpus

* fix: tighten Three shader diagnostics

* fix: download large eval reports

* fix: trace shader variant aliases
2026-07-25 22:04:43 -07:00
Aiden Bai db9d300343 Make all design rules opt-in by default (#1434) 2026-07-23 01:55:55 -07:00
Aiden Bai 1e67af03fe feat: add deterministic design diagnostics (#1428) 2026-07-23 00:27:13 -07:00
Aiden Bai 76263ecc08 Add direct Three.js and WebGL diagnostics (#1424)
* add direct Three.js and WebGL rules

* fix parity-discovered config cycle

* fix Three render construction false positives

* fix(rules): preserve JSX-referenced Three components

* fix(rules): address Three.js review findings

* feat(rules): add Three.js resource cleanup diagnostics
2026-07-22 01:48:13 -07:00
Aiden Bai f5f13b877a feat(rules): add version-aware React Router diagnostics (#1411) 2026-07-20 13:43:35 -07:00
Aiden Bai 5b468f8a92 feat(rules): add React Three Fiber diagnostics (#1371) 2026-07-20 09:22:48 -07:00
Aiden Bai fc1b3a3984 feat(rules): add version-gated Ink diagnostics (#1404) 2026-07-20 08:41:26 -07:00
Aiden Bai 0b0b5ac523 feat(rules): add deterministic UI, accessibility, and animation diagnostics (#1337)
* feat(rules): add 10 motion, accessibility, and design lint rules

Derived from a cross-resource design reference (every principle from the
design knowledge base), deduped against the full existing rule inventory.

Motion: extend no-transition-all to the Tailwind `transition-all` class
(was inline-only) and add no-tailwind-layout-transition for arbitrary
`transition-[<layout-prop>]`.

Accessibility: no-autoplay-without-muted, no-uninformative-aria-label,
no-target-blank-without-rel, and no-low-contrast-inline-style (computes the
real WCAG 2.1 ratio from co-located inline color + backgroundColor).

Design/Tailwind hygiene: no-redundant-display-class, prefer-truncate-shorthand,
no-full-viewport-width, and no-svg-currentcolor-with-fill-class.

All register via codegen (388 rules); 77 co-located tests pass; typecheck,
lint, and format clean.

* fix(rules): correct two false positives found in rule-validate

- no-low-contrast-inline-style: when the inline style has no `fontSize`,
  the text may be sized large via a class (`text-5xl`), so fall back to the
  lenient 3:1 large-text threshold instead of 4.5:1 to avoid flagging large
  text that only needs 3:1. Stricter 4.5:1 applies only when the size is a
  visible normal size.
- no-redundant-display-class: `<li>` defaults to `display: list-item`, not
  `block`, so `block` on an `<li>` is meaningful — removed `li` from the
  block-default set.

Adds regression tests for both.

* fix(rules): resolve Cursor Bugbot findings

- no-low-contrast-inline-style: use a solid `background` shorthand color as
  the background (was skipped entirely, missing low-contrast pairs); skip only
  gradients/images/ambiguous (both backgroundColor + background). Also treat a
  string `fontWeight: "700"` as bold for the large-text threshold.
- no-transition-all: match `transition-all` as a whole Tailwind token (segment
  after variant prefixes) so compound classes like `transition-all-custom` are
  not falsely flagged.
- no-svg-currentcolor-with-fill-class: exclude stroke-WIDTH utilities
  (`stroke-2`, `stroke-[1.5]`) — they set thickness, not color — so they no
  longer false-conflict with `stroke="currentColor"`.

Adds regression tests for each.

* feat(rules): add 3 Tailwind-canonicalization rules from ui.sh guidance

- no-deprecated-tailwind-class: flag Tailwind v4 renames (bg-gradient-* →
  bg-linear-*, flex-shrink-* → shrink-*, flex-grow-* → grow-*,
  overflow-ellipsis → text-ellipsis). Gated on a new `tailwind:4` capability.
- no-arbitrary-px-font-size: `text-[13px]` → rem so text scales with the
  user's root font size (px stays fine for border-*/outline-*).
- prefer-dvh-over-vh: `h-screen`/`min-h-screen`/`h-[100vh]` → dvh (tailwind:3.4).

Adds `tailwind:4` capability to @react-doctor/core and extracts a shared
`get-class-name-tokens` util (variant-aware token splitting), reused by
no-transition-all and no-svg-currentcolor-with-fill-class.

* fix(rules): adversarial-review false positives in the new design rules

- no-tailwind-layout-transition: match transition-[…] property names exactly
  (comma-split + set) so SVG `stroke-width` / `border-width` — which merely
  contain "width" — are no longer flagged as HTML layout thrash.
- no-full-viewport-width: drop `max-w-*` / `maxWidth` (a defensive cap, not the
  overflow footgun); keep `w-screen`/`w-[100vw]`/`width`/`minWidth`.
- prefer-dvh-over-vh: drop `max-h-*` / `maxHeight` (a valid height cap, e.g. a
  scrollable modal); keep `h-screen`/`min-h-screen`/`height`/`minHeight`.
- no-deprecated-tailwind-class: only rename `bg-gradient-to-*` → `bg-linear-to-*`
  (v4 radial/conic are `bg-radial`/`bg-conic`, not `bg-linear-radial`).

Adds regression tests for each false positive.

* fix(rules): resolve thermo-review false positives

- no-svg-currentcolor-with-fill-class: only treat UNPREFIXED `fill-*`/`stroke-*`
  color classes as conflicts; `hover:fill-blue-600` / `dark:fill-white` (state-
  gated, no static conflict with the base `fill="currentColor"`) are no longer
  flagged — a very common icon pattern.
- no-low-contrast-inline-style: reject 4-arg `rgb(0,0,0,0.5)` / slash-form alpha
  (was judged opaque), and bail when the style object contains a `{...spread}`
  that could override the colors. Tag `test-noise` to match sibling no-tiny-text.
- no-full-viewport-width: drop the redundant `category: "Architecture"` override
  (it's the design-bucket default).
- Extract `has-jsx-spread-attribute` util; use it in the two a11y rules.

Adds regression tests for the variant-prefixed svg classes, rgb-alpha, opaque
rgb, and the style-spread bail.

* refactor(rules): deslop the design/motion/a11y rule additions

Consolidate duplication surfaced by an adversarial deslop pass; no behavior
change (the same diagnostics fire for the same inputs):

- no-autoplay-without-muted: fold isStaticallyTrue/isStaticallyFalse into one
  tri-state resolveStaticBoolean, mirroring the existing parseJsxValue idiom
- no-svg-currentcolor-with-fill-class: collapse the duplicated fill/stroke
  branches into a single loop over the two paint attributes
- no-low-contrast-inline-style: hoist the repeated `properties` fallback
- no-uncontrolled-input: drop the local hasJsxSpreadAttribute copy in favor of
  the branch's new shared utils/has-jsx-spread-attribute, completing the extraction

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(rules): no-deprecated-tailwind-class stops flagging overflow-clip

`overflow-clip` is a current Tailwind utility for `overflow: clip` (v3.1+,
still present in v4) — it was never renamed to `text-clip`, which sets the
unrelated `text-overflow: clip`. The stray mapping produced an incorrect
"renamed in v4" suggestion (Cursor Bugbot). Only `overflow-ellipsis ->
text-ellipsis` is a real rename; keep that. Adds a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(capabilities): require a confirmed Tailwind major for the tailwind:4 gate

`tailwind:4` gates no-deprecated-tailwind-class, which tells users a class was
renamed/removed in v4. The optimistic-on-null policy (assume latest for an
unparseable spec like `workspace:*`) is right for the suggestion rule behind
`tailwind:3.4`, but for a deprecation rule it surfaces confidently-wrong
"renamed in v4" warnings on a v3 project. Require a CONFIRMED parsed major >= 4
for tailwind:4 — favor a false negative over a false positive (Cursor Bugbot).
`tailwind:3.4` stays optimistic. Adds an unparseable-version regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(rules): resolve Bugbot/Devin review findings on design/a11y rules

- no-full-viewport-width: match variant-prefixed `md:w-screen` / `lg:min-w-screen`, mirroring the sibling `prefer-dvh-over-vh` regex (overflow happens at every breakpoint).
- no-target-blank-without-rel: resolve `rel={'noopener'}` expression-container literals instead of treating them as dynamic, so reverse-tabnabbing isn't silently missed; share one literal resolver with the `target` check.
- Extract the bare `16` root-font-size divisor into `ROOT_FONT_SIZE_PX` per the magic-number convention.

* fix(rules): no-uninformative-aria-label resolves braced string literals

Extracts no-target-blank-without-rel's inline getStringLiteralAttributeValue
into a shared util and uses it in no-uninformative-aria-label, so a static
braced literal like aria-label={'icon'} is judged instead of silently skipped
(getJsxPropStringValue only handles plain string literals). Adds regression
tests for the braced uninformative and braced descriptive cases.

Addresses Bugbot review on #850.

* test(rules): add liveness fixtures after rebase

* fix(rules): address automated review findings

* fix(rules): handle leading-dot stroke widths

* fix(rules): skip dynamic background overrides

* fix(rules): address target blank and transition reviews

* fix(rules): skip svg layout transitions

* fix(rules): preserve font line height suggestion

* fix(rules): match complete transition utilities

* fix(rules): share layout transition properties

* fix(rules): bound deprecated utility prefixes

* fix(rules): clear dynamically overridden colors

* fix(rules): tighten style and svg precision

* fix(rules): preserve competing display utilities

* fix(rules): preserve animation property scopes

* feat(rules): expand deterministic design checks

* feat(rules): add structural design diagnostics

* feat(rules): complete source-auditable design checks

* fix(rules): skip unresolved input types

* fix(rules): tighten layout and boundary detection

* fix(rules): close motion and padding gaps

* fix(rules): tighten surface and line-height proofs

* fix(rules): evaluate opaque hsl contrast

* fix(rules): honor effective static values

* fix(rules): honor final inline style declarations

* fix(rules): respect variants and share surface checks

* fix(rules): traverse fragments and preserve variant scope

* feat(rules): deepen UI quality diagnostics

* feat(rules): add deterministic animation diagnostics

* fix(rules): retain in-house accessibility checks

* feat(rules): expand deterministic UI diagnostics

* feat(rules): detect generated interface patterns

* fix(rules): respect dynamic style overrides

* feat(rules): expand deterministic design diagnostics

* fix(rules): close design precision gaps

* feat(rules): expand deterministic design and accessibility checks

* feat(rules): detect unsafe Motion API usage

* test(fuzz): remove duplicate module snippets

* feat(cli): add focused design command

* fix(design): include the complete rule bucket

* fix(core): align opt-in rule gates

* feat(rules): expand deterministic motion and interaction checks

* feat(rules): harden deterministic design diagnostics

* fix(rules): harden design expansion diagnostics

* fix(rules): apply large-corpus precision review

* fix(rules): suppress large-corpus accessibility noise

* fix(core): detect compiler in rstack configs

* fix(a11y): avoid responsive navigation false positives

* fix(rules): reduce design corpus false positives

* fix(rules): restore will-change build after rebase

* fix(rules): harden all-rules corpus precision

---------

Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 01:09:01 -07:00
Aiden Bai cd9ca68faa NEW_RULES: Add rule analysis foundation and 56 rules (#1402)
* NEW_RULES: Reapply rule analysis foundation

Reapply the complete reviewed NEW_RULES stack on top of current main after the rollback.

* fix(rules): harden new diagnostics against false positives

* fix(rules): preserve dynamic context providers

* fix(core): resolve styled-components workspace versions

* fix(rules): harden new diagnostics after deep audit
2026-07-20 00:54:55 -07:00
Aiden Bai 61bf03eedc feat(rules): add MobX diagnostics foundation (#1405) 2026-07-19 21:30:10 -07:00
Aiden Bai a4eaeddefe feat(rules): detect Valtio proxy reads during render (#1396) 2026-07-19 06:44:56 -07:00
Aiden Bai 88114a2332 Revert "NEW_RULES: Add rule analysis foundation" (#1401)
Reverts #1338 in full.
2026-07-19 03:16:55 -07:00
Aiden Bai 3018538b7c NEW_RULES: Add rule analysis foundation (#1338)
Replace aggregate PR #1000 with the reviewed NEW_RULES stack, including workspace capability aggregation and stable performance regression coverage.
2026-07-19 02:56:31 -07:00
Ray Arayilakath b97a92f611 refactor(capabilities): unify framework gating into one capability vocabulary (#976) (#978)
Co-authored-by: Aiden Bai <aiden.bai05@gmail.com>
2026-07-08 14:23:28 -07:00
Ray Arayilakath 0053a0283e Revert "feat(rules): add 10 motion, accessibility, and design lint rules (#825)" (#849) 2026-06-16 20:26:21 -04:00
Aiden Bai 09dde181c7 feat(rules): add 10 motion, accessibility, and design lint rules (#825)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com>
2026-06-16 17:21:21 -04:00
Aiden Bai 451beeb284 feat(rules): add 3 mined platform/correctness rules (batch 2) (#828)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com>
2026-06-16 00:06:34 -04:00
Aiden Bai 8908f98d02 feat(rules): add 8 mined correctness, performance, a11y, and security rules (#826)
* feat(rules): add 8 mined correctness, performance, a11y, and security rules

Mined from a 12-cluster pass over the design/engineering brain and
synthesized into docs/rule-candidates-backlog.md. Ships the highest-signal,
lowest-noise batch, each with adversarial + FP-regression tests:

- no-call-component-as-function (Bugs)
- no-create-ref-in-function-component (Bugs)
- no-async-effect-callback (Bugs)
- no-json-parse-stringify-clone (Performance)
- img-missing-dimensions (Performance)
- no-img-lazy-with-high-fetchpriority (Performance)
- dialog-has-accessible-name (Accessibility)
- auth-token-in-web-storage (Security)

Adds the shared has-jsx-spread-attribute util and registers the three
in-house ported-bucket rules in RULES_NOT_PORTED_FROM_EXTERNAL.

* fix(rules): drop img-missing-dimensions; make component rules shadow-safe

- Drop `img-missing-dimensions`: width/height attributes are only one of
  several valid ways to reserve space (CSS aspect-ratio, container sizing),
  and the authoritative signal lives in stylesheets a static linter can't
  see. This is a runtime check (Lighthouse `unsized-images`), so a syntax
  rule would false-positive on any CSS-sized image.
- `no-call-component-as-function`: resolve the callee through its scope
  symbol instead of by name, so a parameter/local shadowing a component
  name is no longer flagged. The component signal is the binding's
  declaration (same-file component returning JSX) or an imported binding
  also rendered as JSX.
- `no-create-ref-in-function-component`: skip a shadowing local `createRef`
  (only flag React's import / `React.createRef`).
- Adds shadow-safety regression tests for both.

* fix(rules): tag no-call-component-as-function test-noise (eval finding)

An OSS eval sweep (react-use, radix primitives, excalidraw, mantine;
~2.8k diagnostics) showed every real-world hit of this rule was a test
render-helper (`render(Wrapper({...}))` in `.test.tsx`/`.story.tsx`) —
harmless for hookless wrappers and the dominant firing surface. Tag it
`test-noise` so it skips test/story/playground files and only nags
shipped code. The sweep also confirmed dialog-has-accessible-name (2 real
a11y bugs in excalidraw) and no-create-ref-in-function-component (1 real
bug in a mantine story) as true positives with zero false positives.
2026-06-15 05:13:44 -07:00
Aiden Bai eacdcf2e65 Add project-level security scan rules (#744)
Co-authored-by: Aiden Bai <aiden.bai05@gmail.com>
Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com>
2026-06-13 03:57:25 -07:00
Nisarg Patel 93d4eecdb8 feat: run React Doctor on TypeScript repos without React (#756)
* feat(core): gate React-specific rules behind a `react` capability

Add a `react` capability (set when React or Preact is detected) and use
it to gate every React-runtime rule family so they stay off on a project
without React. The codegen injects the requirement for the React-runtime
buckets (hooks, JSX, a11y, render performance, React state), and
`shouldEnableRule` treats the `react-jsx-only` tag as implying it. Two
bare-name migration rules that matched class-member names without a React
guard (`no-legacy-class-lifecycles`, `no-legacy-context-api`) now require
it explicitly. Without this, hook/component-name heuristics would
false-fire on ordinary TypeScript once non-React scanning is enabled.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* feat: scan TypeScript projects that don't depend on React

A project is now analyzable when it has source files, not only when React
or Preact is present, so a plain TypeScript/JavaScript codebase runs the
framework-agnostic rules instead of hard-failing with 'No React project
found'. `discoverProject` tolerates a directory with no package.json (a
monorepo's `packages/` subfolder, a loose tree of files) by synthesizing a
ProjectInfo, inheriting dependency/framework detection from the enclosing
workspace root so a React monorepo subdirectory still gets its React
capabilities. Only a directory with neither source files nor an enclosing
project still reports nothing-to-scan.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* refactor(core): clarify discoverProject's no-package.json branches

Split the synthesized ProjectInfo into its two real cases — a workspace
subfolder (spread the enclosing root's detection, scope the directory-
specific fields) and a standalone source tree (explicit all-null,
framework-agnostic) — replacing the noisy `inherited?.x ?? null` chain
repeated across every field. Behavior is unchanged.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>
2026-06-08 22:39:33 -07:00
Aiden Bai cbdff6203d feat(cli): "Add to CI" handoff option + default CI setup in install (#658)
Co-authored-by: Aiden Bai <aiden.bai05@gmail.com>
Co-authored-by: Nisarg Patel <awesomenisarg@gmail.com>
2026-06-04 04:15:05 -07:00
Aiden Bai 87440abac8 Fix React Native rule false positives (#662) 2026-06-03 01:41:06 -07:00
Aiden Bai b3c3aa9401 chore: codebase audit cleanup (dead code, dedup, CI/config fixes) (#619) 2026-05-31 19:36:40 -07:00
Aiden Bai 6e59f10ef8 feat: new onboarding (#596) 2026-05-31 02:24:23 -07:00
Ray Arayilakath b65b15794f feat(zod): add Zod v4 migration rules (#578) 2026-05-29 17:18:27 -07:00
Nisarg Patel 3a8ef93931 chore: triage-driven rule severity and scope adjustments (#536)
* fix(a11y): skip alt-text on Next.js metadata image route files

Next.js App Router treats `opengraph-image.tsx`, `twitter-image.tsx`,
`icon.tsx`, and `apple-icon.tsx` (with optional numeric suffix) as
metadata image routes. Their default export returns an `ImageResponse`
from `next/og` that rasterizes JSX into a static PNG/JPG — no browser
DOM, no screen reader, so `alt` / `aria-*` are unactionable noise.

Extract `isNextjsMetadataImageRouteFilename` as a shared util (so other
DOM-flavoured a11y rules can opt in later) and early-return from
`alt-text.create()` when the filename matches. Adds a regression suite
covering both extensions, numeric suffixes, and the negative case
(`my-opengraph-image.tsx` still flags).

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(architecture): demote only-export-components to warn

Mixing non-component exports in a component module is a Fast Refresh
hint, not a hard failure — projects routinely co-locate stable
constants alongside components and ship fine. Drop from `error` to
`warn` so triage output reflects the actual severity gradient.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(architecture): unregister react-compiler-destructure-method

The React-Compiler memoization premise didn't hold up for the three
canonical hooks the rule targeted: `useRouter`, `useSearchParams`, and
`useNavigation` all return stable references, so destructuring their
methods produces no measurable compiler win. On Pages Router
(`next/router`) destructuring `push` is actively wrong — it captures
a stale reference.

Introduce a `RULE_IDS_TO_SKIP_REGISTRATION` skiplist in the registry
generator (with a required justification comment per entry) so we can
park a rule without deleting its implementation. Drops the rule from
the registered set (297 → 296), `describe.skip`'s the regression
suite, and prunes the rule from the vercel-skill parity table. The
fixture seed is kept so re-enabling is a one-line skiplist edit.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(state-and-effects): promote no-adjust-state-on-prop-change to error

Adjusting derived state inside a useEffect when a prop changes always
forces an extra render with a stale UI between the two commits —
there is no benign instance of this pattern, so a warning understates
the cost. Promote to `severity: "error"` and rewrite the recommendation
and message in the authoritative "what's wrong → why → fix" shape used
by the rest of the error-level effect rules.

Upstream `eslint-plugin-react-you-might-not-need-an-effect` ships this
as a softened `type: "suggestion"`. Document the intentional divergence
(severity + copy only — detector logic is still a 1:1 port) in
`effect/SOURCE.md`, and update the registered-severity assertion in
`scan-resilience.test.ts` to expect `error` for this id.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: address review nits — drop explanatory comments, inline regex

Per @rayhanadev's review:
- drop the two `transient-and-async-issues.tsx` block comments around
  `useRouter` / `LoginLink` (the test files document the rule's intent
  on their own)
- drop the duplicate early-return rationale in `alt-text.ts` (the
  shared util's header doc already explains the why)
- inline the metadata-image basename regex into the helper to remove
  the named-constant indirection

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(a11y): use context.filename instead of removed getFilename

After merging main, `RuleContext` no longer exposes `getFilename` (PR
\#549 made it `Omit<BaseRuleContext, "getFilename">` and exposed the
absolute path as a property). Switch the alt-text early-return to read
`context.filename` to match the new API.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 17:16:51 -07:00
Aiden Bai 7fe3e41131 feat: add Preact framework support and rules (#519) 2026-05-28 02:39:01 -07:00
Aiden Bai 849b25dd91 feat(jotai): add three Jotai rules + register jotai bucket (#531) 2026-05-27 17:33:52 -07:00
Cursor Agent ae7752ba86 refactor(oxlint-plugin): collapse duplicate framework/category/severity from rule-registry
Every generated rule-registry entry stored framework, category, and
severity at BOTH the outer entry level AND inside the inner rule:
{...} object. Consumers read framework/category/severity from
whichever side they happened to import — rules.ts read the outer,
react-doctor-plugin.ts and core/runners/oxlint/config.ts read the
inner.

Stop emitting the outer copies. Keep them only on the inner Rule
object (where they're set by the codegen spread). The outer entry
now ships:

  { key, id, source, originallyExternal, rule: { ...sourceRule,
    framework, category, tags? } }

Consumers in rules.ts now read severity/framework via entry.rule.*
through a small toKeyedSeverity helper.

rule-registry.ts: 4324 -> 3469 lines (-855 / -20%).

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
2026-05-25 11:45:17 +00:00
Aiden Bai 47772b7da4 feat(oxlint-plugin): native TypeScript ports of OXC react/* + jsx-a11y/* + 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 (`'` → `&apos;`); 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
(`/icons/`, `/icon/`, `/svg/`, `/svgs/`, `/brand/`, `/logo/`, `/logos/`,
`/flags/`, `/emoji/`, `/emojis/`, plus `*Icon.tsx` / `*Logo.tsx` /
`sprite.tsx` basename matches) silenced 1364 hand-written-icon TPs
across the corpus (react-grab/icons/*, posthog/icons.tsx,
plane/empty-state/assets/vertical-stack/*). Those bytes are owned and
optimisable by the dev. Recovers +1363 net TPs.

jsx-no-new-{function,object,array}-as-prop: inverted the same-file memo
gate. Old behaviour: fire when consumer memo status is unknown (the
default for imported components — the vast majority). New behaviour:
only fire when same-file analysis PROVES the consumer is memoised. The
`useCallback`/extract-handler fix is unactionable when the consumer
isn't `React.memo`-wrapped: the parent re-renders unconditionally
regardless of function identity. Cuts ~95% of these rules' FPs across
the corpus (-843 / -77 / -80 on the parity diff).

no-multi-comp: added an exemption when total flagged components <= 2.
Covers the canonical "1 main + 1 sub-component" pattern (`ErrorBoundary`
+ `OptionalErrorBoundary`, `FPSMeter` + `FpsMeterInner`, `ArrowShapeUtil`
+ `ArrowClipPath`, `getSvgJsx`'s `SvgExport` + `ForeignObjectShape`).
Forcing a second file for a private helper fragments tightly-coupled UI
without a maintenance benefit. Cuts -182 FPs.

only-export-components: treat custom-hook exports (`use[A-Z]*`) as
allowed. Modern Vite Fast Refresh (>= 4.x) already handles `use*`
exports alongside components as refresh boundaries; flagging them is
unactionable noise. Cuts -49 FPs.

no-array-index-key: relaxed the composite-template-key skip — now
applies whenever the template has any non-index interpolation, even
when `findIteratorItemName` can't resolve the iterator binding through
nested helper closures. Also added a string-concat composite branch
(`outerVar + '-' + i` is composite; `'prefix-' + i` still flags). The
existing "iterator member identity" check stays as the primary path.

Audit + parity numbers in agent-transcript.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(core/rule-key-aliases): cover all 108 ported OXC rules

The legacy → native rule key alias map only covered ~26 entries —
12 `react/*` + 14 `jsx-a11y/*` — but the PR ports 100+ rules from
react / jsx-a11y / react-hooks / react-refresh / react-perf and the
you-might-not-need-an-effect plugin. Without an alias entry, user
configs targeting the old keys silently no-op after upgrade:

  config = {
    ignore: { rules: ["react-hooks/exhaustive-deps"] },
    rules: { "react/no-multi-comp": "off" },
  }

The `applySeverityControls` path and `isSameRuleKey` lookup both
key off this map; missing entries break severity overrides, ignore
filters, and inline-suppression matching.

Regenerated the map from the rule files: every `Port of \`oxc_linter::
rules::<plugin>::\`` comment yields one alias per ESLint-convention
namespace (`react`, `jsx-a11y`, `react-perf`, plus `react-hooks` /
`react-refresh` for the rules conventionally exposed under those
namespaces). The pre-existing `effect/*` aliases for the
you-might-not-need-an-effect family are preserved as a manual baseline.

107 aliases total, alphabetically sorted for stable diffs.

Reported by Cursor Bugbot on commit 2ff18a9b.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: remove dead exports and duplicate effect parity tests

Drop obsolete post-port rule maps, internalize unused exports flagged by
deslop-js, and consolidate effect rule coverage into JSON parity fixtures.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(core/run-oxlint): env override for per-batch oxlint budget

Hardcoded 60s `OXLINT_SPAWN_TIMEOUT_MS` starves every batch under Vercel
Sandbox microVMs in react-doctor-evals (the native binding is markedly
slower there than on a laptop). Allow the evals harness to bump it via
`REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS` without changing the local
default.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(core/run-oxlint): surface first batch failure reason in lint:partial

`onPartialFailure` always blamed the per-batch oxlint budget even when
files were dropped for an unrelated reason (output-too-large, OOM,
native-binding crash). In the eval pipeline this masked sandbox-specific
issues — every report attributed the missing lint diagnostics to a
timeout that never actually fired.

Now the partial-failure string is rephrased as a generic skip ("failed
to lint and were skipped") and appends the first concrete error
message (e.g. "oxlint was killed by SIGABRT") so consumers can tell
the budget case from a native-binding failure at a glance.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(deps): upgrade oxc to latest

- oxlint 1.63.0 → 1.66.0
- oxlint-tsgolint 0.22.1 → 0.23.0
- oxc-parser 0.131.0 → 0.132.0

Picks up the v1.65.0 Linux MUSL fix for the fixed-size allocator
(oxc-project/oxc#22388 — backing fix for the SIGABRT panic at
`crates/oxc_allocator/src/pool/fixed_size.rs:112` we hit in Vercel
Sandbox) and Windows `VirtualAlloc` fix (#22124), plus the new built-in
rules from v1.65/1.66 (no-noninteractive-element-to-interactive-role,
no-noninteractive-element-interactions, control-has-associated-label,
no-implicit-globals, no-implied-eval, id-match,
no-object-type-as-default-prop, no-unstable-nested-components,
import/newline-after-import, jsx-a11y-x support).

All 1404 tests pass.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(core/is-test-file): treat flat fixture projects as production

Bugbot caught a hole in `stripAboveSourceRoot`: when a path matches
`FIXTURE_PROJECT_PATTERN` but the inner project has no
source-root segment (`src/`, `app/`, `lib/`, …), the function returned
the full path. The outer `tests/` / `e2e/` prefix then still satisfied
`TEST_FILE_DIRECTORY_PATTERN`, so flat fixtures like
`tests/fixtures/my-app/Component.tsx` were misclassified as test files
and their `test-noise`-tagged diagnostics auto-suppressed.

Strip up through the fixture segment in that case so the path passed
to the directory heuristic starts at the fixture itself — fixture
contents are production-shaped code, the fixture project is the unit
under test. Existing `tests/fixtures/<proj>/src/...` behavior
preserved (still strips to the inner source root when present).

Added regression test cases for the flat layout.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
2026-05-21 21:25:04 -07:00
Aiden Bai dafe231e24 feat: add category-level rule controls (severity overrides + family tags) (#285)
* feat(core): add severityOverrides config with per-rule / per-category / per-tag channels

Introduces a unified severity-override resolver that lets users
promote, demote, or fully disable rules across every diagnostic
surface (CLI, PR comment, score, --fail-on) without editing source.

- New `severityOverrides` field on `ReactDoctorConfig` with `rules`,
  `categories`, and `tags` channels. Values: `error` | `warn` | `off`.
  Precedence: `rules` > `categories` > `tags`; within the tag channel
  the most permissive override wins (`off` > `warn` > `error`).
- `resolveRuleSeverityOverride` is a pure resolver shared between the
  oxlint config builder (next commit) and the post-lint diagnostic
  filter, so external-plugin rules (`react/*`, `jsx-a11y/*`) honor
  `off` too even though their severities live outside our control.
- `applySeverityOverrides` drops `off` diagnostics, re-stamps
  `warn` / `error` severities, and short-circuits to the input
  array when no overrides are configured so the common path stays
  allocation-free.
- `validateConfigTypes` coerces stringy mistakes and warns on
  invalid severities / channel shapes; `mergeAndFilterDiagnostics`
  runs the override before the existing ignore pipeline.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* feat(core): apply severityOverrides at oxlint config time

Threads `severityOverrides` from the user config through `runOxlint`
into `createOxlintConfig`, where it short-circuits `off` rules
before registration (so they never run, never emit, and never reach
any surface) and re-stamps the registered severity for `warn` /
`error` overrides. The retry-without-extends fallback honors the
same overrides so a crashed adopt-extends path doesn't quietly
reactivate a silenced rule family.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* feat(plugin): bucket-derived auto-tags and migration-hint family

Cross-cutting controls (`severityOverrides.tags`,
`surfaces.*.excludeTags`, `config.ignore.tags`) need a way to target
whole rule families without each rule file repeating the same tag.
The codegen now merges bucket-derived tags into every entry:

- `react-native/` bucket rules get `"react-native"`
- `server/` bucket rules get `"server-action"`

Tags authored on the rule (e.g. `test-noise` on
`no-react19-deprecated-apis`) are preserved and deduped at registry
build time. The four migration-prompt rules now carry an explicit
`migration-hint` tag too:

- `no-react19-deprecated-apis`
- `no-react-dom-deprecated-apis`
- `no-legacy-class-lifecycles`
- `no-legacy-context-api`

`formatAutoTagsLine` mirrors prettier's wrap-at-100-cols decision at
codegen time so the regen-equals-formatted contract enforced by
`gen:check` stays stable.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* test: cover severityOverrides resolution, post-lint apply, and tag registration

- `resolve-rule-severity-override.test.ts` checks precedence,
  empty-overrides short-circuit, the multi-tag most-permissive-wins
  rule, and category / tag fallback.
- `apply-severity-overrides.test.ts` verifies `off` drops both
  react-doctor and external-plugin diagnostics; `warn` / `error`
  re-stamps the severity; and the no-overrides path returns the
  input array by identity.
- `rule-tag-registration.test.ts` guards the bucket-derived auto-tags
  (`react-native` on every RN rule, `server-action` on every server
  rule) and the explicit `migration-hint` tags on the four migration
  rules, plus that authored tags survive the bucket merge.
- `validate-config-types.test.ts` extends the existing surfaces
  coverage with severityOverrides happy / invalid / partial-channel
  paths.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* docs: document severityOverrides and the five rule-family tags

Adds a README section right after the surface controls explaining
when to use `severityOverrides` (single value applied across every
channel) vs `surfaces` (visibility-only, per channel), with a
copy-pasteable JSON example covering the five tag families called
out in the original TODO — `design`, `test-noise`, `react-native`,
`server-action`, `migration-hint`. Updates the TODOs entry to
landed status and adds a minor-bump changeset.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* refactor: tighten severity-override surface for elegance

Net -58 lines while making each path read more directly:

- New `get-diagnostic-rule-identity.ts` projects a diagnostic onto
  the three axes every rule-targeted control reasons about
  (`ruleKey`, `category`, `tags`). `filter-for-surface` and
  `apply-severity-overrides` both consume it instead of repeating
  the plugin-aware tag lookup inline.
- `resolve-rule-severity-override` drops the two-level
  `pickMostPermissive` / `collectTagOverride` helpers in favor of a
  single linear scan with a small `isMorePermissive` predicate;
  reads top-to-bottom in rule > category > tags precedence order.
- `apply-severity-overrides` factors a `restampSeverity` helper
  driven by a `SEVERITY_FOR_OVERRIDE` map, keeping the main loop to
  three lines of decision logic.
- `createOxlintConfig` collapses the override-then-fallback into one
  `?? rule.severity` expression so the "\"off\" skips registration"
  check is the only branch left.
- Codegen drops the manual prettier-width math entirely — the
  shorter `[...new Set(...)]` form always fits under 100 cols, so
  `gen:check` stays idempotent without the wrap-aware fork.
- `validate-config-types` extracts `applyFieldValidator`,
  `isPlainObject`, and `formatType` so the four "if undefined skip;
  validate; store or delete" blocks at the bottom collapse into one
  expression each, and the three "must be plain object" guards stop
  duplicating their type check.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* test: dogfood js-combine-iterations in rule-tag-registration test

React Review flagged two .filter().map() chains in the test file as
`js-combine-iterations` warnings (we eat our own dogfood). Replaces
both chains with a shared `collectRuleIdsMatching` helper that walks
`Object.entries(plugin.rules)` once and pushes ids into a single
result array — same behavior, single pass.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* fix(core): warn on unknown severityOverrides channel keys

Bugbot caught an asymmetry: `validateSurfacesField` iterates the
user-provided keys via `Object.entries` and warns on unknown surface
names, but `validateSeverityOverridesField` only iterated the
allowlist — so a typo like `"rule"` instead of `"rules"` or
`"category"` instead of `"categories"` silently dropped the
override with no signal to the user.

Switches the iteration to walk the user's keys (matching surfaces)
and emits `config field "severityOverrides.<key>" is not a known
channel (expected one of: rules, categories, tags); ignoring.` when
the key isn't recognized. Adds a regression test for the typo path.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* refactor: rename severityOverrides config to severity for ESLint/oxlint idiomacy

The wrapper key `severityOverrides` was the only non-idiomatic part of
the design — ESLint exposes severity under top-level `rules`, oxlint
exposes it under top-level `rules` + `categories`, and Biome under
`linter.rules`. None wrap it in a multi-word `*Overrides` namespace,
and our own project's existing wrapper namespaces (`ignore`,
`surfaces`) are single-word.

Renames:

- Config field `severityOverrides` → `severity`. The triple
  `ignore` / `surfaces` / `severity` reads as a coherent set of
  single-word namespaces parallel to the rest of the project.
- Type `SeverityOverrideControls` → `RuleSeverityControls`. The value
  type stays `RuleSeverityOverride` so it doesn't collide with the
  plugin's existing `RuleSeverity = "error" | "warn"` (the
  registrable subset).
- File `apply-severity-overrides.ts` → `apply-severity-controls.ts`,
  function `applySeverityOverrides` → `applySeverityControls`. Matches
  the config field's noun form.
- Validation error messages updated from
  `config field "severityOverrides.<x>"` to
  `config field "severity.<x>"`.
- README, changeset, and TODOs.md updated to draw the explicit
  parallel to ESLint's `rules: { ... }` and oxlint's
  `rules: { ... } + categories: { ... }`, plus the extra `tags`
  channel for behavioral families.

All 1158 tests pass.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* refactor: promote severity controls to top-level rules / categories / tags (ESLint shape)

The wrapper namespace was the last non-idiomatic piece. ESLint exposes
severity under top-level `rules` (its hallmark field name and shape).
oxlint exposes `rules` + `categories` at the top level. Promoting our
three channels to the config root gives users the exact field they
already know from ESLint, plus oxlint-shaped `categories` and a
`tags` extension with the same Record-of-severity shape.

Before:

  {
    "severity": {
      "rules":      { "react-doctor/no-array-index-as-key": "error" },
      "categories": { "React Native": "warn" },
      "tags":       { "design": "off" }
    }
  }

After (matches ESLint's flat-config / .eslintrc shape exactly, plus
oxlint's `categories`):

  {
    "rules":      { "react-doctor/no-array-index-as-key": "error" },
    "categories": { "React Native": "warn" },
    "tags":       { "design": "off" }
  }

Changes:

- `ReactDoctorConfig` drops `severity` and gains three top-level
  fields `rules`, `categories`, `tags`. Each is a
  `Record<string, RuleSeverityOverride>`.
- New `buildRuleSeverityControls(config)` adapter assembles the
  internal `RuleSeverityControls` shape from the three top-level
  fields so the resolver and the oxlint config builder don't need
  to know about the public API shape.
- `applySeverityControls` and `runOxlint` switch to the adapter;
  `resolveRuleSeverityOverride` is unchanged (still takes
  `RuleSeverityControls` directly).
- Validation: drops the `severity` wrapper validator; adds a
  shared `validateSeverityMap(fieldName, value)` invoked once per
  top-level field. Error messages now read `config field "rules"` /
  `config field "tags.design"` — matching what the user typed.
- Tests, README, changeset, and TODOs.md updated. README now opens
  the section with "The exact ESLint / oxlint shape" so the parity
  is the first thing a reader sees.

All 1161 tests pass (gained 3 from build-rule-severity-controls.test.ts).

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* ci: retrigger workflow after path filter miss

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* ci: nudge actions queue

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* chore: drop changeset for category-level rule controls

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* docs(readme): tighten severity-controls section

Previous version was 32 lines of intro + 5-bullet behavior reference
+ full tag/category enumeration. Cuts to one intro sentence, a
compact JSON example, and a single paragraph covering precedence and
where the value applies. The full tag/category list is implicit from
the example; the precedence and most-permissive-wins rule are now
one sentence each.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* refactor: drop user-facing severity-by-tag (tags are internal metadata)

Per-tag severity at the top level exposed an internal grouping
concept that doesn't exist in ESLint or oxlint. Tags are
behavioral labels rule authors invent; surfacing them as a
first-class user config field made `react-doctor.config.json`
depend on internal labeling decisions.

Drops the top-level `tags` field. The severity surface is now
strictly ESLint + oxlint shaped:

  - `rules`      — ESLint's exact field
  - `categories` — oxlint's exact field

Recovers the dropped functionality via existing fields:

  - whole-family suppression: `ignore.tags: ["design", ...]`
    (existing field, unchanged)
  - per-channel visibility:   `surfaces.*.{include,exclude}Tags`
    (existing field, unchanged)
  - per-rule severity:        list rules explicitly in `rules`

Code falls out:

  - `RuleSeverityControls.tags` removed
  - `resolveRuleSeverityOverride` collapses to a two-line lookup
    (`rules` -> `categories`); drops the most-permissive-wins
    machinery and the `SEVERITY_RANK` map entirely
  - `buildRuleSeverityControls` drops the tag merge
  - `validate-config-types` SEVERITY_FIELD_NAMES = ["rules", "categories"]
  - `applySeverityControls` destructures only the two channels it
    needs from `getDiagnosticRuleIdentity` (which keeps returning
    tags for surface filters / ignore.tags consumers)

Tests, README, and TODOs updated to reflect the smaller surface.
1169 tests still passing.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
2026-05-16 22:55:28 -07:00
Aiden Bai 118741e260 Simplify rule set exports (#277) 2026-05-16 19:58:10 -07:00
Aiden Bai f0198e2f2d refactor: extract @react-doctor/{types,project-info,core} internal packages + cleanup (#249) 2026-05-14 22:22:19 -07:00
Aiden Bai 4d622970b5 Revert "pub"
This reverts commit 4d9d1a5fcf.
2026-05-14 22:05:22 -07:00
Aiden Bai 4d9d1a5fcf pub 2026-05-14 22:03:02 -07:00