Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com>
14 KiB
Rule Candidates Backlog
Synthesized from a 12-cluster mining pass over ~/Developer/brain (react-dev, Kent C. Dodds,
React perf, web perf, Next/Vercel RSC, security, React Native, modern-web, Solid, deep a11y,
ui.sh components, design/motion). ~200 raw candidates → deduped, cross-corroborated, and flagged
against the current ~391-rule inventory.
Legend: corro = number of independent clusters that proposed it (higher = safer signal).
Precision: syntax | scope (needs binding/import resolution) | path (needs control flow).
Each is statically detectable with the noted false-positive risk. Already-shipped this branch is
excluded; overlaps with existing rules are flagged.
Tier S — implement next (cross-corroborated, low-FP, clearly new)
no-call-component-as-function— calling a component as a plain function (Foo(props)) instead of<Foo />; breaks hooks/identity/memoization. scope · low FP. (corro 3: react-dev, kent, react-perf)Sidebar(props)→<Sidebar {...props} />
no-async-effect-callback—useEffect(async () => …)(effect cleanup gets a Promise, races on unmount). scope · low. (corro 2: kent, solid as framework-agnostic)useEffect(async () => { await load() }, [])→ define an inner async fn and call it.
— dropped, not statically sound. width/height attrs are only one way to reserve space; CSSimg-missing-dimensionsaspect-ratio/container sizing work too and live in stylesheets a linter can't see. This is a runtime check (Lighthouseunsized-images), not a static one. (corro 3, but defeated by CSS invisibility)no-json-parse-stringify-clone—JSON.parse(JSON.stringify(x))deep clone (slow, drops Dates/Map/undefined). syntax · low. (corro 2: react-perf, web-perf) →structuredClone(x).no-create-ref-in-function-component—createRef()in a function component (new ref every render) →useRef. scope · low. (corro 2: react-dev, kent)no-set-state-in-usememo— calling a setter insideuseMemo/useCallbackfactory (side effect in render). path · low. (corro 2: react-dev, kent)no-props-mutation— assigning toprops.x/ mutating a prop object. path · low-med. (corro 2: react-dev, kent)dialog-has-accessible-name—<dialog>/role="dialog"with noaria-label/aria-labelledby. syntax · low. (corro 2: modern-web, a11y-deep)no-aria-label-on-generic-element—aria-labelon a non-interactive, role-less<div>/<span>(ignored by AT). scope · med (skip role/tabindex). (corro 1: a11y-deep) — complements shippedno-uninformative-aria-label.auth-token-in-web-storage— writing a token/JWT/secret-named value tolocalStorage/sessionStorage(XSS-exfiltratable). scope · med. (corro 1: security) — high severity.
Tier A — strong, new, low/med FP
Correctness / React
no-object-or-array-literal-as-key—key={{…}}/key={[…]}/key={obj}(new identity each render → remount). syntax · low. (kent)no-state-initialized-from-prop-without-key—useState(props.x)as the only sync (stale on prop change). scope · med. (kent; overlapsno-derived-statefamily — verify before building)no-ref-current-in-render— reading/writingref.currentduring render (not in effect/handler). path · med. (react-dev)no-module-level-mutable-in-component— reassigning a module-scopeletfrom render. scope · med. (react-dev)no-jsx-element-in-usememo—useMemo(() => <X/>, …)(memoizing elements is usually wrong). syntax · med. (kent)
Performance
no-subscription-in-render—.subscribe()/addEventListenercalled in render body. path · low. (react-perf)no-storage-access-in-render—localStorage.getItem/document.cookieread in render (sync I/O + hydration mismatch). path · med. (react-perf; corroborates web-perf)no-window-size-in-render—window.innerWidth/matchMedia().matchesin render (hydration mismatch). path · med. (react-perf, react-native web analog)no-accumulator-spread-in-reduce—arr.reduce((acc,x)=>({...acc,…}),{})O(n²). path · med. (web-perf)no-new-instance-as-prop—new Date()/new RegExp()/new Intl.*literal in JSX prop. syntax · med. (react-perf; sibling of jsx-no-new-object-as-prop)
Web platform / perf
no-document-write—document.write(…). syntax · low. (web-perf)no-sync-xhr—new XMLHttpRequest(); …open(…, false). scope · low. (web-perf)no-blocking-sync-dialog—alert()/confirm()/prompt(). syntax · med (dev usage). (web-perf)iframe-missing-lazy-loading—<iframe>withoutloading="lazy". syntax · low-med. (web-perf)no-img-lazy-with-high-fetchpriority—<img loading="lazy" fetchPriority="high">(contradiction; LCP). syntax · low. (modern-web)img-srcset-requires-sizes—<img srcset>(or<source>) withoutsizes. syntax · low. (modern-web)link-preload-missing-as/link-font-preload-missing-crossorigin—<link rel="preload">correctness. syntax · low. (web-perf)no-wildcard-namespace-import—import * as X from "<known-large pkg>"(tree-shaking-hostile; needs a pkg allowlist). scope · med. (web-perf)
Forms (modern-web + a11y, strongly corroborated)
no-type-number-for-formatted-input—<input type="number">for phone/OTP/card/zip (useinputmode+type=text). scope · med. (modern-web, a11y-deep)prefer-input-type-email-tel— email/tel field astype="text"with noinputMode. scope · med. (modern-web)password-input-requires-autocomplete—<input type="password">withoutautoComplete(current-password/new-password). syntax · med. (modern-web)no-autocomplete-off-on-identity-field—autoComplete="off"on name/email/address fields. syntax · med. (modern-web, a11y-deep)
Accessibility (beyond jsx-a11y)
role-img-requires-name—role="img"withoutaria-label. syntax · low. (a11y-deep)fieldset-requires-legend—<fieldset>without<legend>. syntax · low-med. (a11y-deep)no-filename-alt-text—alt="hero.png"/alt="IMG_1234"(filename as alt). syntax · low. (a11y-deep)no-redundant-live-region—aria-liveon arole="alert"/status(double-announce). syntax · low. (a11y-deep)
Security (mostly scan rules over config/non-linted files)
supabase-service-role-key-in-client— service-role key referenced in client code. scan · low. (security) — high severity.firebase-admin-sdk-in-client—firebase-adminimported in client bundle. scope · low. (security) — high severity.jwt-verify-unpinned-algorithm—jwt.verifywithout analgorithmsallowlist. scope · med. (security; extends existing jwt-insecure-verification)disabled-tls-certificate-validation—rejectUnauthorized: false/NODE_TLS_REJECT_UNAUTHORIZED=0. syntax · low. (security)unsafe-native-deserialization—node-serialize.unserialize/vm.runIn… (untrusted). scope · med. (security)client-controlled-open-redirect—location.href = <req/searchParams value>. path · med. (security)
Motion / design (design-deferred — verdict SHIP)
no-tailwind-will-change— staticwill-change-*class. syntax · med. (extends shipped inlineno-permanent-will-change)no-tailwind-long-duration—duration-[>1000ms]/duration-1000+ on a transition. syntax · low. (extendsno-long-transition-durationto Tailwind)no-animate-presence-child-without-key— conditional<motion.*>inside<AnimatePresence>lackingkey(exit silently breaks). scope · med. (design-deferred, react-perf)no-high-bounce-spring— framertransition={{ type:"spring", bounce:>0.3 }}on UI. scope · med. (design-deferred)no-linear-ease-on-transition—ease-linearon a non-loop transition. syntax · med (loops). (design-deferred)no-uppercase-subunit-leading—uppercase+leading-none/<1.0(cap collision on wrap). syntax · low. (design-deferred, hallmark gate 55)
ui.sh components (Tailwind/structure)
no-unshrinkable-flex-icon— icon (svg/*Icon) in aflexrow withoutshrink-0next to text. scope · med. (uidotsh ★)no-truncate-without-min-w-0—truncateon a flex child withoutmin-w-0(won't truncate). scope · med. (uidotsh)no-hover-on-non-interactive—hover:*styles on a non-interactive, role-less element. scope · med-high. (uidotsh)no-xmlns-on-inline-svg—xmlnson an inline JSX<svg>(redundant bytes). syntax · low. (uidotsh)design-no-emoji-in-jsx-text— emoji glyph as UI text/icon. syntax · med → default-off. (uidotsh, design-deferred)
React Native (Tier 1)
rn-animated-missing-use-native-driver—Animated.timing/springwithoutuseNativeDriver. scope · low. (rn)rn-no-virtualized-list-in-scrollview—<FlatList>/<FlashList>inside<ScrollView>(kills virtualization). scope · low. (rn)rn-no-animated-value-in-render—new Animated.Value()in render body (useuseRef). path · low. (rn)rn-modal-missing-on-request-close—<Modal>withoutonRequestClose(Android back). syntax · low. (rn correctness)rn-no-onpress-on-view—onPresson<View>(no-op; usePressable). syntax · low. (rn)
Tier B — Solid-dialect family (product decision)
React Doctor currently skips Solid/Qwik files (react-jsx-only tag). eslint-plugin-solid is a
ready-made, battle-tested rule set we could port behind a solid capability — a net-new surface,
not a few rules. Highest-value: solid-no-destructure-props (breaks reactivity),
solid-signal-read-uncalled-in-jsx ({count} vs {count()}), solid-no-react-specific-props
(className/htmlFor→class/for), solid-no-react-deps (passing a deps array to createEffect),
solid-prefer-for (<For> over .map), solid-no-derived-state-effect. Plus the
framework-agnostic no-async-effect-callback (already in Tier S).
Already covered / drop (dedupe catches)
no-viewport-zoom-disabled→ EXISTS (no-disabled-zoom).no-set-state-as-dom-event-handler,no-side-effects-in-reducer,no-state-mutation-in-updater→ overlapno-set-state-in-render/no-mutating-reducer-statefamily; verify before building.no-tabular-nums-on-numeric-cell,no-nonconcentric-nested-radius→ need cell-type/geometry; default-off at best.next-image-dangerously-allow-svg,graphql-introspection-enabled-in-prod,cors-credentialed-origin-reflection→ niche security scan; lower priority.
Skip — too noisy / not statically sound (from the deferred pass)
no-named-line-height (leading-tight is often fine), no-inline-static-style, no-arbitrary-over-bare-value (z-[999]→z-999), no-straight-quotes-in-jsx (apostrophes), no-cliche-purple-gradient (opinionated; default-off only), no-uniform-hover-scale (cross-element, not per-element), heading-order (~50% static precision).
Recommended next implementation batch (highest signal × lowest noise) — SHIPPED (7)
no-call-component-as-function, no-async-effect-callback, no-json-parse-stringify-clone,
no-create-ref-in-function-component, dialog-has-accessible-name, auth-token-in-web-storage,
no-img-lazy-with-high-fetchpriority — all syntax/scope-only, low-FP, each grounded in ≥1 (mostly
≥2) sources and orthogonal to the existing rules. Implemented with adversarial tests + FP-regression
coverage (incl. scope-resolution shadow-safety) on branch add-mined-correctness-perf-a11y-rules.
Dropped from the batch: img-missing-dimensions — a static linter cannot see CSS, and width/
height attributes are only one of several valid ways to reserve space (CSS aspect-ratio,
container sizing, CSS width/height all work). Lighthouse's unsized-images audit makes this call at
runtime against computed styles; a syntax rule would false-positive on every image sized by an
external/global stylesheet. Moved to the not-statically-sound list below.
Mining batch 2 — SHIPPED (3) + evidence-based drops
Each shipped rule passed an OSS noise sweep (react-use, radix-ui/primitives, excalidraw, mantine; ~2,800 diagnostics): 0 false positives.
no-document-write(Performance) —document.write()/writeln().no-sync-xhr(Performance) —.open(method, url, false)synchronous XHR.no-string-false-on-boolean-attribute(Bugs) —disabled="false"etc. (truthy string applies the boolean). Curated boolean-attr set on intrinsic elements only; excludesaria-*/enumerated.
Dropped after the sweep (real but too noisy / FP-prone):
no-accumulator-spread-in-reduce— 29 hits across the 4 repos, all idiomatic small-data style merges ([...styles].reduce((acc,i)=>({...acc,...}))). The pattern is genuinely O(n^2) but a static rule can't tell a 3-item merge from a 10k-row one, so default-on is too noisy. Revisit as opt-in / default-off with a large-source heuristic.no-redundant-live-region— both real-world hits were false positives: an intentionalrole="status"+ dynamicaria-liveoverride (radix toast) and a defensive explicitaria-live="polite"(mantine carousel). axe does not flag this pattern; the "redundant" framing is contentious. Dropped.
Raw per-cluster candidate detail (bad/good for all ~200) was produced in
/tmp/rd-mine/*.md(ephemeral). Ask to persist any cluster's full detail into the repo if needed.