FP-FIX(frameworks): eliminate false positives across nextjs/server/tanstack/jotai/preact rules - #995
Conversation
…ack/jotai/preact rules Domain slice of the rule false-positive hardening split off #986: rule detector refinements + regression tests for this domain (and its domain-local utils/constants/fixtures). No changeset (intentional).
commit: |
A catch only forwards a Next.js/TanStack redirect's control-flow error when it re-throws the CAUGHT binding (throw e). Previously: - nextjs: any throw in the catch suppressed (so throw new Error() — which swallows the redirect — was missed). - tanstack-start: a bare isRedirect()/isNotFound() call suppressed even with no rethrow. Both now require throw <caughtBinding>; fresh throws / guard-only catches flag. (Bugbot)
…hared utils The false-positive hardening pass copy-pasted the same helpers across many rules. Extract four shared utils — find-enclosing-function, get-function-binding-name, collect-handler-referenced-names, catch-clause-rethrows-caught — plus a HOOK_NAME_PATTERN constant, and repoint every rule at them. Also reuse existing utils (isFunctionLike, getCalleeName, findJsxAttribute) in place of re-implemented inline copies, and drop a nested ternary. Behavior-preserving: the full plugin suite is unchanged (7409 passing, 88 skipped); typecheck, lint, and format are clean. Net ~-250 source lines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed a follow-up deslop / consolidation commit (
Behaviour-preserving: full plugin suite unchanged (7409 passing / 88 skipped), typecheck + lint + format clean, |
RDE validation (500 distinct OSS repos, cache disabled, 161 hits manually classified) surfaced two systematic false positives: - server-hoist-static-io flagged reads whose path derives from a handler param through intermediate bindings (params -> pathArray -> filePath -> fullPath). Taint now propagates through variable declarations, mirroring tanstack-start-loader-parallel-fetch. - query-mutation-missing-invalidation missed tRPC-style invalidation (`utils.posts.invalidate()`). `invalidate` joins QUERY_CACHE_UPDATE_METHODS. Adds a changeset for the branch's framework-rule FP hardening. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RDE eval results (500 distinct OSS repos, cache disabled)
Fixed from eval findings (with regression tests):
Deferred (documented limits): helper-wrapped invalidation ( Zero-hit audit: 13 silent rules verified — corpus route files in scanned rootDirs contained no violations (all Suite: 7412 passed / 88 skipped, typecheck + format clean. |
…ny mention Bugbot: isForwardedBinding treated ANY non-member-access reference as "forwarded", so an effect dependency array ([query]) or a stray mention in a nested closure silenced the rule while the component read query.data field-by-field in render. Forwarding is now a positive list matching the escape's documented intent: returned (incl. implicit return and returned tuple/object), call argument, JSX, spread, or re-bound. A bare dependency- array mention no longer suppresses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ause throws are not swallowed Bugbot: findEnclosingTryForThrow treated ANY enclosing TryStatement as swallowing. A `throw redirect()` in a bare try/finally (no catch) or inside a finally clause propagates normally, so neither must be flagged. The nextjs sibling already had the correct walk (try BLOCK contains the node + a catch handler exists, keep climbing past catch/finally so an outer swallowing try is still found) — extract it as the shared findGuardingTryStatement util and use it in both rules. Also fixes the inner-catch false negative: a throw in an inner catch swallowed by an OUTER try/catch is now reported. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…trol flow before flagging
…ow request-path bindings
…or shapes before flagging
…tom and raw-query-atom
…ating children as VNodes
…React's ViewTransition
…sion handlers for preventDefault
…mework # Conflicts: # packages/oxlint-plugin-react-doctor/src/plugin/utils/contains-fetch-call.ts
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5b1b879. Configure here.
| let ancestor: EsTreeNode | null | undefined = node.parent; | ||
| while (ancestor) { | ||
| if (isFunctionLike(ancestor) && !isImmediatelyInvokedFunctionCallee(ancestor)) { | ||
| return null; |
There was a problem hiding this comment.
Sync helper skips redirect try check
Medium Severity
findGuardingTryStatement stops at any nested function that is not an IIFE, so a redirect() or router throw redirect() inside a helper that the try block calls synchronously (for example function load() { … } then load(), or const go = () => …; go()) is treated as outside the try. The redirect-in-try-catch rules then stay silent even though the surrounding catch still swallows the control-flow error.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 5b1b879. Configure here.
…g helpers) (#988) * fix(rules): shared FP-hardening infra (test harness + cross-cutting helpers) Foundation for the rule false-positive split off #986. Cross-cutting pieces every domain build/test depends on: - test-utils/attach-source-locations: attach `range` to AST nodes so eslint-scope-backed rules resolve correctly under the test harness. - constants/react: shared hook/name tables used by ~10 rule domains. - utils/contains-fetch-call, utils/build-same-file-memo-registry: shared detectors consumed by multiple domains. No changeset (intentional). * fix(rules): descend into synchronously invoked functions in containsFetchCall The stopAtFunctionBoundary walk pruned every nested function, so effects that kick off fetches via an async IIFE or a name-invoked inner function (`async function loadData(){...} loadData()`, `const loadData = async () => {...}; void loadData()`) were missed. Replace the blanket prune with a fixed-point walk that descends only into functions the body itself invokes, still skipping escaping callbacks like event handlers and returned cleanups. * fix(rules): drop `watch` from cleanup-returning subscription methods Returning a `watch` handle is not cleanup: react-hook-form's `form.watch(cb)` returns `{ unsubscribe }` (not callable) and `fs.watch` returns an FSWatcher needing `.close()`. Keep `listen`, whose returned disposer follows the `subscribe` contract, and pin the behavior with effect-needs-cleanup regression tests. * FP-FIX(a11y): eliminate false positives across accessibility rules (#989) * fix(a11y): eliminate false positives across accessibility rules Domain slice of the rule false-positive hardening split off #986: rule detector refinements + regression tests for this domain (and its domain-local utils/constants/fixtures). No changeset (intentional). * refactor(a11y): deslop FP-fix rules (reuse helpers, de-nest, relocate getImplicitRole) Behavior-preserving cleanup of the accessibility false-positive fixes: - interactive-supports-focus: replace the inline spread-attribute check with the existing `hasJsxSpreadAttribute` util (drops a byte-for-byte reimplementation and a now-dead `isNodeOfType` import). - no-redundant-roles: de-nest the implicit-role ternary into an if/else chain (AGENTS.md: no nested ternaries) and de-shadow the filter parameter. - prefer-tag-over-role: merge the two disjoint role sets that were only ever used together in one boolean union into one `ROLES_WITHOUT_CLEAN_TAG`. - role-supports-aria-props / no-redundant-roles: relocate `getImplicitRole` into `utils/get-implicit-role.ts`, removing the only rule-to-rule import in the a11y directory (shared helpers belong in utils/). - control-has-associated-label: un-glue and tighten a comment block. No behavior change: full oxlint-plugin suite (7380 tests) green; typecheck, lint, and format all clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(a11y): flag role={undefined} in no-noninteractive-element-interactions `collectRoleBranches` marked `null`/`false` literals as role-less but ignored the `undefined` Identifier, so `role={undefined}` — and ternary branches resolving to it — fell through the empty-branch early return and silenced the rule, a false negative on a genuinely role-less non-interactive element. Treat the `undefined` identifier like a `null`/`false` literal (`hasNonRoleBranch`), while leaving a genuinely opaque variable role (`role={dynamicRole}`) silent. Adds regression coverage for both. Addresses the Cursor Bugbot "Undefined role silences rule" finding on #989. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(a11y): treat spread children as content in objectHasAccessibleChild `objectHasAccessibleChild` counted an explicit `children` prop as content but not a `{...props}` spread, so `<h1 {...props} />` (forwardRef card titles, markdown component overrides) and `<a {...props} />` were flagged as empty. The RDE 500-repo run surfaced this as heading-has-content's dominant false positive (~83% of sampled hits, all `<h_ {...props} />`). Treat a JSX spread as possible children in the shared util — one fix covers heading-has-content, anchor-has-content, and alt-text's <object> check — matching the spread-bail pattern six sibling a11y rules already use. Genuinely empty elements (no spread) are still flagged; a11y suite green (3206), no OXC fixture divergence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(rules): treat dot-prefixed tool dirs (.dumi, .storybook) as non-production in isTestlikeFilename * fix(a11y): revert the ARIA-1.2 implicit-combobox upgrade in getImplicitRole (oxc parity) * fix(a11y): exempt <label> and testlike files, harden role-expression analysis in no-noninteractive-element-interactions * fix(a11y): drop the ref exemption in no-noninteractive-tabindex (keyboard handlers still exempt) * fix(a11y): treat native checked state as intrinsic aria-checked and skip custom components in role-has-required-aria-props * fix(a11y): count {" "} and static template children as anchor text, skip testlike files in anchor-has-content * fix(a11y): only display-none classes exempt the programmatic file input in control-has-associated-label --------- Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * FP-FIX(performance): eliminate false positives across perf/js-performance/bundle-size rules (#994) * fix(performance): eliminate false positives across perf/js-performance/bundle-size rules Domain slice of the rule false-positive hardening split off #986: rule detector refinements + regression tests for this domain (and its domain-local utils/constants/fixtures). No changeset (intentional). * fix(js-index-maps): don't treat nested-callback bindings as loop-varying collectLoopBoundNames walked the whole loop subtree, so a binding declared inside a nested callback could mark an outer loop-invariant .find() receiver of the same name as loop-variant and suppress the finding. Prune nested function scopes. (Bugbot) * fix(performance): tighten dynamic-import prefix + Intl memo write detection - no-dynamic-import-path: a relative prefix only counts as static when it has a real directory segment before the first hole. `./${pkg}/index.js` scopes the context to the whole dir and is flagged again; `./locales/${lang}.js` stays silent. - js-hoist-intl: drop `push` from the memo-write set — `array.push(new Intl…)` is unkeyed accumulation, not keyed reuse, so it's flagged again. (Bugbot) * fix(async-await-in-loop): treat return-inside-switch as a loop early exit loopBodyHasEarlyExit pruned whole switch subtrees so a `return` inside a switch (which still exits the loop) was missed, flagging order-dependent first-success loops. Split the walk: `return` prunes only nested functions; `break` still prunes nested loops/switches that capture it. (Bugbot) * fix(no-json-parse-stringify-clone): walk all enclosing functions for snapshot exemption isInsideSnapshotHelper stopped at the innermost function, so a deep clone inside a nested helper within a snapshot* function was wrongly flagged despite the persistence exemption. Walk outward through every enclosing function. (Bugbot) * fix(js-min-max-loop): direction-aware Math.min/max rewrite hint The comparator check accepted both ascending (a-b) and descending (b-a) sorts but always mapped [0]->min / [length-1]->max, inverting the suggestion for a descending sort. Return the sort direction and pick min/max from direction + index. (Bugbot) * fix(performance): fix RDE-caught FPs (cast receiver, replacer) + dedup rule helpers Two false positives surfaced by a 500-repo RDE eval (react-doctor caching disabled): - js-index-maps: `(x as T).find((i) => i.id === k)` on a loop-variant receiver behind a TS cast slipped past isLoopVariantReceiver because getRootIdentifierName didn't peel TSAsExpression. It now peels TS cast / non-null / paren wrappers (and ChainExpression) via stripParenExpression. - no-json-parse-stringify-clone: an inline function/array replacer transforms the output, so structuredClone is not an equivalent rewrite — those are now exempt (an identifier replacer still flags, preserving the existing contract test). Also consolidates duplicated guard helpers introduced across the perf rules onto existing shared utils — isFunctionLike / isInlineFunctionExpression, isEverySpecifierInlineType (promoted from rn-no-deep-imports into is-type-only-import), LOOP_TYPES, ITERATOR_PRODUCING_METHOD_NAMES — removes an unnecessary `as` cast, and trims over-long comments to match neighbor convention. Regression tests added for both FPs; full plugin suite green (7367 pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(changeset): patch changeset for perf-rules FP hardening Covers the perf / js-performance / bundle-size false-positive fixes in this PR (no changeset existed for the batch). Patch bump for oxlint-plugin-react-doctor; the fixed group cascades the rest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(harness): dispatch `${type}:exit` post-order and parse fixtures without ParenthesizedExpression Matches what oxlint compiles at runtime: every visitor gets enter/exit pairs (Program:exit special case removed) and `preserveParens: false` so paren-wrapped fixtures exercise the same AST rules see in production. Harness-parity regression tests pin the rules that depended on it. * fix(performance): precise early-exit and cancellation-guard detection in async rules async-await-in-loop: only an early exit conditioned on an awaited result (enclosing if/switch, or a preceding continue-guard clause like `if (!raw) continue; … return raw;`) marks the loop order-dependent; labeled breaks resolve to the loop they actually exit, and awaited-arg collection reuses collectReferenceIdentifierNames. async-defer-await: a bare `*Ref` identifier test no longer reads as a post-await staleness check — only an actual `.current` read counts. * fix(performance): correlate cache-memo arms in js-hoist-intl; dynamic flags in js-hoist-regexp js-hoist-intl: isInsideCacheMemo requires the `new Intl.*` to correlate with an actual cache read or write (lookup-initialized declarator, ??/|| with a lookup operand, ternary on a lookup test, if-guard that writes the cache, direct `.set`) instead of any enclosing memo-shaped expression. js-hoist-regexp: `new RegExp(pattern, flags)` with a non-constant flags argument is not loop-invariant, so don't flag it. * fix(performance): harden membership and iteration rules against RDE-caught FPs js-index-maps: skip loop-bound computed-index receivers; js-set-map-lookups: recognize single-character `includes` and indexOf-as-membership shapes (±1/0 comparisons behind wrappers); js-combine-iterations: exempt identity/Boolean `.filter` adjacencies (block-body identity included); js-min-max-loop: strip parens around comparator bodies so direction detection survives `(a, b) => (a - b)`; js-length-check-first: dominating length-guard detection handles ||-chains, inequality operators, and array reassignment between guard and comparison. * fix(performance): tighten cache and clone guard precision js-cache-property-access: written member-chain prefixes of any depth invalidate caching reads, `.length` reads are never cache-worthy, and shadowing parameters don't suppress outer-chain reports; js-cache-storage: inline iteration callbacks tally storage reads into the enclosing function instead of a fresh scope; js-tosorted-immutable: only a fresh allocation whose binding has no other reference exempts the `[...x].sort()` spread copy; no-json-parse-stringify-clone: the nearest named ancestor decides the snapshot exemption (React component names don't count) and inline revivers are exempt like replacers. * fix(performance): reduce render-rule FPs (memo comparators, hydration timing, memo-before-return) no-inline-prop-on-memo-component: only a default-equivalent custom comparator keeps the memo bailout relevant; no-usememo-simple-expression: paren-wrapped bodies and hook-call initializers are classified like their unwrapped forms; rendering-hydration-mismatch-time: time/random only counts when the enclosing function executes during render and test-like files are skipped; rerender-derived-state-from-hook: transitive consumer tracking so derivations feeding memoized values aren't flagged; rerender-memo-before-early-return: early returns that use ANY memoized value (including via parens/JSX) don't count as wasted memo work. * chore(changeset): note single-reference constraint on the tosorted spread exemption --------- Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * FP-FIX(architecture): eliminate false positives across architecture/correctness/design rules (#996) * fix(architecture): eliminate false positives across architecture/correctness/design rules Domain slice of the rule false-positive hardening split off #986: rule detector refinements + regression tests for this domain (and its domain-local utils/constants/fixtures). No changeset (intentional). * fix(no-many-boolean-props): exclude props.* wired as JSX event handlers The props-object path only skipped invoked callbacks (props.showMenu()); a boolean-prefixed prop wired as onClick={props.showMenu} was still counted, unlike the destructured path. Mirror the event-handler exclusion (Bugbot). * fix(no-render-in-render): scope render-prop exemption to props/this.props isRenderPropReceiver exempted any X.props.renderY() call; an unrelated object with a .props field could hide a real inline render* call. Require this.props (or the bare props identifier). (Bugbot) * refactor(architecture): consolidate event-handler-attribute predicate Deslop pass over the FP-hardening changes in this PR: - extract the `on*` event-handler-attribute check (duplicated twice in no-many-boolean-props) into a shared isEventHandlerAttribute type-guard util - move the memo(_, areEqual) pass-case test into the pass-cases describe block - add the changeset for this PR's false-positive reductions across the architecture/correctness/design rules (the foundation infra commit intentionally deferred it downstream) Behavior-preserving; full oxlint-plugin suite green (7375 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(no-uncontrolled-input): exempt disabled inputs (RDE false positive) RDE validation across 500 OSS repos surfaced 3 identical false positives: `<input value={x} disabled />` (supabase user-management's read-only email field, ported 3 ways). React suppresses its missing-`onChange` warning for `disabled` inputs just like `readOnly`, so a disabled value-input needs no handler and must not be flagged. - add `disabled` to VALUE_PARTNER_ATTRIBUTES - add no-uncontrolled-input.regressions.test.ts (disabled exempt, onInput exempt, genuinely-uncontrolled still flagged) Verified in the built bin against the real files at their scanned refs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(architecture): address bot review on FP-hardening rules Resolves Cursor Bugbot + Devin review findings on PR #996: - no-render-in-render: scope the render-prop parameter exemption to COMPONENT parameters, so a `render*` param of an ordinary nested helper is flagged again; exempt inline member calls on a nested prop bag (`props.slots.renderX()`) to match its already-exempt destructured form. - no-nested-component-definition: scope the rendered-JSX membership test to the candidate's own enclosing component (via isAstDescendant), so a sibling component's `<Inner/>` no longer falsely flags a same-named call-only helper in another parent. - no-outline-none: exclude the `focus:outline-none` / `focus:shadow-none` / `focus:ring-0` removal utilities from the focus-ring exemption; they strip focus styling rather than add a ring. - rendering-svg-precision: move MIN_DISTINCT_OVERPRECISE_SVG_TOKENS into constants/thresholds.ts per the magic-number convention. - extract a shared isComponentFunction util, deduped from the prop-stack tracker's private copy and reused by no-render-in-render. - drop the misplaced no-derived-useState "re-seeded draft buffer" test: it asserts an FP fix implemented in the state-and-effects PR (#990), not this architecture PR, so it failed here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(architecture): scope-aware props resolution (bugbot re-review) Addresses two Cursor Bugbot findings from the re-review: - no-render-in-render: the `props` carve-out no longer matches by NAME. A local variable named `props` (`const props = { renderRow }`) is no longer treated as the component's props bag, so `props.renderRow()` on it is flagged again. Both `rootsInProps` and `tracesToPropOrParameter` now resolve the root through scope via `isComponentParameterSymbol`. - no-polymorphic-children: a body alias of the props (`const { children } = props`) is flagged again — it's the same smell as `({ children })` / `typeof props.children`. Resolution is component-aware, so `const { children } = node` (a non-prop parameter of a non-component tree walker) still stays quiet. Extracts the shared `isComponentParameterSymbol` util (component-aware parameter check) now used by both rules. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(rules): dispatch :exit visitors in the test harness, count call-argument callbacks as render evidence, treat .dumi as test-noise - run-rule.ts now dispatches `<NodeType>:exit` handlers like production oxlint, so stack-based rules pop between sibling declarations in tests. - functionContainsReactRenderOutput no longer treats function expressions passed directly as call arguments (`.map(item => <li/>)`, `useMemo(() => <div/>)`) as nested-render boundaries. - isTestlikeFilename marks `/.dumi/` docs-site paths as non-production. * fix(architecture): resolve fp-review findings across five architecture rules - no-many-boolean-props: callback exclusions are scope-aware (resolve to the component's own props binding), match renamed destructurings on the VALUE name, and extend to imperative-prefixed call arguments (`setTimeout(props.showMenu, 100)`). - no-nested-component-definition: nested components rendered by reference through a component prop (`component={Inner}`) are flagged too. - no-render-in-render: drop the blanket this.renderX() exemption (class fields are per-instance, so `this.` is not a stability signal) and extend the render-prop carve-out to plain/defaulted/conditional aliases of props-rooted render props. - prefer-module-scope-static-value: abstain when every reference is a read-only scalar lookup (`KEYS.includes(k)`); impure bare-call names only match unresolved globals or imports, not local helpers. - react-compiler-no-manual-memoization: a nullish memo comparator (`memo(Inner, undefined)`) is still redundant. * fix(correctness): resolve fp-review findings across correctness rules - html-no-invalid-paragraph-child / html-no-nested-interactive: the explicit `children` prop is a real DOM child, so it no longer stops the ancestor walk at the attribute boundary. - no-prevent-default: an anchor stays flagged unless the handler carries positive navigation evidence (router/window calls, navigate-shaped names, prop-handler delegation) — analytics-only handlers are still dead links; tag the rule test-noise for demo/test files. - no-uncontrolled-input: a literal `disabled={false}` no longer excuses a missing onChange; tag the rule test-noise. - rendering-svg-precision: count over-precise token OCCURRENCES rather than distinct tokens (Inkscape uniform-scale matrices repeat one factor), tag the rule test-noise, and add regression coverage. * fix(design): resolve fp-review findings across design rules - no-gray-on-colored-background: variant scopes are order-insensitive and !important-aware; base utilities pair with variant-scoped counterparts when the variant carries no same-property override; include the 950 gray shade. - no-layout-transition-inline: match an exact layout-property token set (adds border-*-width, line-height, column-width) so lookalikes like stroke-width stay silent. - no-long-transition-duration: the infinite exemption is per shorthand segment (an animation NAME containing 'infinite' still counts) and decorative aria-hidden elements are exempt. - no-outline-none: only utilities that ADD a visible ring on the element's OWN focus count as a replacement indicator; conditional tabIndex qualifies only when both branches are negative. - no-side-tab-border: the side-scoped border color decides before the base color; parse-color-to-rgb learns hsl() and Tailwind underscore spacing. * chore(changeset): sync fp-fix changeset with final rule behavior --------- Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * FP-FIX(security): eliminate false positives across security + security-scan rules (#993) * fix(security): eliminate false positives across security + security-scan rules Domain slice of the rule false-positive hardening split off #986: rule detector refinements + regression tests for this domain (and its domain-local utils/constants/fixtures). No changeset (intentional). * refactor(security): deslop command-exec, metadata-secret, postmessage rules Behavior-preserving cleanup of the FP-hardening detectors: - command-execution-input-risk: split the two-branch `(?:…)|(?:…)` alternation into scanByPattern's array-of-patterns form (the documented idiom, matching raw-sql-injection-risk) so each taint model reads as a discrete pattern. - package-metadata-secret: drop the fragile regex `.source` string-equality filter for `!pattern.test("service_role")` — robust to a re-spelling or flag change of the source pattern in security.ts. - postmessage-origin-risk: inline a single-use local. No behavior change: matching is byte-identical (281 security + security-scan tests pass; typecheck/lint/format clean). No changeset — behavior-preserving, and this split branch ships no changeset by design. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(security): catch leaked JWT credentials in package metadata Dropping the bare service_role keyword from package-metadata-secret must not drop the credential itself: add JWT_LITERAL_VALUE_PATTERN (header AND payload segments are base64url JSON, so both start with eyJ) and append it to PACKAGE_METADATA_VALUE_PATTERNS only — in source files anon-key JWTs are legitimate, but ANY JWT committed into package.json is a leak. * fix(security): tighten spawn/shell taint detection in command-execution-input-risk Recompose the patterns from named String.raw sources and split the spawn branch into three precise shapes: tainted first argument, fixed shell binary running tainted input via its command flag, and shell:true with request taint anywhere in the call (order-independent). Zero-taint spawn/exec with shell:true and a fixed argv no longer fires; the python shell=True kwarg keeps firing on the exec branch. * fix(security): walk every concat operand in raw-sql-injection-risk Replace the single-operand escaper exemption with a safe-tail regex: after the leading literal, zero or more safe operands (string literal or SQL escaper call) followed by an unsafe one fires. One escaped operand can no longer launder a later raw concatenation, and HTML escapers like _.escape()/validator.escape() are no longer treated as SQL-safe; escapeLiteral/escapeIdentifier and multi-line/parenthesized escaper operands stay exempt. * fix(security): cover ancestor and arrow-prop filter shapes in svg-filter-clickjacking-risk Three alternatives: the same-tag window now tolerates arrow props (=> consumed as a unit, bare > still bounds the tag), an unquoted filter:url(# before the iframe catches wrapper/ancestor filters, and the <fe…> primitive window widens to 700 chars. The mined sibling-after- the-iframe decorative-filter FP stays silent. * fix(security): catch concat-built role params and string-safe firebase windows clickjacking-redirect-risk: move [?&](?:amp;)?role= outside the \b group so concat-built and entity-encoded role URLs match (a quote before ? has no word boundary); the ARIA role FP guard stays silent. firebase-client-owned-authz-field: make the statement window quoted- string-aware so a ; inside a string literal in the write's own args no longer truncates it before the authz field. * fix(security): treat #fragment credentials as credentialed URLs in no-secrets-in-client-code Widen the CREDENTIALED_URL_PATTERN delimiter class [?&] -> [?&#] so a fragment-carried credential (OAuth implicit-flow #access_token=) defeats the public-URL exemption and still flags. * fix(secret-in-fallback): only exempt env names public by construction A PUBLIC/PUBLISHABLE/ANON segment anywhere in the name no longer skips the rule: the exemption now requires a leading framework public prefix (NEXT_PUBLIC_/EXPO_PUBLIC_/GATSBY_PUBLIC_/NUXT_PUBLIC_/REACT_APP_PUBLIC_/ VITE_PUBLIC_/bare PUBLIC_) or a publishable/anon key ending (...PUBLISHABLE_KEY/...ANON_KEY), and never applies to names ending in _SECRET/_PRIVATE_KEY/_PASSWORD/_PASSWD. INTERNAL_PUBLIC_WEBHOOK_SECRET with a hardcoded fallback flags again; NEXT_PUBLIC_*, STRIPE_PUBLISHABLE_KEY, and SUPABASE_ANON_KEY fallbacks stay silent. --------- Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * FP-FIX(react-native): eliminate false positives across react-native rules (#992) * fix(react-native): eliminate false positives across react-native rules Domain slice of the rule false-positive hardening split off #986: rule detector refinements + regression tests for this domain (and its domain-local utils/constants/fixtures). No changeset (intentional). * fix(react-native): inline type imports + literal-flip scroll latch - rn-no-legacy-expo-packages / rn-no-non-native-navigator: skip fully inline-type imports (`import { type Foo }`), not just `import type`, via the shared isTypeOnlyImport helper — they erase to no runtime code. Mixed type+value imports still flag. - rn-no-scroll-state: a set-once latch must write a literal constant; a guard that reads the same state but writes a changing value (`if (offset !== last) setLast(offset)`) is a per-frame sync and stays reported. (Bugbot) * fix(rn-no-falsy-and-render): scope-prune nested useState shadowing isBooleanUseStateName walked the whole enclosing scope, so a nested component's useState(false) of the same name could mask an outer useState(0) gate and hide a real bare-0 render crash. Don't descend into nested function scopes. (Bugbot) * refactor(react-native): consolidate type-only-import checks, reuse isHookCall Route all six react-native import rules (bottom-sheet, deprecated-modules, prefer-expo-image, prefer-pressable, prefer-reanimated, no-panresponder) through the shared isTypeOnlyImport helper instead of re-implementing the importKind / every-specifier check inline, and delegate rn-no-falsy-and-render's isUseStateCall to the existing isHookCall. Drop the seven near-duplicate "type-only imports are erased" comments now that the helper's doc is the single source of truth. Behavior-preserving: verified byte-identical over adversarial fixtures and the 283 react-native rule tests are unchanged. Net -39 LOC. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(rn-list-data-mapped): flag aliased recycler imports The rule gated on `REACT_NATIVE_LIST_COMPONENTS.has(localName)`, so an aliased recycler (`import { FlashList as FL } from "@shopify/flash-list"`) resolved to `FL`, missed the set, and returned before the provenance check ever ran — a false negative its two sibling list rules already handled. Extract the recycler-resolution pattern (duplicated across all three list rules) into a shared `resolveImportedRecyclerName` helper that maps a local JSX name back to its canonical export via a real import, covering aliases. rn-list-data-mapped now resolves recyclers by provenance and restricts the built-in branch to the true built-in lists (FlatList/SectionList/ VirtualizedList), so an ambient un-imported recycler still never fires. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(utils): binding-provenance helpers + inline-type-import superset * fix(react-native): resolve real list and Dimensions bindings through aliases, namespaces, and requires * fix(react-native): tighten latch, static-value, and list-item detectors * fix(react-native): recognize namespace member aliases as RN bindings `const FlatList = RN.FlatList` / `const Dimensions = ReactNative.Dimensions` (where RN is a react-native namespace import) were misclassified as local rebindings, silencing rn-list-data-mapped and rn-no-dimensions-get. Extract the shared provenance check into getInitializerModuleSource — require call (member-unwrapped) or namespace-import-rooted reference — and route both rules through it. --------- Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * FP-FIX(frameworks): eliminate false positives across nextjs/server/tanstack/jotai/preact rules (#995) * fix(frameworks): eliminate false positives across nextjs/server/tanstack/jotai/preact rules Domain slice of the rule false-positive hardening split off #986: rule detector refinements + regression tests for this domain (and its domain-local utils/constants/fixtures). No changeset (intentional). * fix(redirect-in-try-catch): require a real re-throw of the caught error A catch only forwards a Next.js/TanStack redirect's control-flow error when it re-throws the CAUGHT binding (throw e). Previously: - nextjs: any throw in the catch suppressed (so throw new Error() — which swallows the redirect — was missed). - tanstack-start: a bare isRedirect()/isNotFound() call suppressed even with no rethrow. Both now require throw <caughtBinding>; fresh throws / guard-only catches flag. (Bugbot) * refactor(rules): consolidate duplicated framework-rule helpers into shared utils The false-positive hardening pass copy-pasted the same helpers across many rules. Extract four shared utils — find-enclosing-function, get-function-binding-name, collect-handler-referenced-names, catch-clause-rethrows-caught — plus a HOOK_NAME_PATTERN constant, and repoint every rule at them. Also reuse existing utils (isFunctionLike, getCalleeName, findJsxAttribute) in place of re-implemented inline copies, and drop a nested ternary. Behavior-preserving: the full plugin suite is unchanged (7409 passing, 88 skipped); typecheck, lint, and format are clean. Net ~-250 source lines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(rules): close two FP patterns found by a 500-repo eval run RDE validation (500 distinct OSS repos, cache disabled, 161 hits manually classified) surfaced two systematic false positives: - server-hoist-static-io flagged reads whose path derives from a handler param through intermediate bindings (params -> pathArray -> filePath -> fullPath). Taint now propagates through variable declarations, mirroring tanstack-start-loader-parallel-fetch. - query-mutation-missing-invalidation missed tRPC-style invalidation (`utils.posts.invalidate()`). `invalidate` joins QUERY_CACHE_UPDATE_METHODS. Adds a changeset for the branch's framework-rule FP hardening. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(query-destructure-result): only real forwarding suppresses, not any mention Bugbot: isForwardedBinding treated ANY non-member-access reference as "forwarded", so an effect dependency array ([query]) or a stray mention in a nested closure silenced the rule while the component read query.data field-by-field in render. Forwarding is now a positive list matching the escape's documented intent: returned (incl. implicit return and returned tuple/object), call argument, JSX, spread, or re-bound. A bare dependency- array mention no longer suppresses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(tanstack-start-redirect-in-try-catch): try/finally and finally-clause throws are not swallowed Bugbot: findEnclosingTryForThrow treated ANY enclosing TryStatement as swallowing. A `throw redirect()` in a bare try/finally (no catch) or inside a finally clause propagates normally, so neither must be flagged. The nextjs sibling already had the correct walk (try BLOCK contains the node + a catch handler exists, keep climbing past catch/finally so an outer swallowing try is still found) — extract it as the shared findGuardingTryStatement util and use it in both rules. Also fixes the inner-catch false negative: a throw in an inner catch swallowed by an OUTER try/catch is now reported. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(rules): shared helpers follow effect-invoked functions and transparent try/catch * fix(nextjs): resolve real next/navigation bindings and effect-run control flow before flagging * fix(server): only flag module state that is actually mutated and follow request-path bindings * fix(tanstack-query): verify query/mutation receiver bindings through scope analysis * fix(tanstack-start): follow loader/server-fn control flow and validator shapes before flagging * fix(jotai): scope-aware atom and query-envelope detection in select-atom and raw-query-atom * fix(preact-no-children-length): require component evidence before treating children as VNodes * fix(no-document-start-view-transition): only fire in files importing React's ViewTransition * fix(client-passive-event-listeners): trace assigned and member-expression handlers for preventDefault * docs(changeset): describe the full framework FP-hardening surface --------- Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * FP-FIX(state-and-effects): eliminate false positives across state/effect rules (#990) * fix(state-and-effects): eliminate false positives across state/effect rules Domain slice of the rule false-positive hardening split off #986: rule detector refinements + regression tests for this domain (and its domain-local utils/constants/fixtures). No changeset (intentional). * fix(no-direct-state-mutation): recognize lazy useState object/array initializers The plain-state gate only matched a direct ObjectExpression/ArrayExpression initializer; lazy initializers (useState(() => []), useState(() => ({}))) were excluded, so in-place mutations of that render state went unreported. Resolve the lazy initializer's returned literal too (Bugbot). * fix(rules): harden the shared guards behind the state/effect FP fixes - reads-post-mount-value: member/global post-mount reads split into reusable predicates; wider imperative-source coverage - contains-non-deterministic-source: don't descend into stored callbacks - is-result-discarded-call: discarded compound positions (guards, statement ternaries, sequences, concise arrow bodies) - is-namespaced-api-call: match the base receiver identifier on member chains (destructured router.replace(...) et al) - is-testlike-filename: match dot-directory tooling surfaces (/.dumi/, /.storybook/) against the full path, before the source-root cut - data-sink-method-names: split STRING_READ_METHOD_NAMES out so callers can special-case props-object string-read collisions - new is-controlled-prop-mirror: setter wired into a JSX event-handler attribute + bare-prop re-sync (the controlled/uncontrolled mirror) * fix(state-and-effects): stop flagging data sinks and discarded reads in the pass-to-parent rules no-pass-data-to-parent: a namespaced API receiver (router.replace(...)) is not a parent callback, but a string-read name called directly ON the props object (props.search(results)) is — un-exempt that collision. no-pass-live-state-to-parent: recognize the widened discarded-result shapes so guarded/ternary/concise-arrow notifications still count as fire-and-forget hand-backs. * fix(state-and-effects): skip controlled-prop mirrors and post-mount reads in the derived-state family A useState seeded from a prop whose setter is wired into a JSX event handler is a controlled/uncontrolled value mirror holding the user's live edits — not a render-derivable value (no-derived-state, no-derived-state-effect, no-derived-use-state). A value measured off a ref or read from a browser global after mount can't be computed during render, so effects that adjust state from it stay silent (no-adjust-state-on-prop-change). * fix(no-cascading-set-state): walk stored handlers, model early-return guard exclusivity Inverted the function-walk skip: only INLINE callbacks handed to non-sync-iteration callees (subscribe/then/setTimeout) are deferred; variable-stored helpers and listener handlers are walked again — their call sites are the writes the reducer recommendation targets (cookiekit bench). Statement sequences now treat an early-returning guard branch as mutually exclusive with the post-guard body, and switch runs sum fall-through cases. * fix(state-and-effects): suppress externally-driven state in the effect-origin rules State written exclusively by a timer / listener / observer / subscription (or seeded from a post-mount read) has no React handler to fold into and no render-time value to derive, so no-initialize-state, no-event-handler, no-prop-callback-in-effect, and no-chain-state-updates stay silent for it while keeping handler-driven state flagged. * fix(state-and-effects): rerender-rule FP hardening rerender-functional-setstate: spread-merge setters are flagged only in deferred-execution contexts (setTimeout / .then / listeners / debounce) where the closure outlives later renders; sync render-path handlers close over fresh state. Arithmetic reads in sync click handlers still fire (batched double-clicks lose updates). rerender-lazy-state-init: skip initializers that read post-mount values. rerender-state-only-in- handlers: drop refs driven exclusively by imperative browser events while keeping the other tested props/state reported. * fix(advanced-event-handler-refs): scope-aware stable-handler resolution Replace the name-based block scan with the scope-aware findVariableInitializer (a prop param shadowing an outer stable binding now resolves to the param). Treat the useEvent-family hooks and empty-deps useMemo as stable handler identities, and exempt a useRef-backed subscription receiver from the re-subscription bailout. * fix(rules): mutation, selector, and uncontrolled-input FPs no-direct-state-mutation: sharpen the plain-state boundary between React-managed data and opaque third-party instances (method chains on plain data stay plain). redux-useselector-inline-derivation: stop flagging selectors whose derivation is the store's own memoized shape. no-uncontrolled-input: skip mined testlike shapes and static inputs that never carry user state. * test(react-doctor): re-pin the state-rule e2e regressions to the hardened detectors external-state-origin: the deferred-suppression case is now an inline subscription callback (stored listener handlers are counted by design — cookiekit bench), with a new true-positive pin for the stored-listener fan-out. no-initialize-state-post-mount and no-uncontrolled-input pick up the widened post-mount and testlike discriminators. * FP-FIX(react-builtins): eliminate false positives across builtin DOM/JSX rules (#991) * fix(react-builtins): eliminate false positives across builtin DOM/JSX rules Domain slice of the rule false-positive hardening split off #986: rule detector refinements + regression tests for this domain (and its domain-local utils/constants/fixtures). No changeset (intentional). * fix(button-has-type): mirror JSX spread bailout + quoted type-prop forward (createElement) - createElement now bails on a spread-only / opaque props bag (`{ ...props }`, `createElement("button", props)`) the same way the JSX arm bails on a spread, since the forwarded object may supply `type` at runtime. null / absent props still report missing. - bindsToDestructuredTypeProp recognizes a quoted `{ "type": kind }` key as a consumer forward, matching the bare-identifier key. (Bugbot) * fix(button-has-type): report missing type for nullish createElement props undefined / void 0 props carry no type, like an explicit null, so they must report missing rather than be treated as an opaque (possibly type-forwarding) props bag. (Bugbot follow-up) * refactor(react-builtins): deslop FP-hardening without changing behavior Simplify the recently added false-positive guards while preserving every regression-tested behavior (plugin suite unchanged: 7357 pass / 91 skip). - Extract the re-inlined `null | undefined` check into one shared `utils/is-nullish-expression.ts` and repoint `button-has-type` (`isNullishPropsArgument`, keeping its local `void 0` case) and `is-meaningful-jsx-child`, removing the triplicated logic. - button-has-type: unscramble the comment stack so each rationale sits above the function it describes. - no-unstable-nested-components: drop the redundant `reportNode` param from `enqueueCandidate` (every call site passed the candidate twice). - void-dom-elements-no-children: `.some(isMeaningfulJsxChild)` — drop a gratuitous `as EsTreeNode` cast, the wrapper arrow, and the dead import. - no-find-dom-node: inline the single-use `REACT_DOM_MODULE` constant. - exhaustive-deps / dom-property-tags: trim comments that restated themselves or the file header. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(changeset): add patch changeset for react-builtins false-positive fixes The FP-hardening changes published rule behavior in oxlint-plugin-react-doctor but the branch shipped without a changeset; add one so the fix is released. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(iframe-missing-sandbox): mirror the spread/opaque-props bailout in createElement The JSX path already bails on a spread (`<iframe {...props} />`) since sandbox can be forwarded at runtime, but the `createElement` path still reported a missing sandbox for an opaque props bag (`createElement("iframe", props)`) and for `createElement("iframe", { ...props })`. Mirror the JSX bailout — and the identical handling in `button-has-type` — so those cases no longer false-positive, while `createElement("iframe", null)` and an explicit object without a spread or `sandbox` still report. Uses the shared `isNullishExpression` util. (Cursor Bugbot #991.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(void-dom-elements-no-children): ignore nullish positional children in createElement RDE (500-repo sweep) surfaced `React.createElement("img", attr, null)` — the idiomatic "no children" form — being falsely flagged: the createElement path counted any positional arg via `arguments.slice(2).length > 0`, so a `null` / `undefined` / `void 0` child triggered the diagnostic. Mirror the JSX path's isMeaningfulJsxChild (and the sibling iframe createElement bailout) by filtering nullish positional children before deciding. A real positional child still fires. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(react-builtins): resolve all PR #991 review-thread findings Bugbot: - no-unstable-nested-components / no-call-component-as-function: the instantiation gates are now keyed by the BINDING IDENTIFIER node the usage resolves to (via scopes.symbolFor(...).bindingIdentifier), not by name, so a same-named JSX usage of a different binding (shadowed import, name collision) no longer counts. The binding node — not symbol.id — is the key because scope analysis registers a hoisted declaration under two symbol records that share one binding identifier. For a named function expression the declarator id wins over the inner `.id` (outside references resolve to the variable). - no-call-component-as-function: `createElement(Name, …)` now counts as an instantiation alongside `<Name/>`. - rules-of-hooks: the multi-hook render-scope escape now also covers the React 19 `use()` branch, so `use(...)` in a `create*` factory that issues several hook calls is no longer reported as non-component. Devin: - no-danger-with-children: the createElement path filters nullish positional children (`…, null)`), mirroring the JSX path. - isNullishExpression now includes the `void` UnaryExpression case — every caller added it manually, and `{void 0}` renders exactly like `{undefined}` — deleting the three per-rule void checks and fixing the JSX/createElement asymmetry. - react-native collect-text-wrapper-components: local isMeaningfulJsxChild renamed to isNonWhitespaceJsxChild (same name as the shared util but deliberately different semantics; rename only). Regression tests added for each; plugin suite 7373 pass / 91 skip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(react-builtins): count callback JSX as render-output evidence for the enclosing function Only name-bound function expressions (nested component definitions) are render-evidence boundaries now; JSX inside call-arg callbacks (rows.map(row => <tr/>)) and returned closures flows into the outer function's render, so it counts as evidence again. * fix(react-builtins): resolve dumi-docs mined false positives - treat /.dumi/ doc trees as non-production files (checked against the full path, before the source-root slice) - allow transform-origin on a/defs/gradients/stop SVG elements - recognize indicator/decoration/*Children as slot props in jsx-no-jsx-as-prop * fix(iframe-missing-sandbox): narrow the spread bailout to iframes without an explicit src An explicit src marks the element as the real embed site, so a missing sandbox alongside a spread is the author's omission and is reported again (JSX and createElement paths). Also skip non-production files. * fix(rules-of-hooks): tighten the render-scope escape and local non-hook callees - gate the structural render-scope escape on factory-shaped names (init/create*/make*/build*) with no enclosing component or hook, so an arbitrary helper with two copy-pasted hooks no longer qualifies - a use-prefixed callee resolving to a local hook-free function (e.g. ajv's useKeyword) is not treated as a React hook on the non-component report paths * fix(no-unstable-nested-components): count escaping reads and member-expression instantiations A nested component consumed by reference (withAnalytics(Inner), component={Inner}, object/array/return/assignment values) escapes its declaration site, and <Thing.Panel/> / createElement(sections.General) instantiate member-assigned candidates — all now count as instantiation evidence. A direct call (Inner()) still does not. * fix(no-prevent-default): exempt href-less anchors and handlers that navigate An <a> without href never navigates on click (anchor-as-button), and an anchor whose handler performs its own navigation after preventDefault() (router push, location.href assignment, window.open, platform link openers) is custom SPA navigation — neither is a dead link. Handlers that only log or track after preventDefault() are still flagged; the <form> path is untouched. * fix(exhaustive-deps): harden accessor-call dependency handling - a computed callee (items[index]()) stays a complex dependency instead of collapsing to its root name and silently matching the capture - unstable-function-dep symbol lookups resolve through the callee for accessor-call deps, so getConfig() finds its binding - an unused zero-arg call dep (Date.now()) is reported as a complex expression instead of a callee-keyed unnecessary-dep message * fix(react-builtins): four rule-local point fixes - button-has-type: identifier resolution is proof only through an unconditional const initializer, and a destructured type key is a consumer forward only when the pattern roots at a function parameter - jsx-no-comment-textnodes: a // separator line continuing a preceding {expression} ({used} // 512 GB) is rendered text, not a comment - no-danger-with-children: two or more surviving children form an array that trips React's props.children != null guard even when every entry is nullish (JSX and createElement paths) - no-call-component-as-function: a nested helper that owns hook calls is not exempt — calling it inlines its hooks into the caller's order * chore(changeset): describe the fp-review hardening round * fix(react-builtins): only count render-flowing reads as nested-component instantiation A PascalCase escaping read now counts as instantiation evidence only when its value flows into a render-shaped sink (JSX expression container, createElement/cloneElement argument, return value, or a PascalCase binding a consumer can render). Arguments to non-rendering calls whose result is discarded (track(Inner), console.log(Inner)) no longer resurrect an inline-only helper. * fix(react-builtins): exclude local non-hook use* callees from the render-scope hook count countOwnScopeHookCalls now applies the same isLocalNonHookFunctionCallee exclusion the report paths use, so a create*-factory with one real hook plus one local useKeyword(...) codegen-helper call no longer reaches the render-scope threshold and wrongly exempts the real hook. A shared visited set guards the mutual recursion between the two helpers. * chore(react-builtins): drop unused Rule type import from rules-of-hooks * fix(react-builtins): unify nearestEnclosingFunction callers onto findEnclosingFunction after foundation merge * fix(no-prevent-default): keep spread anchors checked since a spread can forward a real href * fix(no-prevent-default): require a global receiver for location.href navigation evidence * fix(jsx-no-comment-textnodes): only treat digit-leading text after an expression as a separator --------- Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: resolve PR #988 bugbot findings across four rules - no-pass-live-state-to-parent: port the props.search() string-read carve-out from no-pass-data-to-parent (shared isWholePropsObjectReference helper) - no-render-in-render: exempt render props called through props aliases (const slots = props.slots) with cycle-safe symbol tracing - no-noninteractive-element-interactions: opaque role branches no longer count toward the always-interactive proof; truthy string-literal || left short-circuits - effect-needs-cleanup: .listen() only counts as disposer-returning in the callback-argument shape (Node server.listen(port) returns the server) --------- Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>


FP-FIX — Framework adapter rules
Part of the false-positive hardening stack. Removes false positives across the framework adapters: nextjs, server, tanstack-query, tanstack-start, jotai, preact, client, view-transitions. The largest slice by file count. Based on #988 (foundation) — merge that first.
Highlights
nextjs-no-redirect-in-try-catch/tanstack-start-redirect-in-try-catch— aredirect()/notFound()thrown inside atryis only safe when thecatchre-throws the caught binding (throw e). Fixed in this slice (see Bugbot below). Localredirectshadows and deferred-callback throws are not flagged.query-stable-query-client/query-no-query-in-effect/query-mutation-missing-invalidation/query-destructure-result— TanStack Query patterns narrowed to real misuse.jotai-select-atom-in-render-body/jotai-derived-atom-returns-fresh-object— event-handler / memoized callsites ofselectAtomaren't flagged.server-*(fetch-revalidate, hoist-static-io, mutable-module-state, sequential-await) andnextjs-*(image sizes, inline-script id, no-a-element, client-fetch-for-server-data, client-side-redirect, font-link, script-in-head, side-effect-in-get-handler) tightened. Plustanstack-start-*,preact-no-children-length,client-*,no-document-start-view-transition.Bugbot follow-ups
throw new Error(...)(which swallows the redirect) is reported again.isRedirect()call alone suppresses — FIXED: requires an actual re-throw, not just a guard call.handle*-named fn / lowercase helper actually runs during render), which would reintroduce FPs on the common deferred-handler case. (See threads.)Eval results
407fdbf5w/ regression tests, rest documentedREACT_DOCTOR_NO_CACHE=1+ fresh RDE spec cache).evals/-Users-…-prague.jsonl(RDE), filtered hits in eval commentZero-hit audit: the 13 silent rules (incl. all 7 tanstack-start) were probed directly — in-scope corpus code was genuinely clean (every
throw redirectan idiomaticbeforeLoadguard), and CLI fixtures confirmed each hardened rule still fires on violating shapes while allowing the documented-safe ones. Details in the eval comment.Test plan
mainNote
Low Risk
Changes are confined to static lint heuristics and regression tests in the plugin; they do not alter application runtime behavior. Residual risk is misclassified diagnostics if new edge cases slip past the expanded analysis.
Overview
Patch release for
oxlint-plugin-react-doctorthat tightens framework adapter rules (Next.js, server, TanStack Query, Jotai, Preact, client, view transitions) so legitimate patterns stop firing while real violations still report.Redirect / try-catch —
nextjs-no-redirect-in-try-catch(and the parallel TanStack Start rule) now resolves realnext/navigationimports, ignores shadowed locals, and usesfindGuardingTryStatementso catches that re-throw the caught error, baretry/finally, and deferred callbacks are not treated as swallowing;throw new Errorin catch still flags.Effects & mounts — Client fetch/redirect-in-
useEffectrules follow IIFEs, immediately invoked locals, and promise chains run on mount, but skip event handlers, cleanup, and later-fired callbacks viacollectEffectInvokedFunctions/containsFetchCallboundaries.Server —
server-hoist-static-iotaints request-derived paths through destructuring and bindings;server-no-mutable-module-stateonly reportsconstcontainers that are actually mutated (aliases, nested writes, one-hop same-file helpers);server-fetch-without-revalidateabstains on non-literal or spread options;server-sequential-independent-awaitignores auth/connect gates and already-started promises; GET handler side-effect detection follows one same-file helper hop and no longer flags read-only “mutating” route names alone.TanStack Query / Jotai — Invalidation recognizes scoped
invalidateQueriesand tRPCuse*Utils→.invalidate(); destructure-result distinguishes forwarding the whole query from field reads; raw*QueryAtomimports require envelope-shaped consumption;selectAtom/ derived-atom rules add memo, effect, handler, and reference-stability escape hatches.Next.js & client UX — Image/script rules respect JSX spreads; anchors skip
//, downloads, and_blank; passive listeners resolve handlers forpreventDefaultand explicitpassive; localStorage versioning accepts camelCaseV2keys; Preactchildrenchecks gate on component-like functions.Regression tests were added across these rules; changeset notes validation on 500 OSS repos.
Reviewed by Cursor Bugbot for commit 5b1b879. Bugbot is set up for automated code reviews on this repo. Configure here.