Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .changeset/category-severity-no-opt-in.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-doctor": patch
---

Fix category-level severities silently force-enabling opt-out rules. A config that only re-stamps category severities (e.g. `categories: { "Maintainability": "warn" }`) no longer activates `defaultEnabled: false` rules such as `forbid-component-props`, `react-in-jsx-scope`, `no-danger`, or `design-no-redundant-size-axes` in that category — enabling an opt-out rule now requires pinning the rule itself (or a legacy alias key) to `"warn"`/`"error"` under `rules`, matching the documented contract. Category severities still re-stamp the severity of already-enabled rules, and `react-doctor rules` now previews the same behavior.
13 changes: 13 additions & 0 deletions .changeset/cli-audit-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"react-doctor": patch
---

CLI audit fixes:

- Windows agent hooks no longer report false findings on every edit (cmd.exe's exit 9009 falls through, the local bin is probed as the runnable `.cmd` shim, 16 MiB output buffer, guarded output read).
- Legacy `.sh` agent hooks (≤0.5.8) are upgraded to the current Node hook by a once-per-repo migration on your next interactive scan (and on re-install) instead of scanning every edit twice; the cleanup is anchored to the exact legacy install paths and never touches unrelated hook groups in your settings.
- `ci upgrade --pr` restores the workflow file and explains an already-open React Doctor PR instead of silently claiming success; `ci config` bails to the apply-by-hand snippet on YAML syntax errors instead of crashing.
- The action-pin migration only rewrites `millionco/react-doctor` refs (in any owner casing) — a fork's `@main` is no longer rewritten to a tag that may not exist on the fork.
- Baseline and `--staged` scans resolve `config.plugins` from the real config directory, so custom-plugin findings are no longer mislabeled as newly introduced.
- A workspace module's `noScore: true` survives workspace scans, and the multi-project share prompt honors each project's merged `noScore`/`share` — any opted-out project now suppresses the aggregate share link.
- Degraded baseline results are no longer cached, and older binaries treat a newer CLI state schema as read-only (reads never rewrite the state file).
22 changes: 22 additions & 0 deletions .changeset/fix-1013-audit-followups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"oxlint-plugin-react-doctor": patch
"eslint-plugin-react-doctor": patch
---

fix(rules): close three follow-up gaps in the 20-day audit fixes

- **Comment stripper**: `isRegexLiteralStart` now uses a Unicode-aware
identifier class, so a division after a non-ASCII identifier (`café / total`,
`合計 / 個数`) is no longer misread as a regex literal — which had blanked
real code up to the next slash and let `/* … */` comment bodies escape
stripping across the pattern-based security-scan rules.
- **`server-auth-actions`**: the cache/navigation exemption now requires the
callee to resolve to _any_ import rather than specifically `next/cache` /
`next/navigation`. A module-local `const revalidatePath = …` (a privileged
shadow) is still flagged, but a revalidation-only action importing through a
common re-export barrel (`import { revalidatePath } from "@/lib/cache"`) is no
longer a false positive.
- **`rn-no-raw-text`**: fragment piercing now sees through named
`<Fragment>` / `<React.Fragment>` (via the existing `isJsxFragmentElement`
helper), not only the shorthand `<>`, so children forwarded through a named
fragment into a host are classified the same as the shorthand form.
13 changes: 13 additions & 0 deletions .changeset/fix-979-ink-tui-rule-false-positives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"oxlint-plugin-react-doctor": patch
"eslint-plugin-react-doctor": patch
"react-doctor": patch
---

Fix four false positives found by React Doctor reviewing real, idiomatic React code (the Ink TUI in #979):

- `no-derived-state` no longer flags state accumulators — a `setState` inside an effect whose functional updater computes the new value from its own parameter (`setKeys((previous) => new Set(previous).add(key))`, `setTotal((prev) => prev + count)`, `setItems((prev) => [...prev, item])`). Accumulated history is by definition not derivable from the current props/state. The spread-only object merge (`setForm((prev) => ({ ...prev, field: <derived> }))`) still reports.
- `no-array-index-as-key` no longer flags positional rendering of string fragments (characters, lines, tokens): `[...str]` and `Array.from(str)` where the source is provably a string (literal, template, `String()` call, or a binding/prop typed `string` in the same file), plus any `str.split(...)` receiver (only strings have `.split`, so no proof is needed) — including a local binding initialized from one (`const parts = line.split(" "); parts.map(...)`). Fragment position is the stable identity there — nothing reorders, filters, or carries per-item state. Data lists still report.
- `prefer-useReducer` now requires an actual co-update signal instead of merely counting `useState` calls: it reports only when the threshold number of distinct setters are called together as sibling statements of one handler/effect block. Independent state updated from separate handlers or separate keyboard-handler branches stays quiet, and the message no longer claims each `useState` "can trigger a separate render" (wrong since React 18 automatic batching) — it now explains the real rationale: state that changes together is easier to keep consistent as a single reducer action.
- `jsx-no-jsx-as-prop` only claims what it can prove: when the receiving component is not resolvable in the current file (imported), the message uses conditional wording ("If this child is memoized, …") instead of asserting a memo bailout that may not exist. Same-file components provably wrapped in `memo()` (or MobX `observer()`) keep the assertive message; provably plain function components already stayed quiet.
- `lazy()` / `React.lazy()` components are no longer treated as memoized — `lazy` defers loading but does not skip re-renders. `jsx-no-jsx-as-prop` now uses the conditional wording for them, and the memoised-consumer-gated rules (`jsx-no-new-object-as-prop`, `jsx-no-new-array-as-prop`, `jsx-no-new-function-as-prop`, `prefer-stable-empty-fallback`) no longer report fresh-reference props passed to a `lazy()` component, matching their premise of a provably defeated memo bailout.
6 changes: 6 additions & 0 deletions .changeset/fix-core-20-day-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"react-doctor": patch
"@react-doctor/core": patch
---

Core-engine reliability and security fixes from the 20-day audit. The lint binary-split retry budget is now scoped per batch and anchored at the first failure, so one pathological batch no longer starves the rest of the scan's recovery (and drop reasons name the limit that fired). `REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS` can now raise the supply-chain budget instead of only lowering it. A corrupt per-file lint cache no longer fails every warm scan until hand-deleted, and the cache now busts when the oxlint child runs a different Node than the CLI (nvm fallback). The `/tmp` fallback cache directory is scoped per user so another local user can't pre-create and poison it, and the auto-detected default branch is validated before reaching git argv. The spawn argv guard is platform-sized (Windows 24k chars, macOS 800k, other POSIX 1.5M), so large `--scope lines` diffs no longer silently degrade to file scope on Linux/macOS. A security-scan I/O failure now skips that pass instead of failing the whole scan, and is reported on the run's telemetry as `securityScan.failed`. Note: the per-file lint cache is invalidated once on upgrade (its ruleset-hash separators changed).
22 changes: 22 additions & 0 deletions .changeset/fix-react-builtins-false-positives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"oxlint-plugin-react-doctor": patch
---

fix(react-builtins): eliminate false positives across builtin DOM/JSX rules

Harden the react-builtins rules against false positives on real-world code:

- `button-has-type`, `iframe-missing-sandbox`, `checked-requires-onchange-or-readonly`: a JSX or `createElement` spread (`{...props}`) can forward the "missing" attribute at runtime, so these rules no longer report an attribute they cannot see — except an `<iframe>` with an explicit `src`, which marks the real embed site where a missing `sandbox` is the author's omission. `button-has-type` also resolves locally-bound and destructured/renamed `type` props (only through `const` initializers and only when the destructure roots at a function parameter), and treats explicitly nullish `createElement` props (`null` / `undefined` / `void 0`) as missing.
- `no-find-dom-node`: a bare `findDOMNode(...)` is flagged only when it is imported from `react-dom`, so a local helper of the same name is left alone.
- `no-is-mounted`, `no-this-in-sfc`: fire only inside an actual React component, so a plain class that exposes an `isMounted` method or an ES5 constructor keeps its real `this`.
- `no-call-component-as-function`, `no-unstable-nested-components`: a capitalized helper that is only ever called `Name()` (never instantiated as an element) is treated as an inline render helper, not a component — unless the helper owns hook calls, which inline into the caller's hook order. The instantiation check is keyed by binding, not name — a same-named component rendered elsewhere in the file doesn't count — and `createElement(Name, …)`, `<Thing.Panel/>`, and escaping reads (`withAnalytics(Inner)`, `component={Inner}`) count as instantiation alongside `<Name/>`.
- `rules-of-hooks`: a factory-named function (`init` / `create*` / `make*` / `build*`) outside any component or hook whose own scope issues several hook calls is treated as a custom hook / factory body even though its name breaks the `useXxx` / PascalCase convention (Solid→React ports use these shapes); this escape also covers the React 19 `use()` hook. A `use`-prefixed callee that resolves to a local hook-free function (e.g. ajv's `useKeyword`) is not treated as a React hook.
- `exhaustive-deps`: a zero-arg accessor call (`foo()`) listed in the deps array now matches the captured accessor instead of being dropped as a complex dependency, and its callee resolves for the unstable-function-dep check; a computed callee (`items[index]()`) stays a complex dependency, and an unused zero-arg call dep (`Date.now()`) is reported as a complex expression instead of a misleading unnecessary-dependency message.
- `jsx-no-script-url`: the `javascript:` match is anchored to the URL start, so an ordinary `https:` link that merely contains `JavaScript:` deeper in its path is not flagged.
- `jsx-no-comment-textnodes`: an interpolated `//` separator glyph (`{used} // {total} GB`) — including one with a literal right side (`{used} // 512 GB`) — is no longer mistaken for a `// comment`.
- `no-string-false-on-boolean-attribute`: custom elements (hyphenated tag names) own their attribute semantics and are skipped.
- `void-dom-elements-no-children`, `no-danger-with-children`: whitespace-with-newline text, `{/* comment */}`, and `{undefined}` / `{null}` / `{void 0}` no longer count as meaningful children; both rules also ignore nullish positional children in `createElement` (`createElement("img", props, null)`). `no-danger-with-children` still reports when two or more children survive the JSX transform, since React's `props.children != null` conflict guard sees the resulting array even when every entry is nullish.
- `no-unknown-property`: `transform-origin` is allowed on every transformable SVG element (including `a`, `defs`, gradients, and `stop`), not just `<rect>`.
- `no-prevent-default`: an href-less `<a>` (anchor-as-button) is not a dead link, and an anchor whose handler performs its own navigation after `preventDefault()` (router push, `location.href` assignment, `window.open`) is custom SPA navigation, not a dead link.
- `jsx-no-jsx-as-prop`: `indicator`, `decoration`, and `*Children` props (antd `checkedChildren` and friends) are recognized as slot props.
- dumi doc trees (`/.dumi/`) are treated as non-production files, so demo/docs code inside them is skipped by the rules that skip test-like files.
13 changes: 13 additions & 0 deletions .changeset/fix-security-rules-skip-test-files.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"react-doctor": patch
"oxlint-plugin-react-doctor": patch
"eslint-plugin-react-doctor": patch
---

Stop `no-eval` and `auth-token-in-web-storage` from firing in non-production files

`eval` / `new Function` / a stringy `setTimeout`, and a token written to web
storage, are only vulnerabilities in code that ships to users. Both rules now
skip test, spec, fixture, story, and script files (`isTestlikeFilename`), so a
`new Function(...)` inside a `*.test.ts` or a throwaway token in `__tests__/` is
no longer reported. The rules stay fully enabled in production code.
24 changes: 24 additions & 0 deletions .changeset/fix-server-auth-actions-cache-revalidation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"oxlint-plugin-react-doctor": patch
---

fix: stop flagging non-privileged server actions in server-auth-actions

`server-auth-actions` flagged any exported server action without an auth check,
including actions that touch no protected data. It now exempts an action whose
body only:

- busts the Next.js cache — `revalidateTag`, `revalidatePath`, `expireTag`,
`expirePath`, and the `unstable_` variants, and/or
- navigates — `redirect`, `permanentRedirect`, `notFound`, `forbidden`,
`unauthorized`.

An unauthenticated caller gains nothing by invoking such actions, so requiring
an auth guard was a false positive.

The exemption is deliberately conservative — the body must contain at least one
cache- or navigation call (matched only as a bare imported identifier, never a
same-named method like `obj.redirect()`) and **no** other effect. Any DB query,
`fetch`, imported helper, raw-SQL tagged template (`sql\`DELETE …\``),
constructor, or assignment keeps the action flagged, so a genuinely sensitive
action is never silently allowed through.
33 changes: 33 additions & 0 deletions .changeset/fp-fix-architecture-correctness-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
"oxlint-plugin-react-doctor": patch
---

fix(architecture): eliminate false positives across architecture, correctness, and design rules

Hardens ~15 rules so they stop firing on valid code, without weakening the real smell each targets.

Architecture:

- `no-many-boolean-props` requires actual render output before treating a parameter as component props (so non-component factories like `CreateValidator(options)` are skipped; JSX inside `.map`/`useMemo` callbacks still counts), and no longer counts props that are invoked, wired as event handlers (`onClick={showMenu}`), or passed as imperative-prefixed call arguments (`setTimeout(props.showMenu, 100)`) as boolean flags — resolving each name to the component's own props binding, including renamed destructurings.
- `no-nested-component-definition` only flags a nested definition that is actually rendered — as JSX (`<Inner/>`) or by reference through a component prop (`component={Inner}`) — inside its own enclosing component, not a capitalized helper that is merely called (`Inner()`), and no longer leaks a sibling component's `<Inner/>` onto a same-named call-only helper.
- `no-render-in-render` exempts render-prop invocations (`props.renderX()`, `this.props.renderX()`, `props.slots.renderX()` on a nested prop bag, and render props destructured or aliased from props or a component parameter — including defaulted/conditional aliases like `props.renderItem ?? defaultRender`), while still flagging local `render*` helpers, `this.renderX()` class-field calls, and a `render*` parameter of an ordinary nested helper.
- `no-render-prop-children` ignores `render*Props` config bags and literal `render*` mode/flag values, which are not render slots.
- `prefer-module-scope-static-value` no longer hoists initializers that call impure globals (`Date.now()`, `Math.random()`, `crypto.randomUUID()`, `nanoid()`, …) — local helpers that merely share one of those names stay hoistable — and abstains when every reference is a read-only scalar lookup (`KEYS.includes(k)`), where referential identity can't matter.
- `react-compiler-destructure-method` drops `useSearchParams` (its methods are unbound and throw when destructured).
- `react-compiler-no-manual-memoization` leaves `memo(Component, areEqual)` with a custom comparator alone (a nullish second argument still counts as redundant).

Correctness:

- `html-no-invalid-paragraph-child` and `html-no-nested-interactive` stop at JSX attribute boundaries, so an element passed as a prop is no longer treated as a DOM child / nested element — except the explicit `children` prop, which React renders as a real DOM child.
- `no-polymorphic-children` only flags `typeof children` when `children` resolves to the component's props, not a local variable or field that happens to be named `children`.
- `no-prevent-default` skips `<form action=…>` (which has a native no-JS submit path) and anchors whose handler carries positive navigation evidence after `preventDefault()` (`router.push`, `navigate(...)`, `window.open`, delegation to a prop handler) — analytics-only handlers stay flagged as dead links — and stays quiet in test/demo files.
- `no-uncontrolled-input` treats `onInput` as controlling like `onChange`, no longer flags `disabled` inputs (React suppresses its missing-`onChange` warning for `disabled` fields, just like `readOnly`) unless `disabled={false}` is literal, and stays quiet in test/demo files.
- `rendering-svg-precision` requires at least two over-precise token occurrences before reporting, and stays quiet in test/demo/docs-site files.

Design:

- `no-gray-on-colored-background` pairs gray text and colored backgrounds by Tailwind variant scope (order-insensitive, `!important`-aware), including the additive case where a base utility applies under a variant with no same-property override, and tightens the palette/shade patterns.
- `no-layout-transition-inline` matches an exact set of layout property tokens (now also `border-*-width`, `line-height`, `column-width`) so lookalikes such as `stroke-width` no longer match.
- `no-long-transition-duration` exempts infinite / looping animation segments (an animation NAME containing "infinite" still counts) and decorative `aria-hidden` elements.
- `no-outline-none` allows `outline: none` alongside a class that ADDS a visible ring on the element's OWN focus (removal utilities like `focus:ring-0` / `focus:outline-hidden` and `group-focus:`/`peer-focus:` variants don't count) or on elements removed from the tab order (negative `tabIndex`, including conditionals where both branches are negative).
- `no-side-tab-border` runs arbitrary hex/rgb/hsl border colors through the same achromatic check as named palette colors, preferring the color scoped to the flagged side (`border-l-[#e5e7eb]`) over the base border color.
31 changes: 31 additions & 0 deletions .changeset/fp-perf-rules-hardening.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"oxlint-plugin-react-doctor": patch
---

fix(performance): reduce false positives across performance, js-performance, and bundle-size rules

Hardens the performance rule families so common, legitimate patterns stop
triggering warnings. Validated against 500 distinct OSS repos with the RDE
harness (react-doctor caching disabled).

- **bundle-size** — `no-dynamic-import-path` only treats bundler-analyzable
relative specifiers (`./`, `../`) as static prefixes (protocol/absolute
URLs stay flagged); heavy-library rules skip type-only imports;
`no-undeferred-third-party` ignores `type="module"` and non-executable
script types.
- **js-performance** — smarter guards for order-dependent async
(`async-await-in-loop`, `async-parallel`), `.find()` in loops
(`js-index-maps`: single-field equality, loop-variant receivers — including
receivers behind a TS cast — and nested-scope bindings), property-access and
`localStorage` caching, `filter(Boolean)` chains, `Intl`/`RegExp` memo and
hoist patterns, direction-aware `Math.min`/`Math.max` hints, small literal
`includes`, and `[...x].sort()` when `x` is a fresh, otherwise-unreferenced
array or iterator.
- **`no-json-parse-stringify-clone`** — exempts clones inside `snapshot*`
helpers, and no longer flags `JSON.parse(JSON.stringify(x, replacer))` when
the replacer is an inline function or array (it transforms the output, so
`structuredClone` is not an equivalent rewrite).
- **performance / React** — memo inline-prop skips custom comparators and
`ref`/`key`; hoist-JSX respects render-local components; the hydration rule
ignores time/random inside nested handlers; loading-state, derived-hook, and
memo-before-return rules only fire when the suggested refactor would help.
Loading
Loading