* docs: remove stale dead-code claims after Knip removal (#246)
PR #246 removed the bundled knip integration, but several user-facing
docs still advertised dead-code coverage and exposed the removed
`--dead-code` / `--no-dead-code` CLI flags:
- packages/react-doctor/README.md ("and dead code" in the categories list)
- skills/react-doctor/SKILL.md (skill description)
- packages/website/public/llms.txt (CLI options block)
Strips those claims and adds a short migration note pointing users to
`npx knip` directly for dead-code analysis.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* chore(todos): mark stale dead-code doc cleanup as resolved
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Builds the AST-only foundation needed to port the 8 rules from
eslint-plugin-react-you-might-not-need-an-effect (NickvanDyke,
v0.10.1, upstream SHA 4c71faaa7623d2d5feb33983dc2ebcc08206bcc5).
Upstream is ESLint-native and uses context.sourceCode.getScope()
references / resolved.defs[].node.init recursively to chase the
ultimate source of every value. Oxlint plugins have no scope
manager. This commit ports the impedance-bridging analyzer:
- get-effect-deps-array.ts: typed accessor for useEffect(..., deps)
- has-effect-cleanup.ts: matches upstream's loose hasCleanup shape
- is-custom-hook-name.ts: `use[A-Z]` predicate
- is-synchronous.ts: parent-pointer walk bailing on await / void /
nested function-likes
- get-containing-component-or-hook.ts: classifies the surrounding
function as component / HOC-wrapped component / custom hook
(mirrors upstream's pure-HOC short-circuit for memo/forwardRef)
- collect-use-ref-bindings.ts: top-level useRef binding collector
- collect-local-function-bindings.ts: FunctionDeclaration / arrow /
useCallback bindings with param names + body for indirect-call
reasoning
- analyze-component-bindings.ts: the cornerstone — classifies every
component-scope name as state / setter / ref / prop / constant /
local-function and precomputes localFunctionCalls{StateSetter,
PropFunction,RefMethod} flags so callees like setX(...) inside an
intermediateSetter() still register as state-setter calls
- classify-effect-callee-argument.ts: leaf / transitive upstream
classification used by every rule for argument and deps analysis
- stringify-expression-snippet.ts: bounded AST → text formatter for
the no-initialize-state "set to ..." message
- create-effect-analyzer-tracker.ts: function-entry / -exit visitor
pair that maintains the ComponentBindingTable stack
Plus rules/state-and-effects/effect/SOURCE.md with upstream commit
SHA, license attribution, and known divergences.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix: scope client secret diagnostics
Avoid reporting weak secret-name heuristics in config and tooling files, and render framework-specific public env guidance so non-Next.js projects get actionable output.
* fix: broaden secret false-positive guards
Extend the weak client-secret heuristic boundary to common server and tooling paths while keeping exact secret-shape detection active, and move recommendation helpers into focused utilities.
* fix: tighten rc file secret heuristic
Require rc config filenames to be explicit so normal source files like src.ts still use the weak secret-name heuristic.
* test: cover explicit rc secret heuristic
Add regression coverage that explicit rc config files remain outside the weak client-secret name heuristic while regular src.ts files still report.
* fix: refine secret heuristic boundaries
Keep client API helpers covered while making the server-only heuristic exclusions easier to audit.
* chore: format TODO tracker
Format the inherited TODO tracker so PR merge-commit checks can pass.
* fix: classify client secret exposure
Gate the weak secret-name heuristic on explicit client exposure while keeping high-confidence secret values active everywhere.
* test: cover secret exposure boundaries
Lock down client, server, ambiguous-source, and App Router cases for the client-secret heuristic.
* fix: pass framework to secret classifier
Use detected framework settings to avoid path-only client exposure guesses for app directories and server-owned source roots.
* fix: respect use server in secret classifier
Classify use-server modules as server code so client-path heuristics do not report server action constants.
* fix: harden secret exposure edges
Cover mixed server-only scopes, server-owned source roots, and additional client entry shapes so the weak secret heuristic stays scoped to bundled client code.
* fix: refine secret classifier precedence
Classify secret exposure from project-relative paths and gate Next-only route heuristics so explicit client signals win over broad server path names.
* fix: suppress secret label false positives
Avoid weak secret-name diagnostics for UI copy and storage-key prefixes found during evals while preserving client secret fallback coverage.
* fix: preserve api key secret detection
Keep camel-case apiKey variables eligible for the weak secret heuristic while retaining UI text and storage prefix suppressions.
* chore: format changeset prerelease metadata
Keep the new prerelease metadata from main aligned with the repository formatter so CI can pass on the PR merge commit.
react-doctor is dropping dead-code analysis. knip exists as a standalone
tool; users who want it can run `knip` themselves. This removes the
entire integration: subprocess runner, config loading, filter caching,
result merging, CLI flag, types, and tests.
## Source files deleted (7)
- `src/core/run-knip.ts` (224 LOC)
- `src/core/collect-unused-file-paths.ts`
- `src/core/sanitize-knip-config-patterns.ts`
- `src/core/has-knip-config.ts`
- `src/core/is-likely-build-entry.ts` (only consumer was the knip
auto-suppression branch in merge-and-filter-diagnostics)
- `src/types/knip.ts`
- `src/knip.d.ts`
## Test files deleted (5)
- `tests/run-knip.test.ts`
- `tests/has-knip-config.test.ts`
- `tests/sanitize-knip-config-patterns.test.ts`
- `tests/collect-unused-file-paths.test.ts`
- `tests/extract-failed-plugin-name.test.ts` (was knip-specific)
## Modified files (26)
- `src/core/inspect.ts` — drop `deadCode` option, the entire
`deadCodePromise` block, `runKnip` import, `didDeadCodeFail`
tracking, and the "dead code" skip-list entry. `Promise.all` over
one promise → direct `await`.
- `src/core/combine-diagnostics.ts` — drop `deadCodeDiagnostics`
parameter
- `src/core/merge-and-filter-diagnostics.ts` — drop the
`if (plugin === "knip" && rule === "files")` auto-suppression branch
and its `isLikelyBuildEntry` cache
- `src/types/inspect.ts`, `types/diagnose.ts`, `types/config.ts` —
drop `deadCode?: boolean` fields and `entryFiles?` (knip-only)
- `src/cli/cli-flags.ts`, `cli/resolve-cli-inspect-options.ts`,
`cli/index.ts`, `cli/commands/inspect.ts` — drop `--dead-code` /
`--no-dead-code` flags
- `src/core/validate-config-types.ts` — drop validator entries for
`deadCode` and `entryFiles`
- `package.json` — drop `"knip"` from `keywords` and from
`dependencies`
- Various test files — drop knip-related assertions
## Verification
- `pnpm typecheck` — 0 errors
- `pnpm test` — 75 test files / **688 / 688 pass** (was 80/734 — 5
test files and 46 tests removed with knip)
- `pnpm lint` — 0 warnings, 0 errors
- `pnpm format` — clean
- Diagnostic-snapshot diff vs `origin/main` — byte-identical 262 lines
(the snapshot only covers `runOxlint` output; knip was a separate
diagnostic source that didn't contribute on the test fixtures)
## Stats
- **39 files changed, +43 / -1700 net (1657 line reduction)**
- 13 files deleted
- One full dependency dropped (`knip` from package.json)
Ran fallow (https://github.com/fallow-rs/fallow) against the repo for
dead-code analysis. Acted on the genuine findings, skipped false
positives.
## Genuine dead code (deleted)
- **`USE_EFFECT_EVENT_MIN_MAJOR`** (`src/constants.ts`) — only referenced
in a comment, never in actual code. Real dead.
- **`EM_DASH_CHARACTER`** (`src/plugin/constants/design.ts`) — was used
by the `no-em-dash-in-jsx-text` rule that #242 deleted as an orphan;
stale leftover.
## Test helpers (made internal)
- `tests/run-oxlint/_helpers.ts` had `FIXTURES_DIRECTORY` and
`findDiagnosticsByRule` marked `export const`, but neither is consumed
outside the file. Stripped the `export` keyword on both — internal use
still works, dead-export warning gone.
## Duplicate export (renamed)
- `RuleSeverity` was declared in two places with different definitions:
- `plugin/utils/rule.ts`: `"error" | "warn"` (plugin's defineRule
type — never "off" because off rules aren't registered)
- `core/runners/oxlint/types.ts`: `"error" | "warn" | "off"` (oxlint
config entries — "off" explicitly disables a rule)
These are related but semantically distinct. Unified by making the
oxlint version DERIVE from the plugin type: `OxlintRuleSeverity =
RuleSeverity | "off"`. Renamed all oxlint-layer usages to
`OxlintRuleSeverity` to make the distinction clear in code. Plugin's
`RuleSeverity` remains the single source of truth.
## Tool integration
- Added `fallow` as a devDependency
- Added `.fallowrc.json` with this project's entry points
(`src/cli/index.ts`, `src/index.ts`, `src/plugin/react-doctor-plugin.ts`,
`src/eslint-plugin.ts`, `scripts/*.mjs`) and `tests/fixtures/**`
ignored (those are intentional broken-code fixtures)
Run `pnpm exec fallow --summary` from the package to re-audit.
## Not acted on (false positives or out-of-scope)
- 5 `name` field warnings on `Error` subclasses in `src/errors.ts` —
the `name` property is consumed by the JS runtime when formatting
errors, not by direct code references. False positive.
- 2 "unlisted dependency" warnings (`undici`, `vite-plus`) — both are
available transitively. Adding them to `dependencies` would change
install behavior, which is outside the dead-code-sweep scope.
- 4 "unresolved import" warnings in `tests/fixtures/**` — intentional
broken-code fixtures used by suppression / resilience tests. Already
in `.fallowrc.json` ignorePatterns; the next fallow run will skip.
## Verification
- `pnpm typecheck` — 0 errors
- `pnpm test` — 734 / 734 pass
- `pnpm lint` — 0 warnings, 0 errors
- `pnpm format` — clean
- Diagnostic-snapshot diff vs `origin/main` — byte-identical 262 lines
## Stats
- 11 files changed, +115 / -28 net (most of the +/- is the
`OxlintRuleSeverity` rename touching 5 files)
- Net deletion of 2 dead constants + 2 unnecessary exports
* refactor: tier 1 cleanup — drop dead fields, flatten core/, inline single-use utils
Five structural simplifications in one PR, all zero-behavior-change
(verified: byte-identical 262-line diagnostic snapshot vs `main`).
## 1. Drop `examples` field on Rule
The `examples: RuleExample[]` slot was authored across all 178 rules in
PR #234 but nothing ever consumed it. Pure storage / maintenance burden.
Removed the field from the interface, deleted `RuleExample` type, and
stripped the field from every rule via codemod.
## 2. Derive `framework` from bucket directory (no per-rule field)
The `framework` field was always 1:1 with the bucket directory:
- `nextjs/*.ts` → `framework: "nextjs"`
- `react-native/*.ts` → `framework: "react-native"`
- `tanstack-start/*.ts` → `framework: "tanstack-start"`
- `tanstack-query/*.ts` → `framework: "tanstack-query"`
- everything else → `framework: "global"`
Pure redundancy. Updated the codegen (`scripts/generate-rule-registry.mjs`)
to compute it from the bucket directory and inject it into the registry
entry. Rule files no longer carry the field at all. 178 lines removed
across rule files; codegen gained the bucket→framework table.
## 3. Default `category` from bucket, preserve overrides
Same shape as #2 but with overrides — most rules in a bucket share a
category, but a few (e.g. `tanstack-start-no-secrets-in-loader`,
`nextjs-no-side-effect-in-get-handler`) override to `"Security"`.
Codegen now provides a bucket→default-category table, and rules only
ship the `category` field when overriding. Codemod stripped ~170 of the
178 `category:` declarations (left 4 overrides intact, verified by
diagnostic snapshot).
## 4. Inline single-use plugin utils
Helpers in `plugin/utils/` and `plugin/rules/<bucket>/utils/` with only
one importer got inlined back into their consumer. Pure file-count
reduction — utilities shared by 2+ rules stay.
- `plugin/utils/`: ~50 → 40 files
- `plugin/rules/*/utils/`: ~30 → 27 files
## 5. Flatten `core/` subdirectories
The 5 subdirectories (`core/config/`, `core/detection/`,
`core/diagnostics/`, `core/runners/`, `core/scoring/`) collapsed into a
flat `core/` directory. Subdirs weren't pulling weight — consumers always
imported by specific file name, never as a group. Now everything's one
level deep.
(Exception: `core/runners/oxlint/` stays nested — those 6 files form a
cohesive oxlint-config subcluster.)
200+ import statements updated across the codebase. `git mv` used so
history is preserved.
## Verification
- `pnpm typecheck` — 0 errors
- `pnpm test` — 80 test files / 734 / 734 pass
- `pnpm lint` — 0 warnings, 0 errors
- `pnpm format` — clean
- Diagnostic-snapshot diff vs `origin/main` — byte-identical 262 lines
## Stats
- 330 files changed, **+650 / -6312 net** (5662 line reduction)
- 18 files deleted (dead utils + the now-pointless RuleExample type)
- 12 renamed (the core/ flatten)
- 54 renamed-with-modifications (core/ flatten + import updates)
After this PR, the largest source-of-truth files are the 178 rule files
themselves; everything else is supporting infrastructure that's now
smaller and flatter.
* fix(codegen): emit multi-line registry entries so prettier doesn't re-wrap them
CI's `format:check` reformatted long single-line registry entries to
multi-line, which would have caused the codegen-overwrite-on-build to
loop forever. Pre-emit the multi-line shape so the file is byte-stable
across `pnpm gen → pnpm format → pnpm gen`.
Verified: MD5 unchanged across gen → format → gen cycle.