FN mining
A repeatable false-negative mining harness for oxlint-plugin-react-doctor
rules. Each case is a syntactic variant of a rule's documented bad
pattern — same underlying problem, different surface shape (arithmetic on
the index, a template literal, an aliased import, indirection through a
variable, a different loop kind). The runner executes every variant against
its rule and reports which ones did NOT fire. Those silent variants are
false-negative candidates for human triage.
Run
pnpm fn-mining
The runner always exits 0 — it is a mining/report tool, not a CI gate. A silent variant is not automatically a bug: many rules deliberately trade recall for precision (documented carve-outs, name gates, same-file-proof requirements). Every silent variant needs a human to read the rule source and decide: real FN worth fixing, or deliberate scoping.
The script runs through tsx (provided transitively by vite-plus)
rather than node --experimental-strip-types like the other root scripts,
because it imports the plugin's TypeScript sources, whose internal
./x.js-suffixed imports Node's type stripping cannot resolve.
Adding cases
- Pick a rule where detection breadth matters (pattern-matching rules, not
simple attribute checks). Read its source under
packages/oxlint-plugin-react-doctor/src/plugin/rules/to understand exactly what it matches — the goal is probing edge shapes it might miss, not re-testing its canonical example. - Create
cases/<rule-id>.tsexporting an array ofFnMiningCase:
import type { FnMiningCase } from "../fn-mining-case.js";
export const myRuleCases: FnMiningCase[] = [
{
ruleId: "my-rule", // the rule's public id (ruleRegistry key)
description: "index offset by arithmetic: key={index + 1}",
filePath: "src/list.tsx", // drives parser lang + file-based gates
code: `...`,
shouldFire: true,
},
];
- Register the array in
cases/index.ts. When triage concludes a silent variant is deliberate rule scoping, keep the case but setshouldFire: falseand explain the gate incarveOutReason— the runner then prints it as[carved]instead of re-flagging it as an FN candidate, and warns if it ever starts firing (a precision regression). - Include one or two canonical "must fire" variants as a baseline — if those go silent, the case itself is broken (wrong file path hitting a testlike-filename gate, a missing import a rule's import check needs, etc.), not the rule.
Case-writing notes:
filePathmatters. Rules taggedtest-noiseskip testlike filenames (*.test.tsx,*.stories.tsx); scan rules skip non-production paths. Use a production-looking path unless the file-based gate is itself what you are probing.- AST rules run through
runRule(the same pure-TS oxc-parser host the unit tests use); rules with ascanfunction run throughrunScanRuleagainst an in-memory file. The runner picks automatically based on the rule shape in the registry. - Framework gating (
requires: ["react-native"]etc.) is a registry-level concern and does not apply here — the rule logic runs directly.
Interpreting output
Per rule, each variant prints as [fired] or [SILENT], followed by a
summary and the aggregated FN-candidate list. Triage each silent variant
by reading the rule source:
- Real FN candidate — the variant is the same user-visible problem and
the miss is an implementation gap (e.g. the key-expression walker not
handling
key={i + 1}while handlingkey={\item-${i}`}`). - Deliberate scoping — the rule author chose precision over recall and the source says so (e.g. spread props may carry the missing attribute, same-file memo proof required, name-gated index parameters).
Report real FNs with a pointer into the rule source (file:line of the check that bails) so the fix conversation starts concrete.