What happens
Every rule tagged test-noise — 332 of the 884 in the registry — is silently disabled on any file whose path carries a directory segment from the built-in "non-application" list. tools, demo, examples, migrations, perf, it and spec are on that list, and in a lot of app codebases those are ordinary product directories: src/components/tools/** is a feature area, not a build script.
Nothing says so. The file is still scanned — untagged rules report on it normally — so the scan looks complete and the affected families simply read as zero. That makes a "this family is at zero repo-wide, promote it to error" ratchet unsound: the zero can mean the rule never ran.
Minimal reproduction
package.json (no doctor.config.ts, no .oxlintrc.json, no ignore rules anywhere):
{
"name": "reach-repro",
"private": true,
"dependencies": { "next": "16.0.0", "react": "19.2.5", "react-dom": "19.2.5" },
"devDependencies": { "react-doctor": "0.9.12" }
}
Two byte-identical files:
// src/components/chat/widget.tsx AND src/components/tools/widget.tsx
export function Widget() {
return <a href="/about">About</a>;
}
npx react-doctor . --lint --no-dead-code --no-supply-chain --json
Actual: one diagnostic.
src/components/chat/widget.tsx nextjs-no-a-element warning
Expected: two — nextjs-no-a-element is tagged test-noise, and src/components/tools/ is application code.
The same holds for no-transition-all (className="transition-all"), no-tiny-text, js-tosorted-immutable and every other test-noise rule. Untagged rules (alt-text, no-array-index-as-key) report on both files, which is what makes the gap easy to miss.
Where it comes from
The rule's create(context) runs for the file and returns the empty-visitor sentinel, so the visitor never fires — the CLI-side diagnostic pipeline never sees anything to drop (shouldAutoSuppress / classifyFileContext both read these paths as "production", correctly). The path list is the 56-entry literal in oxlint-plugin-react-doctor/dist/capability-*.js that starts /test/,/tests/,/testing/,… and includes /scripts/,/cli/,/bin/,/tooling/,/tools/,/codemods/,/migrations/,/generators/,/devtools/.
Bisected on 0.9.12 by planting the same file at a range of paths: tools, scripts, bin, tooling, cli, codemods, generators, examples, demos, playground, fixtures, mocks, __tests__, e2e, cypress, benchmarks, perf, migrations, devtools → silent; tool, toolsx, aitools, config, configs, script, docs, vendor, public → reported.
Why it is hard to work around
ReactDoctorConfig has no application-paths field, so a project cannot opt a directory back in. ignore.overrides only removes rules; there is no inverse. The remaining options are renaming a large feature directory or accepting that half the rule set does not run there, and neither is visible from the scan output.
Suggestions
- Make it visible. Report the count somewhere in the JSON and summary — e.g.
skippedFileCount per tag, or a suppressions entry with source non-application-path alongside the ones already tracked. A silent skip is indistinguishable from a clean file.
- Make it configurable. An
applicationPaths / productionPaths field on ReactDoctorConfig, or honouring an explicit include, would let a project say "src/** is all shipped code".
- Consider narrowing the default heuristic inside a framework source root. A
tools/ or demo/ directory nested under src/, app/ or components/ is product code far more often than it is tooling; the same name at the repo root usually is not.
Version: react-doctor 0.9.12, oxlint-plugin-react-doctor 0.9.12, macOS arm64, Node 24.
What happens
Every rule tagged
test-noise— 332 of the 884 in the registry — is silently disabled on any file whose path carries a directory segment from the built-in "non-application" list.tools,demo,examples,migrations,perf,itandspecare on that list, and in a lot of app codebases those are ordinary product directories:src/components/tools/**is a feature area, not a build script.Nothing says so. The file is still scanned — untagged rules report on it normally — so the scan looks complete and the affected families simply read as zero. That makes a "this family is at zero repo-wide, promote it to error" ratchet unsound: the zero can mean the rule never ran.
Minimal reproduction
package.json(nodoctor.config.ts, no.oxlintrc.json, no ignore rules anywhere):{ "name": "reach-repro", "private": true, "dependencies": { "next": "16.0.0", "react": "19.2.5", "react-dom": "19.2.5" }, "devDependencies": { "react-doctor": "0.9.12" } }Two byte-identical files:
Actual: one diagnostic.
Expected: two —
nextjs-no-a-elementis taggedtest-noise, andsrc/components/tools/is application code.The same holds for
no-transition-all(className="transition-all"),no-tiny-text,js-tosorted-immutableand every othertest-noiserule. Untagged rules (alt-text,no-array-index-as-key) report on both files, which is what makes the gap easy to miss.Where it comes from
The rule's
create(context)runs for the file and returns the empty-visitor sentinel, so the visitor never fires — the CLI-side diagnostic pipeline never sees anything to drop (shouldAutoSuppress/classifyFileContextboth read these paths as"production", correctly). The path list is the 56-entry literal inoxlint-plugin-react-doctor/dist/capability-*.jsthat starts/test/,/tests/,/testing/,…and includes/scripts/,/cli/,/bin/,/tooling/,/tools/,/codemods/,/migrations/,/generators/,/devtools/.Bisected on 0.9.12 by planting the same file at a range of paths:
tools,scripts,bin,tooling,cli,codemods,generators,examples,demos,playground,fixtures,mocks,__tests__,e2e,cypress,benchmarks,perf,migrations,devtools→ silent;tool,toolsx,aitools,config,configs,script,docs,vendor,public→ reported.Why it is hard to work around
ReactDoctorConfighas no application-paths field, so a project cannot opt a directory back in.ignore.overridesonly removes rules; there is no inverse. The remaining options are renaming a large feature directory or accepting that half the rule set does not run there, and neither is visible from the scan output.Suggestions
skippedFileCountper tag, or asuppressionsentry with sourcenon-application-pathalongside the ones already tracked. A silent skip is indistinguishable from a clean file.applicationPaths/productionPathsfield onReactDoctorConfig, or honouring an explicitinclude, would let a project say "src/**is all shipped code".tools/ordemo/directory nested undersrc/,app/orcomponents/is product code far more often than it is tooling; the same name at the repo root usually is not.Version: react-doctor 0.9.12, oxlint-plugin-react-doctor 0.9.12, macOS arm64, Node 24.