Adversarially fuzz React Doctor rules with @react-doctor/fuzz — crash, slowness, and metamorphic-invariance oracles over generated, pathological, and real-world programs. Use after rule tests pass, when hunting false positives, when triaging a fuzz finding, or whenever a NEW false positive is confirmed from any source (user report, RDE eval, react-bench run, review) so it gets added to the evolving regression corpus.
rule-validate:rule-research defines the rule contract.rule-writing turns the contract into tests and implementation.rule-validate verifies noise, correctness, changesets, PR copy.fuzz stress-tests the implementation mechanically and keeps a
permanent, growing memory of every false positive ever confirmed.packages/fuzz (@react-doctor/fuzz, private). Read
its README.md for architecture; this skill is the operating manual.pnpm fuzz # all rules, crash+slow oracles
FUZZ_RULE=<id substring> pnpm fuzz # one rule / one family
FUZZ_ITERATIONS=200 FUZZ_SEED=42 pnpm fuzz # more cases, fixed seed
FUZZ_INVARIANTS=1 pnpm fuzz # + metamorphic oracle (warn)
FUZZ_STRICT=1 pnpm fuzz # fail on invariant violations
FUZZ_CORPUS_DIR=<dir of repos> pnpm fuzz # + real files & crossover
FUZZ_PRINT_SILENT=1 pnpm fuzz # list rules that never fired
FUZZ_RULE=<rule-id> FUZZ_STRICT=1 FUZZ_ITERATIONS=500 pnpm fuzz
bun scripts/sync-fuzz-corpus.ts
materializes a pinned 48-repo sample (symlinking the RDE cache when
present, cloning otherwise), and bun scripts/build-bench-corpus.ts
extracts react-bench's diagnostic-dense RD-health target files:cd packages/fuzz && bun scripts/sync-fuzz-corpus.ts
FUZZ_INVARIANTS=1 FUZZ_ITERATIONS=100 FUZZ_CORPUS_DIR=packages/fuzz/tmp/corpus-repos pnpm fuzz
cd packages/fuzz && bun scripts/hunt-false-positives.ts
<button> — but cross-rule hits on the seed's core
idiom have repeatedly exposed real bugs: a stale ARIA role→props table,
useLayoutEffect treated as post-paint, async-useCallback opacity).
HUNT_CORPUS_DIR=<repos> appends a hit census over real files for
noise-ranking rules.fuzz fire-coverage: N/total rules produced a diagnostic.
A rule that never fires is only having its early bails fuzzed — its
"survived fuzzing" result proves almost nothing. When validating a specific
rule, first confirm it FIRES under fuzz:FUZZ_RULE=<rule-id> FUZZ_PRINT_SILENT=1 pnpm fuzz
packages/fuzz/src/snippet-pools.ts (effects, state, handlers, guards,
library idioms, JSX attributes/leaves, module scope) with a snippet that
uses the shared lexicon (state/setState, items, value, url,
handle, containerRef, …). Tune with the measurement scripts:cd packages/fuzz
bun scripts/measure-coverage.ts # generator fire-coverage
bun scripts/measure-corpus-coverage.ts # corpus fire-coverage
packages/fuzz/tmp/fuzz-findings/ with
rule/kind/seed headers. Every case replays from its seed alone.FUZZ_RULE=<id> FUZZ_SEED=<seed> FUZZ_ITERATIONS=1 pnpm fuzz..test.ts (asserting no throw), fix the visitor (typical
causes: missing null check on parent, assuming a member is an
Identifier, assuming trigger token implies full context).SLOW_RULE_THRESHOLD_MS to
make a finding disappear.packages/fuzz/corpus/regressions/. Every file there is a confirmed false
positive — valid code a rule once wrongly flagged — and every fuzz run
mutates and crosses over these seeds, concentrating pressure on the
historically weakest detection logic.packages/fuzz/corpus/regressions/<rule-id>--<slug>.tsx with the header
(see packages/fuzz/corpus/README.md): rule id, weakness class, source.pnpm test in packages/fuzz — a smoke test enforces every seed
parses cleanly.src/snippet-pools.ts (so mutation pressure applies to generated
programs too, not just this one seed) and add the class to the catalog
below..test.ts (the
corpus seed complements it, never replaces it).tmp/fuzz-findings/ into
corpus/regressions/ after minimizing.| class | the trick | canonical examples |
|---|---|---|
| library-idiom | the "violation" is a library's documented API shape | observer(() => hooks), track()(fn), create*Container, Promise.resolve().then microtask, styled/emotion templates, ?? 0 - getTimezoneOffset() negation, split-fragment index keys, download/target="_blank" anchors |
| control-flow | a guard/branch/lifecycle the rule's walk doesn't connect | visibility gates (showX && <JSX window…/>), cancellation flags, cleanup-only setters, loop-variant construction (new RegExp(t, flags) in loop), statically bounded sources (Array(4)), catch-path resets via helper |
| wrapper-transparency | a wrapper forwards what the rule demands | {...props} can carry type/onChange/children/name; memo/forwardRef/factory callbacks; TS as/satisfies/paren wrappers |
| name-heuristic | naming implies a different kind than the pattern assumes | showMenu prop that is a callback not a flag; INIT_TIMESTAMP per-process constant; aria-label/aria-labelledby as accessible names; boolean var named count |
| alias-guard | the value is guarded through another binding | const price = item?.price; if (!price) return; item?.price * 2; renamed destructuring ({ type: kind }); local const resolving to a valid literal |
| cross-file | the proof lives in another module | imported canUseDOM/IS_BROWSER; escape helpers defined elsewhere; cross-file const endpoints; caller-side try/catch |
| framework-gating | valid only/never under a framework context | SSR-only rules in client-only code; Next opengraph-image.tsx rendered via next/og; Next 15 async API rules on Next 14; test/story/e2e paths |
| paren-shape | parenthesization or token shape flips meaning | x ?? (0 / y) vs x ?? 0 / y (note: oxlint parses with preserveParens: false — paren detection must be positional/range-based, never rely on ParenthesizedExpression nodes) |
| dynamic-computed | computed member/call defeats static naming | items[index]() in deps; obj["use" + "State"]; computed removal method |
| copy-tracking | a fresh copy is mutated, not the original | [...items].sort(), items.slice(), Array.from(x), structuredClone, spread-then-mutate-then-set |
@hookform/resolvers/zod), barrel re-export chains, peer-dependency
satisfaction, config-file-only references (postcss/tailwind), pnpm
overrides, baseUrl resolution, dynamic-import template paths. These had
the worst measured FP rates historically (up to ~75% for
unused-dev-dependency) but are project-level scans, not oxlint rules.Fuzz:
- command: <exact env + command>
- fire-coverage: <did the target rule fire? N programs>
- findings: <none | list with kinds and seeds>
- corpus seeds added: <paths or none>
- pool snippets added: <yes/no + domain>