oxlint-plugin-react-doctor 0.9.2-dev.d81eeda → 0.9.2-dev.de6d280
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{A as e,C as t,D as n,Dt as r,E as i,Et as a,F as o,G as s,I as c,J as l,K as u,L as d,Lt as f,M as p,N as m,O as h,Ot as g,P as _,R as v,Rt as y,S as b,T as x,W as S,Y as C,_ as w,a as T,aa as ee,b as E,c as D,d as O,di as k,f as A,g as j,h as M,i as N,ia as P,j as F,k as I,l as L,li as R,n as te,o as z,oa as ne,p as B,q as V,r as H,ra as U,s as W,sa as G,t as K,u as q,ui as J,v as Y,w as X,x as Z,y as Q,zi as re,zt as ie}from"./capability-DEny-1Qf.js";var ae=[{key:`react-doctor/active-static-asset`,id:`active-static-asset`,source:`react-doctor`,originallyExternal:!1,rule:{id:`active-static-asset`,title:`Executable SVG exposure`,severity:`warn`,recommendation:"Prefer `<img>` for SVG images; if SVG must be served directly, use attachment disposition and a CSP that blocks scripts and objects.",category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/activity-wraps-effect-heavy-subtree`,id:`activity-wraps-effect-heavy-subtree`,source:`react-doctor`,originallyExternal:!1,rule:{id:`activity-wraps-effect-heavy-subtree`,title:`Activity wraps an effect-heavy subtree`,severity:`warn`,recommendation:"Check what's under `<Activity>`. Every hide and show rebuilds every Effect inside from scratch. Move subscriptions and effect-driven setState out of the Activity, or load the data above it.",category:`Bugs`,framework:`global`,requires:[`react`,`react:19.2`],isScanRule:!1}},{key:`react-doctor/advanced-event-handler-refs`,id:`advanced-event-handler-refs`,source:`react-doctor`,originallyExternal:!1,rule:{id:`advanced-event-handler-refs`,title:`Listener re-subscribes on every handler change`,severity:`warn`,recommendation:"Store the handler in a ref and have the listener read `handlerRef.current()`. The subscription stays put while the latest handler still runs.",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/agent-tool-capability-risk`,id:`agent-tool-capability-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`agent-tool-capability-risk`,title:`Agent tool exposes dangerous capability`,severity:`warn`,recommendation:`Treat tool inputs as prompt-injection controlled. Validate arguments, scope permissions per call, and avoid exposing shell/file/network primitives directly to agents.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/alt-text`,id:`alt-text`,source:`react-doctor`,originallyExternal:!0,rule:{id:`alt-text`,title:`Image missing alt text`,severity:`error`,recommendation:"Give every meaningful image an `alt`, `aria-label`, or `aria-labelledby`.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-ambiguous-text`,id:`anchor-ambiguous-text`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-ambiguous-text`,title:`Ambiguous link text`,severity:`warn`,recommendation:`Name where a link goes. Avoid 'click here', 'learn more', and 'link'.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-has-content`,id:`anchor-has-content`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-has-content`,title:`Anchor has no content`,severity:`warn`,recommendation:`Put readable text inside every link.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-is-valid`,id:`anchor-is-valid`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-is-valid`,title:`Anchor used as a button`,severity:`warn`,recommendation:"Give links a real destination. Use `<button>` for in-page actions.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-activedescendant-has-tabindex`,id:`aria-activedescendant-has-tabindex`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-activedescendant-has-tabindex`,title:`aria-activedescendant missing tabindex`,severity:`warn`,recommendation:"Add `tabIndex` so keyboard users can reach elements that use `aria-activedescendant`.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-braille-equivalent`,id:`aria-braille-equivalent`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-braille-equivalent`,title:`Braille-only accessible description`,severity:`warn`,recommendation:`Pair braille-specific labels and role descriptions with an equivalent accessible name or role description.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/aria-props`,id:`aria-props`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-props`,title:`Invalid ARIA attribute`,severity:`error`,recommendation:"Only use `aria-*` attributes that actually exist.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-proptypes`,id:`aria-proptypes`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-proptypes`,title:`Invalid ARIA attribute value`,severity:`error`,recommendation:"Give each `aria-*` attribute the kind of value it expects.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-role`,id:`aria-role`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-role`,title:`Invalid ARIA role`,severity:`error`,recommendation:`Use a real, non-abstract ARIA role so assistive tech can expose the element correctly.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-unsupported-elements`,id:`aria-unsupported-elements`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-unsupported-elements`,title:`ARIA on unsupported element`,severity:`error`,recommendation:"Do not put `role` or `aria-*` on reserved HTML elements.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/artifact-baas-authority-surface`,id:`artifact-baas-authority-surface`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-baas-authority-surface`,title:`BaaS authority map shipped in browser artifact`,severity:`warn`,recommendation:`Client BaaS config is often public, but shipped collection names plus owner, role, tenant, or admin fields give attackers a precise authorization map. Verify rules/RLS enforce every boundary server-side.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/artifact-env-leak`,id:`artifact-env-leak`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-env-leak`,title:`Server env leaked to browser artifact`,severity:`error`,recommendation:`Treat public env prefixes as publication, not secrecy; keep secret env vars server-only and rebuild after rotating leaked keys.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/artifact-secret-leak`,id:`artifact-secret-leak`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-secret-leak`,title:`Secret shipped in browser artifact`,severity:`error`,recommendation:`Remove the secret from client bundles/static assets, rotate it, and route privileged service calls through server-only code.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/async-await-in-loop`,id:`async-await-in-loop`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-await-in-loop`,title:`await inside a loop`,severity:`warn`,recommendation:"Collect the items, then use `await Promise.all(items.map(...))` so independent work runs at the same time",category:`Performance`,framework:`global`,tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/async-defer-await`,id:`async-defer-await`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-defer-await`,title:`await before an early-return guard`,severity:`warn`,recommendation:"Move the `await` below the early-return guard so the skip path stays fast and avoids unnecessary async work.",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/async-parallel`,id:`async-parallel`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-parallel`,title:`Independent awaits run sequentially`,severity:`warn`,recommendation:"Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` so independent calls run at the same time",category:`Performance`,framework:`global`,tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/auth-token-in-web-storage`,id:`auth-token-in-web-storage`,source:`react-doctor`,originallyExternal:!1,rule:{id:`auth-token-in-web-storage`,title:`Auth token in web storage`,severity:`warn`,recommendation:"Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",category:`Security`,framework:`global`,isScanRule:!1}},{key:`react-doctor/autocomplete-valid`,id:`autocomplete-valid`,source:`react-doctor`,originallyExternal:!0,rule:{id:`autocomplete-valid`,title:`Invalid autocomplete value`,severity:`warn`,recommendation:"Use a valid autofill token in `autoComplete` so browsers can fill the right field reliably.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/build-pipeline-secret-boundary`,id:`build-pipeline-secret-boundary`,source:`react-doctor`,originallyExternal:!1,rule:{id:`build-pipeline-secret-boundary`,title:`Build pipeline runs code near secrets`,severity:`warn`,recommendation:`Run dependency installs with scripts disabled before exposing secrets, isolate untrusted build code, and move signing/deploy authority into a narrow privileged step.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/button-has-type`,id:`button-has-type`,source:`react-doctor`,originallyExternal:!0,rule:{id:`button-has-type`,title:`Button missing explicit type`,severity:`warn`,recommendation:'Set an explicit button `type` so plain buttons do not submit forms by accident: `type="button"`, `"submit"`, or `"reset"`.',category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/checked-requires-onchange-or-readonly`,id:`checked-requires-onchange-or-readonly`,source:`react-doctor`,originallyExternal:!0,rule:{id:`checked-requires-onchange-or-readonly`,title:`Checked input without onChange`,severity:`warn`,recommendation:"Add `onChange`, `readOnly`, or `defaultChecked` so React knows whether the checkbox is editable, display-only, or uncontrolled.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/class-component-missing-component-will-unmount-teardown`,id:`class-component-missing-component-will-unmount-teardown`,source:`react-doctor`,originallyExternal:!1,rule:{id:`class-component-missing-component-will-unmount-teardown`,title:`Class component acquires a resource with no teardown`,severity:`warn`,recommendation:"Release listeners and timers acquired in `componentDidMount`/`constructor` by adding a `componentWillUnmount` that removes them (or use MobX `disposeOnUnmount`).",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/click-events-have-key-events`,id:`click-events-have-key-events`,source:`react-doctor`,originallyExternal:!0,rule:{id:`click-events-have-key-events`,title:`Click handler missing keyboard handler`,severity:`warn`,recommendation:"Pair `onClick` with a key handler so keyboard users can trigger it.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/clickjacking-redirect-risk`,id:`clickjacking-redirect-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`clickjacking-redirect-risk`,title:`Redirect or frame boundary risk`,severity:`warn`,recommendation:"Allowlist redirect origins/paths, set `frame-ancestors` for privileged pages, and avoid URL-prefilled privileged dialogs.",category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/client-localstorage-no-version`,id:`client-localstorage-no-version`,source:`react-doctor`,originallyExternal:!1,rule:{id:`client-localstorage-no-version`,title:`Unversioned localStorage key`,severity:`warn`,recommendation:`Put a version in the storage key (e.g. "myKey:v1"). If you change the data shape later, old saved data can be ignored instead of crashing the app.`,category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/client-passive-event-listeners`,id:`client-passive-event-listeners`,source:`react-doctor`,originallyExternal:!1,rule:{id:`client-passive-event-listeners`,title:`Non-passive scroll listener`,severity:`warn`,recommendation:"Add `{ passive: true }` as the third argument: `addEventListener('touchmove', handler, { passive: true })`. Only do this if the handler doesn't call `event.preventDefault()`, since passive listeners ignore it (which breaks pull-to-refresh, custom gestures, and nested scrolling).",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/command-execution-input-risk`,id:`command-execution-input-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`command-execution-input-risk`,title:`Command execution uses caller-shaped input`,severity:`error`,recommendation:`Avoid shell execution for caller-controlled values. Use fixed commands, argument arrays, strict allowlists, and no shell interpolation.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/context-provider-value-from-unmemoized-local-literal`,id:`context-provider-value-from-unmemoized-local-literal`,source:`react-doctor`,originallyExternal:!1,rule:{id:`context-provider-value-from-unmemoized-local-literal`,title:`Context value from an unmemoized local literal`,severity:`warn`,recommendation:`Memoize the context value in component scope so consumers do not redraw every render. For mapped providers, extract each item into a child component and memoize there.`,category:`Performance`,framework:`global`,requires:[`react`],disabledWhen:[`react-compiler`],tags:[`react-jsx-only`,`test-noise`],isScanRule:!1}},{key:`react-doctor/control-has-associated-label`,id:`control-has-associated-label`,source:`react-doctor`,originallyExternal:!0,rule:{id:`control-has-associated-label`,title:`Control missing accessible label`,severity:`warn`,recommendation:`Give every interactive control a label screen readers can read.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/cors-cookie-trust-risk`,id:`cors-cookie-trust-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`cors-cookie-trust-risk`,title:`Broad cookie or credentialed CORS trust`,severity:`warn`,recommendation:`Keep auth cookies host-only and HttpOnly, avoid credentialed CORS for less-trusted docs/vendor origins, and isolate documentation domains from app sessions.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/dangerous-html-sink`,id:`dangerous-html-sink`,source:`react-doctor`,originallyExternal:!1,rule:{id:`dangerous-html-sink`,title:`HTML injection sink with dynamic content`,severity:`warn`,recommendation:`Prefer rendering structured React nodes. If HTML is required, sanitize with a well-reviewed sanitizer and keep the trust boundary close to the sink.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/data-table-requires-accessible-name`,id:`data-table-requires-accessible-name`,source:`react-doctor`,originallyExternal:!1,rule:{id:`data-table-requires-accessible-name`,title:`Data table has no accessible name`,severity:`warn`,recommendation:`Give data tables a concise caption, or reference an existing visible title with aria-labelledby.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/debounce-no-cleanup`,id:`debounce-no-cleanup`,source:`react-doctor`,originallyExternal:!1,rule:{id:`debounce-no-cleanup`,title:`Memoized debounce never cancelled on unmount`,severity:`warn`,recommendation:"A debounced/throttled callback holds a pending timer that still fires after unmount, so add `useEffect(() => () => debounced.cancel(), [debounced])` to cancel the trailing invocation when the component tears down.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/design-no-em-dash-in-jsx-text`,id:`design-no-em-dash-in-jsx-text`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-em-dash-in-jsx-text`,title:`Em dash in JSX text`,severity:`warn`,recommendation:`Replace em dashes in UI text with commas, colons, semicolons, or parentheses so the copy reads less like AI output.`,category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-redundant-padding-axes`,id:`design-no-redundant-padding-axes`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-redundant-padding-axes`,title:`Redundant padding axes`,severity:`warn`,recommendation:"Collapse matching padding axes to `p-N` so duplicated classes do not make spacing harder to scan; keep split axes only when breakpoints differ.",category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-redundant-size-axes`,id:`design-no-redundant-size-axes`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-redundant-size-axes`,title:`Redundant width and height axes`,severity:`warn`,recommendation:"Collapse matching width and height to `size-N` so duplicated classes do not make layout harder to scan.",category:`Maintainability`,framework:`global`,requires:[`react`,`tailwind:3.4`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-space-on-flex-children`,id:`design-no-space-on-flex-children`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-space-on-flex-children`,title:`space-* utility on flex children`,severity:`warn`,recommendation:"Use `gap-*` on the flex or grid parent. `space-x-*` and `space-y-*` leave gaps when a child is hidden, miss spacing on wrapped lines, and don't flip in right-to-left layouts.",category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-three-period-ellipsis`,id:`design-no-three-period-ellipsis`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-three-period-ellipsis`,title:`Three dots instead of ellipsis`,severity:`warn`,recommendation:`Use the real ellipsis character ("…") so UI labels look polished and consistent instead of like three separate periods.`,category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-vague-button-label`,id:`design-no-vague-button-label`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-vague-button-label`,title:`Vague button label`,severity:`warn`,recommendation:`Name the action: "Save changes" instead of "Continue", "Send invite" instead of "Submit". The label is the button's accessible name.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/details-requires-summary`,id:`details-requires-summary`,source:`react-doctor`,originallyExternal:!1,rule:{id:`details-requires-summary`,title:`Details disclosure has no explicit summary`,severity:`warn`,recommendation:`Make a meaningful summary the first child of details so users can understand and operate the disclosure.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/dialog-has-accessible-name`,id:`dialog-has-accessible-name`,source:`react-doctor`,originallyExternal:!1,rule:{id:`dialog-has-accessible-name`,title:`Dialog without accessible name`,severity:`warn`,recommendation:'Give every `<dialog>` / `role="dialog"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog\'s title element).',category:`Accessibility`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/display-name`,id:`display-name`,source:`react-doctor`,originallyExternal:!0,rule:{id:`display-name`,title:`Component missing display name`,severity:`warn`,recommendation:"Give each component a `displayName` so DevTools shows a clear name.",category:`Maintainability`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/effect-listener-cleanup-mismatch`,id:`effect-listener-cleanup-mismatch`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-listener-cleanup-mismatch`,title:`Effect cleanup does not match its event listener`,severity:`error`,recommendation:"Pass the same callback binding and capture flag to `addEventListener` and `removeEventListener`, or abort the registration's local AbortController during cleanup.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-listener-cleanup-reference-mismatch`,id:`effect-listener-cleanup-reference-mismatch`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-listener-cleanup-reference-mismatch`,title:`Effect cleanup removes the wrong listener reference`,severity:`error`,recommendation:`Removal APIs match by reference identity, so the second inline function passed to the remove call can never equal the one you added; hoist the handler into a single named const (or useCallback) and pass that same reference to both the add and remove calls.`,category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-needs-cleanup`,id:`effect-needs-cleanup`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-needs-cleanup`,title:`Effect subscription or timer never cleaned up`,severity:`error`,recommendation:"Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/effect-observer-needs-disconnect`,id:`effect-observer-needs-disconnect`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-observer-needs-disconnect`,title:`Observer created in an effect never disconnected`,severity:`error`,recommendation:"Return a cleanup function that calls `observer.disconnect()` (or `observer.unobserve(node)`) so the observer stops firing callbacks against detached nodes after unmount instead of leaking on every mount.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-raf-loop-needs-cancel`,id:`effect-raf-loop-needs-cancel`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-raf-loop-needs-cancel`,title:`requestAnimationFrame loop never cancelled`,severity:`warn`,recommendation:"Store the frame id and return a cleanup that calls `cancelAnimationFrame(id)` so self-scheduling work cannot continue after unmount.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-remove-listener-inline-handler`,id:`effect-remove-listener-inline-handler`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-remove-listener-inline-handler`,title:`removeEventListener called with a fresh inline handler`,severity:`error`,recommendation:"Removal APIs match the listener by reference equality, so a fresh inline arrow, function expression, or `.bind(...)` result can never equal the registered handler; hoist the handler into a named const and pass that same reference to both the add and remove calls.",category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/empty-table-header`,id:`empty-table-header`,source:`react-doctor`,originallyExternal:!0,rule:{id:`empty-table-header`,title:`Table header has no accessible text`,severity:`warn`,recommendation:`Give every table header concise text or an explicit accessible name.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/exhaustive-deps`,id:`exhaustive-deps`,source:`react-doctor`,originallyExternal:!0,rule:{id:`exhaustive-deps`,title:`Missing effect dependencies`,severity:`warn`,recommendation:`Don't blindly add missing dependencies. Read the hook callback first.
|
|
1
|
+
import{A as e,C as t,D as n,Dt as r,E as i,Et as a,F as o,G as s,I as c,J as l,K as u,L as d,Lt as f,M as p,N as m,O as h,Ot as g,P as _,R as v,Rt as y,S as b,T as x,W as S,Y as C,_ as w,a as T,aa as ee,b as E,c as D,d as O,di as k,f as A,g as j,h as M,i as N,ia as P,j as F,k as I,l as L,li as R,n as te,o as z,oa as ne,p as B,q as V,r as H,ra as U,s as W,sa as G,t as K,u as q,ui as J,v as Y,w as X,x as Z,y as Q,zi as re,zt as ie}from"./capability-CGSnrcnu.js";var ae=[{key:`react-doctor/active-static-asset`,id:`active-static-asset`,source:`react-doctor`,originallyExternal:!1,rule:{id:`active-static-asset`,title:`Executable SVG exposure`,severity:`warn`,recommendation:"Prefer `<img>` for SVG images; if SVG must be served directly, use attachment disposition and a CSP that blocks scripts and objects.",category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/activity-wraps-effect-heavy-subtree`,id:`activity-wraps-effect-heavy-subtree`,source:`react-doctor`,originallyExternal:!1,rule:{id:`activity-wraps-effect-heavy-subtree`,title:`Activity wraps an effect-heavy subtree`,severity:`warn`,recommendation:"Check what's under `<Activity>`. Every hide and show rebuilds every Effect inside from scratch. Move subscriptions and effect-driven setState out of the Activity, or load the data above it.",category:`Bugs`,framework:`global`,requires:[`react`,`react:19.2`],isScanRule:!1}},{key:`react-doctor/advanced-event-handler-refs`,id:`advanced-event-handler-refs`,source:`react-doctor`,originallyExternal:!1,rule:{id:`advanced-event-handler-refs`,title:`Listener re-subscribes on every handler change`,severity:`warn`,recommendation:"Store the handler in a ref and have the listener read `handlerRef.current()`. The subscription stays put while the latest handler still runs.",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/agent-tool-capability-risk`,id:`agent-tool-capability-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`agent-tool-capability-risk`,title:`Agent tool exposes dangerous capability`,severity:`warn`,recommendation:`Treat tool inputs as prompt-injection controlled. Validate arguments, scope permissions per call, and avoid exposing shell/file/network primitives directly to agents.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/alt-text`,id:`alt-text`,source:`react-doctor`,originallyExternal:!0,rule:{id:`alt-text`,title:`Image missing alt text`,severity:`error`,recommendation:"Give every meaningful image an `alt`, `aria-label`, or `aria-labelledby`.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-ambiguous-text`,id:`anchor-ambiguous-text`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-ambiguous-text`,title:`Ambiguous link text`,severity:`warn`,recommendation:`Name where a link goes. Avoid 'click here', 'learn more', and 'link'.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-has-content`,id:`anchor-has-content`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-has-content`,title:`Anchor has no content`,severity:`warn`,recommendation:`Put readable text inside every link.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-is-valid`,id:`anchor-is-valid`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-is-valid`,title:`Anchor used as a button`,severity:`warn`,recommendation:"Give links a real destination. Use `<button>` for in-page actions.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-activedescendant-has-tabindex`,id:`aria-activedescendant-has-tabindex`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-activedescendant-has-tabindex`,title:`aria-activedescendant missing tabindex`,severity:`warn`,recommendation:"Add `tabIndex` so keyboard users can reach elements that use `aria-activedescendant`.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-braille-equivalent`,id:`aria-braille-equivalent`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-braille-equivalent`,title:`Braille-only accessible description`,severity:`warn`,recommendation:`Pair braille-specific labels and role descriptions with an equivalent accessible name or role description.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/aria-props`,id:`aria-props`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-props`,title:`Invalid ARIA attribute`,severity:`error`,recommendation:"Only use `aria-*` attributes that actually exist.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-proptypes`,id:`aria-proptypes`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-proptypes`,title:`Invalid ARIA attribute value`,severity:`error`,recommendation:"Give each `aria-*` attribute the kind of value it expects.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-role`,id:`aria-role`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-role`,title:`Invalid ARIA role`,severity:`error`,recommendation:`Use a real, non-abstract ARIA role so assistive tech can expose the element correctly.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-unsupported-elements`,id:`aria-unsupported-elements`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-unsupported-elements`,title:`ARIA on unsupported element`,severity:`error`,recommendation:"Do not put `role` or `aria-*` on reserved HTML elements.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/artifact-baas-authority-surface`,id:`artifact-baas-authority-surface`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-baas-authority-surface`,title:`BaaS authority map shipped in browser artifact`,severity:`warn`,recommendation:`Client BaaS config is often public, but shipped collection names plus owner, role, tenant, or admin fields give attackers a precise authorization map. Verify rules/RLS enforce every boundary server-side.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/artifact-env-leak`,id:`artifact-env-leak`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-env-leak`,title:`Server env leaked to browser artifact`,severity:`error`,recommendation:`Treat public env prefixes as publication, not secrecy; keep secret env vars server-only and rebuild after rotating leaked keys.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/artifact-secret-leak`,id:`artifact-secret-leak`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-secret-leak`,title:`Secret shipped in browser artifact`,severity:`error`,recommendation:`Remove the secret from client bundles/static assets, rotate it, and route privileged service calls through server-only code.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/async-await-in-loop`,id:`async-await-in-loop`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-await-in-loop`,title:`await inside a loop`,severity:`warn`,recommendation:"Collect the items, then use `await Promise.all(items.map(...))` so independent work runs at the same time",category:`Performance`,framework:`global`,tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/async-defer-await`,id:`async-defer-await`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-defer-await`,title:`await before an early-return guard`,severity:`warn`,recommendation:"Move the `await` below the early-return guard so the skip path stays fast and avoids unnecessary async work.",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/async-parallel`,id:`async-parallel`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-parallel`,title:`Independent awaits run sequentially`,severity:`warn`,recommendation:"Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` so independent calls run at the same time",category:`Performance`,framework:`global`,tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/auth-token-in-web-storage`,id:`auth-token-in-web-storage`,source:`react-doctor`,originallyExternal:!1,rule:{id:`auth-token-in-web-storage`,title:`Auth token in web storage`,severity:`warn`,recommendation:"Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",category:`Security`,framework:`global`,isScanRule:!1}},{key:`react-doctor/autocomplete-valid`,id:`autocomplete-valid`,source:`react-doctor`,originallyExternal:!0,rule:{id:`autocomplete-valid`,title:`Invalid autocomplete value`,severity:`warn`,recommendation:"Use a valid autofill token in `autoComplete` so browsers can fill the right field reliably.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/build-pipeline-secret-boundary`,id:`build-pipeline-secret-boundary`,source:`react-doctor`,originallyExternal:!1,rule:{id:`build-pipeline-secret-boundary`,title:`Build pipeline runs code near secrets`,severity:`warn`,recommendation:`Run dependency installs with scripts disabled before exposing secrets, isolate untrusted build code, and move signing/deploy authority into a narrow privileged step.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/button-has-type`,id:`button-has-type`,source:`react-doctor`,originallyExternal:!0,rule:{id:`button-has-type`,title:`Button missing explicit type`,severity:`warn`,recommendation:'Set an explicit button `type` so plain buttons do not submit forms by accident: `type="button"`, `"submit"`, or `"reset"`.',category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/checked-requires-onchange-or-readonly`,id:`checked-requires-onchange-or-readonly`,source:`react-doctor`,originallyExternal:!0,rule:{id:`checked-requires-onchange-or-readonly`,title:`Checked input without onChange`,severity:`warn`,recommendation:"Add `onChange`, `readOnly`, or `defaultChecked` so React knows whether the checkbox is editable, display-only, or uncontrolled.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/class-component-missing-component-will-unmount-teardown`,id:`class-component-missing-component-will-unmount-teardown`,source:`react-doctor`,originallyExternal:!1,rule:{id:`class-component-missing-component-will-unmount-teardown`,title:`Class component acquires a resource with no teardown`,severity:`warn`,recommendation:"Release listeners and timers acquired in `componentDidMount`/`constructor` by adding a `componentWillUnmount` that removes them (or use MobX `disposeOnUnmount`).",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/click-events-have-key-events`,id:`click-events-have-key-events`,source:`react-doctor`,originallyExternal:!0,rule:{id:`click-events-have-key-events`,title:`Click handler missing keyboard handler`,severity:`warn`,recommendation:"Pair `onClick` with a key handler so keyboard users can trigger it.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/clickjacking-redirect-risk`,id:`clickjacking-redirect-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`clickjacking-redirect-risk`,title:`Redirect or frame boundary risk`,severity:`warn`,recommendation:"Allowlist redirect origins/paths, set `frame-ancestors` for privileged pages, and avoid URL-prefilled privileged dialogs.",category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/client-localstorage-no-version`,id:`client-localstorage-no-version`,source:`react-doctor`,originallyExternal:!1,rule:{id:`client-localstorage-no-version`,title:`Unversioned localStorage key`,severity:`warn`,recommendation:`Put a version in the storage key (e.g. "myKey:v1"). If you change the data shape later, old saved data can be ignored instead of crashing the app.`,category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/client-passive-event-listeners`,id:`client-passive-event-listeners`,source:`react-doctor`,originallyExternal:!1,rule:{id:`client-passive-event-listeners`,title:`Non-passive scroll listener`,severity:`warn`,recommendation:"Add `{ passive: true }` as the third argument: `addEventListener('touchmove', handler, { passive: true })`. Only do this if the handler doesn't call `event.preventDefault()`, since passive listeners ignore it (which breaks pull-to-refresh, custom gestures, and nested scrolling).",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/command-execution-input-risk`,id:`command-execution-input-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`command-execution-input-risk`,title:`Command execution uses caller-shaped input`,severity:`error`,recommendation:`Avoid shell execution for caller-controlled values. Use fixed commands, argument arrays, strict allowlists, and no shell interpolation.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/context-provider-value-from-unmemoized-local-literal`,id:`context-provider-value-from-unmemoized-local-literal`,source:`react-doctor`,originallyExternal:!1,rule:{id:`context-provider-value-from-unmemoized-local-literal`,title:`Context value from an unmemoized local literal`,severity:`warn`,recommendation:`Memoize the context value in component scope so consumers do not redraw every render. For mapped providers, extract each item into a child component and memoize there.`,category:`Performance`,framework:`global`,requires:[`react`],disabledWhen:[`react-compiler`],tags:[`react-jsx-only`,`test-noise`],isScanRule:!1}},{key:`react-doctor/control-has-associated-label`,id:`control-has-associated-label`,source:`react-doctor`,originallyExternal:!0,rule:{id:`control-has-associated-label`,title:`Control missing accessible label`,severity:`warn`,recommendation:`Give every interactive control a label screen readers can read.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/cors-cookie-trust-risk`,id:`cors-cookie-trust-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`cors-cookie-trust-risk`,title:`Broad cookie or credentialed CORS trust`,severity:`warn`,recommendation:`Keep auth cookies host-only and HttpOnly, avoid credentialed CORS for less-trusted docs/vendor origins, and isolate documentation domains from app sessions.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/dangerous-html-sink`,id:`dangerous-html-sink`,source:`react-doctor`,originallyExternal:!1,rule:{id:`dangerous-html-sink`,title:`HTML injection sink with dynamic content`,severity:`warn`,recommendation:`Prefer rendering structured React nodes. If HTML is required, sanitize with a well-reviewed sanitizer and keep the trust boundary close to the sink.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/data-table-requires-accessible-name`,id:`data-table-requires-accessible-name`,source:`react-doctor`,originallyExternal:!1,rule:{id:`data-table-requires-accessible-name`,title:`Data table has no accessible name`,severity:`warn`,recommendation:`Give data tables a concise caption, or reference an existing visible title with aria-labelledby.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/debounce-no-cleanup`,id:`debounce-no-cleanup`,source:`react-doctor`,originallyExternal:!1,rule:{id:`debounce-no-cleanup`,title:`Memoized debounce never cancelled on unmount`,severity:`warn`,recommendation:"A debounced/throttled callback holds a pending timer that still fires after unmount, so add `useEffect(() => () => debounced.cancel(), [debounced])` to cancel the trailing invocation when the component tears down.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/design-no-em-dash-in-jsx-text`,id:`design-no-em-dash-in-jsx-text`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-em-dash-in-jsx-text`,title:`Em dash in JSX text`,severity:`warn`,recommendation:`Replace em dashes in UI text with commas, colons, semicolons, or parentheses so the copy reads less like AI output.`,category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-redundant-padding-axes`,id:`design-no-redundant-padding-axes`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-redundant-padding-axes`,title:`Redundant padding axes`,severity:`warn`,recommendation:"Collapse matching padding axes to `p-N` so duplicated classes do not make spacing harder to scan; keep split axes only when breakpoints differ.",category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-redundant-size-axes`,id:`design-no-redundant-size-axes`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-redundant-size-axes`,title:`Redundant width and height axes`,severity:`warn`,recommendation:"Collapse matching width and height to `size-N` so duplicated classes do not make layout harder to scan.",category:`Maintainability`,framework:`global`,requires:[`react`,`tailwind:3.4`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-space-on-flex-children`,id:`design-no-space-on-flex-children`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-space-on-flex-children`,title:`space-* utility on flex children`,severity:`warn`,recommendation:"Use `gap-*` on the flex or grid parent. `space-x-*` and `space-y-*` leave gaps when a child is hidden, miss spacing on wrapped lines, and don't flip in right-to-left layouts.",category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-three-period-ellipsis`,id:`design-no-three-period-ellipsis`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-three-period-ellipsis`,title:`Three dots instead of ellipsis`,severity:`warn`,recommendation:`Use the real ellipsis character ("…") so UI labels look polished and consistent instead of like three separate periods.`,category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-vague-button-label`,id:`design-no-vague-button-label`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-vague-button-label`,title:`Vague button label`,severity:`warn`,recommendation:`Name the action: "Save changes" instead of "Continue", "Send invite" instead of "Submit". The label is the button's accessible name.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/details-requires-summary`,id:`details-requires-summary`,source:`react-doctor`,originallyExternal:!1,rule:{id:`details-requires-summary`,title:`Details disclosure has no explicit summary`,severity:`warn`,recommendation:`Make a meaningful summary the first child of details so users can understand and operate the disclosure.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/dialog-has-accessible-name`,id:`dialog-has-accessible-name`,source:`react-doctor`,originallyExternal:!1,rule:{id:`dialog-has-accessible-name`,title:`Dialog without accessible name`,severity:`warn`,recommendation:'Give every `<dialog>` / `role="dialog"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog\'s title element).',category:`Accessibility`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/display-name`,id:`display-name`,source:`react-doctor`,originallyExternal:!0,rule:{id:`display-name`,title:`Component missing display name`,severity:`warn`,recommendation:"Give each component a `displayName` so DevTools shows a clear name.",category:`Maintainability`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/effect-listener-cleanup-mismatch`,id:`effect-listener-cleanup-mismatch`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-listener-cleanup-mismatch`,title:`Effect cleanup does not match its event listener`,severity:`error`,recommendation:"Pass the same callback binding and capture flag to `addEventListener` and `removeEventListener`, or abort the registration's local AbortController during cleanup.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-listener-cleanup-reference-mismatch`,id:`effect-listener-cleanup-reference-mismatch`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-listener-cleanup-reference-mismatch`,title:`Effect cleanup removes the wrong listener reference`,severity:`error`,recommendation:`Removal APIs match by reference identity, so the second inline function passed to the remove call can never equal the one you added; hoist the handler into a single named const (or useCallback) and pass that same reference to both the add and remove calls.`,category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-needs-cleanup`,id:`effect-needs-cleanup`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-needs-cleanup`,title:`Effect subscription or timer never cleaned up`,severity:`error`,recommendation:"Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/effect-observer-needs-disconnect`,id:`effect-observer-needs-disconnect`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-observer-needs-disconnect`,title:`Observer created in an effect never disconnected`,severity:`error`,recommendation:"Return a cleanup function that calls `observer.disconnect()` (or `observer.unobserve(node)`) so the observer stops firing callbacks against detached nodes after unmount instead of leaking on every mount.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-raf-loop-needs-cancel`,id:`effect-raf-loop-needs-cancel`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-raf-loop-needs-cancel`,title:`requestAnimationFrame loop never cancelled`,severity:`warn`,recommendation:"Store the frame id and return a cleanup that calls `cancelAnimationFrame(id)` so self-scheduling work cannot continue after unmount.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-remove-listener-inline-handler`,id:`effect-remove-listener-inline-handler`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-remove-listener-inline-handler`,title:`removeEventListener called with a fresh inline handler`,severity:`error`,recommendation:"Removal APIs match the listener by reference equality, so a fresh inline arrow, function expression, or `.bind(...)` result can never equal the registered handler; hoist the handler into a named const and pass that same reference to both the add and remove calls.",category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/empty-table-header`,id:`empty-table-header`,source:`react-doctor`,originallyExternal:!0,rule:{id:`empty-table-header`,title:`Table header has no accessible text`,severity:`warn`,recommendation:`Give every table header concise text or an explicit accessible name.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/exhaustive-deps`,id:`exhaustive-deps`,source:`react-doctor`,originallyExternal:!0,rule:{id:`exhaustive-deps`,title:`Missing effect dependencies`,severity:`warn`,recommendation:`Don't blindly add missing dependencies. Read the hook callback first.
|
|
2
2
|
|
|
3
3
|
Bad:
|
|
4
4
|
useEffect(() => {
|