mirror of
https://github.com/millionco/react-doctor.git
synced 2026-09-14 20:00:24 +08:00
codex/oxc-source-patch
1865 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
29b7229ea1 | 0.2.0-beta.4 | ||
|
|
10d5de804f | 0.2.0-beta.3 | ||
|
|
9783acf525 | Fix no-barrel-import index false positives (#253) | ||
|
|
809e38ceba | pub | ||
|
|
0cd93551a4 | fix: narrow effect event handler detection (#256) | ||
|
|
bca5d30fc5 | fix(oxlint): collapse duplicate diagnostics + recover diagnostics from large monorepo projects (eval-driven) (#262) | ||
|
|
ffbd20f3d0 | fix: suppress local use hook false positives (#257) | ||
|
|
bfaf9c9530 | fix: avoid React 19 rule false positives on React 18 (#254) | ||
|
|
6bc33c8aab | fix: treat return guards as render state reads (#255) | ||
|
|
d23137e177 | test: add fixed issue regression coverage (#258) | ||
|
|
92829c7123 | chore: format TODOS.md to fix format:check on main | ||
|
|
0a52ca3d05 | fix | ||
|
|
9902ea671d | TODOS.md | ||
|
|
6e2ee9d474 | refactor(cli): reorganize cli/ into commands/utils + port from react-grab (#250) | ||
|
|
f0198e2f2d | refactor: extract @react-doctor/{types,project-info,core} internal packages + cleanup (#249) | ||
|
|
4d622970b5 |
Revert "pub"
This reverts commit
|
||
|
|
4d9d1a5fcf | pub | ||
|
|
12d9bf75ab | chore: drop .fallowrc.json + fallow devDep (one-time sweep, not committing to fallow integration) (#247) | ||
|
|
6d8ba1b993 |
refactor: rip out knip / dead-code-detection integration (#246)
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) |
||
|
|
575a245179 |
chore(dead-code): fallow sweep — drop 2 dead constants, dedupe RuleSeverity, fix test exports (#245)
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 |
||
|
|
7169551251 |
refactor: tier 1 cleanup — drop dead fields, flatten core/, inline single-use utils (#244)
* 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. |
||
|
|
8393d9c8ee |
refactor: auto-register rules via codegen + split 3 kitchen-sink files (types, state-rules.test, run-oxlint.test) (#242)
* refactor: auto-register rules via codegen + split 3 kitchen-sink files (types, state-rules.test, run-oxlint.test)
Four structural cleanups in one PR. Pure file-organization — zero
behavior change, byte-identical 262-line diagnostic snapshot vs `main`.
## 1. Auto-register rules via codegen
`plugin/react-doctor-plugin.ts` shrinks from **403 → 14 lines**. Every
rule now declares its own `id: "<rule-id>"` field in its
`defineRule({...})` call; a build-time codegen script
(`scripts/generate-rule-registry.mjs`) scans every per-rule file and
emits `src/plugin/rule-registry.ts`, which is the only thing the plugin
imports.
Adding a new rule used to require editing 2 places (import line +
registry entry in `react-doctor-plugin.ts`) and silently dropped the
rule from the scan if you forgot. Now it's a single-file operation —
create `plugin/rules/<bucket>/<rule>.ts` with an `id` field, run
`pnpm gen`. The codegen fails loudly on missing-id or duplicate-id.
Wired into the package scripts so `pnpm typecheck`, `pnpm test`,
`pnpm build` all auto-run codegen. A `pnpm gen:check` script (CI-ready)
asserts the committed registry matches what codegen would produce.
Also deletes the orphan `plugin/rules/react-ui/no-em-dash-in-jsx-text.ts`
— it was authored but never registered in the plugin (dead code for 3+
PRs). Re-adding it properly is a feature commit, not a refactor.
## 2. `src/types.ts` (346 LOC) → `src/types/<domain>.ts`
Split the kitchen-sink types file into focused per-domain files
(`project-info.ts`, `diagnostic.ts`, `inspect.ts`, `diagnose.ts`,
`config.ts`, `knip.ts`, `prompts.ts`, `score.ts`, `handle-error.ts`).
Every consumer's import path updated to point at the specific domain
file. No barrel.
## 3. `tests/regressions/state-rules.test.ts` (2788 LOC) → per-rule files
Split the biggest single file in the repo into per-rule test files
under `tests/regressions/state-rules/<rule-id>.test.ts`. Shared setup
extracted to `state-rules/_helpers.ts`.
## 4. `tests/run-oxlint.test.ts` (983 LOC) → per-concern files
Split into per-concern files under `tests/run-oxlint/<concern>.test.ts`
(globals, nextjs, tanstack-start, tanstack-query, react-native,
categories, suppression, custom-rules-only, adopt-existing-lint-config,
architecture, async-performance, bundle-size, correctness,
performance, etc.). Shared helpers in `_helpers.ts`.
## Verification
- `pnpm typecheck` — 0 errors
- `pnpm test` — 80 test files (was 54) / **734 / 734 pass** (same count)
- `pnpm lint` — 0 warnings, 0 errors
- `pnpm format` — clean
- Diagnostic-snapshot diff vs `origin/main` — **byte-identical** 262 lines
## Stats
- 264 files changed, +302 / -4628 net (most of the deletion is the test
splits collapsing duplicate imports + the 400-line plugin registry
being replaced by a 14-line wrapper + a generated file)
- Plugin registry: 403 → 14 hand-written lines (plus 366 generated)
* fix(codegen): use POSIX separators in rule-registry import paths (Windows fix)
* fix(plugin): delete 16 dead bucket re-export barrels (rules/<bucket>.ts)
Bugbot caught these as orphaned after the auto-register refactor. The
codegen-built `rule-registry.ts` imports directly from per-rule files
(`plugin/rules/<bucket>/<rule>.ts`), so the bucket-level barrels
(`plugin/rules/architecture.ts`, `bundle-size.ts`, `client.ts`,
`correctness.ts`, `design.ts`, `js-performance.ts`, `nextjs.ts`,
`performance.ts`, `react-native.ts`, `react-ui.ts`, `security.ts`,
`server.ts`, `state-and-effects.ts`, `tanstack-query.ts`,
`tanstack-start.ts`, `view-transitions.ts`) are no longer reached by
any consumer. ~200 lines of pure dead re-exports.
|
||
|
|
b71a492481 |
refactor: dead-file sweep + split 3 kitchen-sink files (constants, discover-project, cli/index) (#237)
* refactor: dead-file sweep + split 3 kitchen-sink files (constants, discover-project, cli/index) Seven structural cleanups in one PR. ## Dead files removed (zero consumers) - `plugin/utils/index.ts` (52-line barrel, never imported) - `plugin/helpers.ts` (28-line shim from the original utils/ split) - `plugin/types.ts` (9-line type-only shim, single consumer inlined) - `core/runners/oxlint-config.ts` (11-line re-export barrel, 2 consumers now import directly from the underlying `oxlint/config.js` and `oxlint/react-doctor-rules.js`) ## `plugin/constants.ts` (831 lines) → `plugin/constants/<domain>.ts` Split the kitchen-sink constants file into 11 domain-grouped sibling files. Every rule now imports just the constants it actually uses, from the specific domain file (no barrel): - `constants/thresholds.ts` — numeric "too many" / "too big" cutoffs - `constants/style.ts` — layout/CSS/motion property sets - `constants/tanstack.ts` — TanStack Router / Query / Start - `constants/react.ts` — React / React Compiler / effect hooks - `constants/react-native.ts` — RN-specific - `constants/nextjs.ts` — Next.js-specific - `constants/security.ts` — secret/auth patterns - `constants/library.ts` — heavy-library bundles, fetch callees - `constants/dom.ts` — passive events, barrel suffixes - `constants/js.ts` — JS-runtime constants - `constants/design.ts` — Tailwind/design tokens ## `core/detection/discover-project.ts` (849 → ~142 lines) Extracted each detection routine to its own focused file in `core/detection/`. The top-level file is now a slim orchestrator that composes the per-concern detectors. ## `cli/index.ts` (743 → ~102 lines) + `cli/commands/` Extracted each subcommand handler to `cli/commands/<command>.ts` (`inspect.ts`, `install.ts`). `cli/index.ts` is now just Commander setup + command registration + `program.parse(...)`. ## 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 - 150 files changed (109 modified, 5 deleted, 36 added) - +171 / -2447 net lines deleted (mostly from the constants split where every rule went from one giant import to ~3 focused imports) * fix(detection): hoist `.map().filter()` into single for...of loop in resolve-workspace-directories Eats own dogfood on `js-combine-iterations`. The other 5 dogfood warnings from React Review on this PR are either false positives of `js-set-map-lookups` on `string.indexOf`/`string.includes` (not array methods) or are intentional sequential UX in the inspect loop where logger output between iterations must stay ordered. |
||
|
|
f85d980e1d |
refactor(oxlint): split rule-maps.ts into external-plugin-rules + react-doctor-rules (#236)
* refactor(oxlint): split rule-maps.ts into external-plugin-rules + react-doctor-rules
`oxlint/rule-maps.ts` was mixing two unrelated concerns: 4 hand-maintained
maps of rules adopted from upstream plugins (react, jsx-a11y,
react-hooks-js, you-might-not-need-effect) and 5 react-doctor-derived
constants computed from `plugin.rules`. Splits them into purpose-named
sibling files and deletes the misnamed `rule-maps.ts`:
- **`oxlint/external-plugin-rules.ts`** — the 4 upstream-plugin maps.
Pure data, no derivation. Loaded by `createOxlintConfig` to merge into
the generated config.
- **`oxlint/react-doctor-rules.ts`** — the 5 per-framework maps PLUS
`ALL_REACT_DOCTOR_RULE_KEYS` and `FRAMEWORK_SPECIFIC_RULE_KEYS`. All
derived at module load from the colocated `defineRule({...})` metadata
on each rule. The two `*_RULE_KEYS` sets now derive **directly** from
`plugin.rules` instead of going through the per-framework maps, which
cuts one indirection and makes the source of truth explicit.
Public API unchanged — `oxlint-config.ts` re-exports the same names from
their new locations, so consumers (`run-oxlint.ts`,
`merge-and-filter-diagnostics.ts`, `eslint-plugin.ts`) don't move.
Verified: byte-identical 262-line diagnostic snapshot from `runOxlint`
against `basic-react`, `nextjs-app`, and `tanstack-start-app` fixtures
vs `origin/main`. 734 / 734 tests pass.
* fix(rule-maps): use for...of instead of .filter().map() to satisfy js-combine-iterations
|
||
|
|
b2dfc018d1 |
refactor(types): EsTreeNode = TSESTree.Node (drop loose [key: string]: any escape hatch) (#235)
Replaces the custom loose `EsTreeNode` interface (which exposed a
`[key: string]: any` index signature so any property access typechecked
as `any`) with a direct alias for `@typescript-eslint/types`'
`TSESTree.Node` discriminated union, with `parent` relaxed via the same
`WithLooseParent` helper that's already used by `EsTreeNodeOfType<T>`.
Net effect:
- **Native discriminated-union narrowing** — `if (node.type === "CallExpression") { node.callee }` now narrows `node` automatically (no `isNodeOfType` ceremony required for inline cases).
- **Strict by default** — every property access on a non-narrowed `EsTreeNode` is a typecheck error. Catches missing narrowings at compile time the way TS catches missing imports.
- **`isNodeOfType` still works** — it's now just a runtime-safe equivalent of `node?.type === "X"` that narrows through opaque inputs (e.g. `.find` results). Most existing call sites keep it for readability.
To get to 0 errors, three classes of fixes landed:
1. **Visitor-parameter retype** (codemod): 222 visitor methods across 172
rule files had their `(node: EsTreeNode)` annotation tightened to
`(node: EsTreeNodeOfType<"<SelectorKey>">)`. The visitor's selector
IS the node type the handler receives, so this is mechanical.
2. **`RuleVisitors` parameter loosened to `any`**. Function-type
parameters are contravariant — `(node: CallExpression) => void` is
NOT assignable to `(node: EsTreeNode) => void`. Typing the visitor's
`any` parameter sidesteps variance while keeping `EsTreeNode` strict
everywhere else. Scoped to the visitor entry-point only.
3. **Per-call-site narrowings** for the remaining ~282 sites where
helper functions or nested member accesses needed an `isNodeOfType`
guard (or further narrowing of an already-narrowed union like
`Expression` → `ArrowFunctionExpression`). Each fix is a tightening
— preserves the original behavior on the cases the code handled,
silently no-ops on the cases the loose `any` previously let through.
Verified: byte-identical 262-line diagnostic snapshot from `runOxlint`
against `basic-react`, `nextjs-app`, and `tanstack-start-app` fixtures
vs `origin/main`. 734 / 734 tests pass.
|
||
|
|
6b357778f0 |
refactor(rules): final colocation pass (requires/tags + examples + tighten interface) (#234)
Three changes in one PR — the last seams in the per-rule metadata model:
**1. Colocate `RULE_METADATA` (requires + tags) into each `defineRule({...})`**
Drops the 166-line `oxlint/rule-metadata.ts` table that gated framework-
specific rules by capability (`react:19`, `nextjs`, `tailwind:3.4`, …)
and grouped them by behavioral tag (`test-noise`, `design`). Moves
every entry onto the matching rule:
```ts
export const noReact19DeprecatedApis = defineRule<Rule>({
category: "Architecture",
framework: "global",
severity: "warn",
requires: ["react:19"],
tags: ["test-noise"],
recommendation: "…",
// …
create: …
});
```
`createOxlintConfig` now reads `requires` + `tags` off the plugin
registry; `merge-and-filter-diagnostics` reads `tags` the same way for
auto-suppression of test-noise rules in test files. The
`rule-metadata.ts` file is deleted entirely.
**2. Tighten `Rule.framework` + `Rule.severity` to required**
Now that every rule ships both (via #231 + this PR's verification),
flipping them from optional to required catches "forgot to set framework
on a new rule" at compile time. The factory used by the deprecated-API
rules (`createDeprecatedReactImportRule`) is retyped to
`Pick<Rule, "create">` so the spread doesn't double-specify these
fields. The orphan `no-em-dash-in-jsx-text` rule (registered nowhere
but still typechecked) gets the required fields filled in to keep the
file valid.
**3. Add `examples` for every rule (178 hand-authored before/after pairs)**
The `Rule.examples: RuleExample[]` slot existed since the
`defineRule` introduction but was unused. Every rule now ships at
least one minimal before/after example next to its recommendation,
extracted from the rule's diagnostic message and `create` body. The
examples are deliberately short (≤4 lines each side) — they describe
the shape the rule fires on, not full real-world snippets.
**Phase 4 (strict `EsTreeNode` — dropping the `[key: string]: any`
escape hatch) was attempted in this branch and reverted: it exposed
747 type errors across ~150 rule files, which is genuinely a separate
PR's worth of work.** Filed as the natural next step.
**Verified runtime equivalence:** byte-identical 262-line diagnostic
snapshot from `runOxlint` against `basic-react`, `nextjs-app`, and
`tanstack-start-app` fixtures vs `origin/main`.
- 188 files changed, +1348/-206
- 734/734 tests pass, 0 lint warnings, 0 typecheck errors
|
||
|
|
2c1f7423c9 |
refactor(rules): colocate severity + framework with each defineRule call (#231)
Drops the 5 framework-keyed maps (`GLOBAL_REACT_DOCTOR_RULES`,
`NEXTJS_RULES`, `REACT_NATIVE_RULES`, `TANSTACK_START_RULES`,
`TANSTACK_QUERY_RULES`) in `oxlint/rule-maps.ts` and moves every entry
onto the matching rule's `defineRule({...})` call as `framework: "..."`
+ `severity: "..."` fields (next to the `category` and `recommendation`
that landed in #228 / #230). The 5 maps are now thin derived constants
computed at module load by filtering `plugin.rules` on each rule's
`framework` field, so every downstream consumer (`createOxlintConfig`,
`eslint-plugin.ts`'s flat configs) keeps the same public shape.
178 rule files updated. The single source of truth for "which framework
does this rule belong to" and "at what severity does it fire" now lives
next to the rule's `create` function — adding a new rule no longer
requires editing a separate map (and forgetting to is no longer how
rules silently miss the framework filter).
Verified runtime behavior: `runOxlint` produces a byte-identical 262-line
diagnostic snapshot against `basic-react`, `nextjs-app`, and
`tanstack-start-app` fixtures vs `main`.
|
||
|
|
4241e3d470 |
refactor(rules): colocate category with each defineRule call (#230)
Drops the 196-line `RULE_CATEGORY_MAP` table in `run-oxlint.ts` and moves
every entry onto the matching rule's `defineRule({...})` call as a
`category: "..."` field (next to the `recommendation` that landed in
#228). `run-oxlint.ts` now reads the category off the plugin registry at
diagnostic-rendering time, so output is unchanged but each rule has a
single obvious place for both its recommendation AND its category
bucket.
178 rule files updated; `run-oxlint.ts` shrinks by ~196 lines. No
behavior change — same category strings, same fallback through
`PLUGIN_CATEGORY_MAP` for non-react-doctor diagnostics, same
registration validator (now warning on any rule whose `defineRule`
call doesn't ship a `category`).
|
||
|
|
d5bc247ea9 |
refactor(plugin): port inline node.type === "X" checks to isNodeOfType(node, "X") (#229)
* refactor(plugin): port inline `node.type === \"X\"` checks to `isNodeOfType(node, \"X\")` Replaces 1127 inline `<chain>(?.|.)type ===/!== "TypeName"` checks across the rules with calls to the existing `isNodeOfType(<chain>, "TypeName")` type-guard. The helper narrows the chain to the real TSESTree node shape so subsequent property access is type-checked; the inline string comparison only checks at runtime (`EsTreeNode.type: string` doesn't form a discriminated union, so `if (node.type === "Identifier")` did NOT narrow `node` for TypeScript). 193 files touched. Same runtime behavior — but the new narrowing exposed ~42 latent type errors that were previously swallowed by the loose `EsTreeNode[key: string]: any` index signature. Each was fixed by adding the narrowing the code had implicitly assumed: - `ImportSpecifier.imported` is `Identifier | StringLiteral` (ES2022 module-string imports). Adds a `getImportedName(specifier)` helper that returns the name from either shape; 9 rule files now use it. - `MemberExpression.property` is `PrivateIdentifier | Expression` — rules that wanted the property's name now narrow to `Identifier` first (e.g. `no-array-index-as-key`, `no-inline-prop-on-memo-component`, `rerender-functional-setstate`). - `ObjectExpression.properties[]` is `(Property | SpreadElement | RestElement)[]` — rules that read `.key` / `.value` now narrow to `Property` first (`query-no-void-query-fn`, `query-no-use-query-for-mutation`, `walk-server-fn-chain`). - `Literal.value` is `string | number | bigint | boolean | RegExp | null` — callers that fed it to a `string` API now `typeof === "string"` first (`client-passive-event-listeners`, `no-layout-property-animation`). - A handful of other rules narrow chained `callee` / `init` / `expression` / destructuring-pattern elements that the loose type let through. `isNodeOfType` is null-safe (delegates to `hasTypeProperty`) so all optional-chain forms (`x?.type ===` etc.) are preserved in semantics. * fix(rule): drop redundant `isNodeOfType` re-check on already-narrowed `node.id` Bugbot caught a leftover narrowing check from the previous commit's fix: the `if (!isNodeOfType(node.id, "ArrayPattern")) return;` on line 21 was unnecessary because line 14 already narrows `node.id` to `ArrayPattern`, and TypeScript preserves that narrowing across the intermediate `node.init` accesses (`node.id` isn't reassigned). Tests + typecheck remain green. * fix(rule): preserve PrivateIdentifier name in `rerender-lazy-state-init` message The previous narrowing fix only accepted `Identifier` as the MemberExpression property, so `useState(obj.#method())` started rendering as `useState(fn())` instead of `useState(method())`. The original code reached `.name` directly off the loose `EsTreeNode` and got the private-field name for free. Add an explicit `PrivateIdentifier` narrowing alongside the `Identifier` one so the diagnostic message keeps quoting the actual method name in both cases. Cosmetic only — the rule still fires on the same code shape; only the text inside the suggested replacement message changes. |
||
|
|
ca0ba6ea03 |
refactor(rules): colocate recommendation text with each defineRule call (#228)
* refactor(rules): colocate recommendation text with each defineRule call
Drops the 375-line `RULE_HELP_MAP` table in `run-oxlint.ts` and moves every
entry onto the matching rule's `defineRule({...})` call as a
`recommendation: "..."` field. `run-oxlint.ts` now reads the recommendation
off the plugin registry at lookup time, so the diagnostic-rendering path is
unchanged but each new rule has a single obvious place to put its
recommendation text (next to the rule logic it describes).
178 rule files updated; `run-oxlint.ts` shrinks by 365 net lines. No
behavior or output change — same recommendation strings, same registration
validator (now warning on any rule whose `defineRule` call doesn't ship a
`recommendation`).
* fix(plugin): move plugin object out of plugin/index.ts to avoid barrel-import warning
Renames `plugin/index.ts` → `plugin/react-doctor-plugin.ts` (where it's
actually constructed) and updates the 3 consumers (`eslint-plugin.ts`,
`run-oxlint.ts`, `vite.config.ts`) to import from the direct path. The
file's only export was the plugin object built in-place, so this isn't a
re-export barrel — but the `/index.js` suffix tripped react-doctor's own
`no-barrel-import` rule once a second importer landed in this PR.
Fixes the React Review warning at `run-oxlint.ts:22` introduced by the
previous commit.
* pub
|
||
|
|
6e9d682334 |
refactor(diagnose): use combineDiagnostics helper instead of inlining its body (#227)
`diagnose()` in src/index.ts manually inlined the exact same logic that
`combineDiagnostics` (the helper already used by `inspect()`) provides:
1. Conditionally add `checkReducedMotion(directory)` diagnostics
(skip when isDiffMode)
2. Merge [...lint, ...deadCode, ...env]
3. Pass through `mergeAndFilterDiagnostics` with the same options
Audit (see PR description) confirmed the two paths are functionally
identical -- `combineDiagnostics` was added later and `diagnose()`
never adopted it.
After this change:
- Both `inspect()` and `diagnose()` go through the same merge helper
- Future bug fixes / behavior tweaks only have to happen in one place
- `diagnose()` no longer needs to import `checkReducedMotion` or
`mergeAndFilterDiagnostics` directly
Net change: 1 file, +8/-8 (drop 2 imports, add 1; replace 8-line inline
with 8-line helper call). Functionally identical -- 734/734 tests pass.
This is the small, low-risk slice of the bigger inspect/diagnose
unification question. The remaining differences (subproject discovery,
nvm-Node detection, offline scoring, skipped-checks tracking) are
intentional behavioral choices that need product decisions, not
refactoring -- they stay out of scope here.
|
||
|
|
4e62a2c0e2 |
refactor: split oxlint-config.ts (722 LOC) into core/runners/oxlint/ (#226)
Same shape as the scan.ts split: the 722-line oxlint-config.ts mixed
five distinct concerns. Carved into six focused files under
src/core/runners/oxlint/, with oxlint-config.ts kept as a thin
re-export barrel so existing importers (run-oxlint.ts, eslint-plugin.ts,
merge-and-filter-diagnostics.ts, tests) keep working unchanged.
New layout:
src/core/runners/oxlint/
types.ts 11 LOC RuleSeverity + OxlintConfigOptions
rule-maps.ts 305 LOC per-framework severity maps (NEXTJS_RULES,
REACT_NATIVE_RULES, TANSTACK_START_RULES,
TANSTACK_QUERY_RULES, GLOBAL_REACT_DOCTOR_RULES,
REACT_COMPILER_RULES, YOU_MIGHT_NOT_NEED_EFFECT_RULES,
BUILTIN_REACT_RULES, BUILTIN_A11Y_RULES,
ALL_REACT_DOCTOR_RULE_KEYS, FRAMEWORK_SPECIFIC_RULE_KEYS)
rule-metadata.ts 166 LOC RULE_METADATA + tag constants + RuleMetadataEntry
plugin-resolution.ts 110 LOC resolveReactHooksJsPlugin / resolve-You-Might-...
/ readPluginRuleNames / filterRulesToAvailable
capabilities.ts 56 LOC buildCapabilities + shouldEnableRule
config.ts 98 LOC createOxlintConfig (the composer that pulls
everything together)
src/core/runners/oxlint-config.ts 12 LOC re-export barrel
Compared to before: 722-line monolithic file -> 758 LOC total across 7
focused files, each under 305 LOC.
Internal-only types/helpers that were exported unnecessarily are now
unexported in their new homes (e.g. RuleMetadataEntry stays internal to
rule-metadata.ts; buildCapabilities/shouldEnableRule are internal to
the capabilities module).
Verification:
- typecheck/lint/format clean
- 734/734 tests pass
- No public API change (barrel exposes the same surface)
- run-oxlint.ts, eslint-plugin.ts, merge-and-filter-diagnostics.ts,
and tests all import from the unchanged oxlint-config.js barrel
|
||
|
|
8bb54c9823 |
refactor: split src/utils/ into src/core/{config,detection,diagnostics,scoring,runners}/ + src/cli/ (#224)
* refactor: split src/utils/ (68 flat files) into src/core/{config,detection,diagnostics,scoring,runners}/ + src/cli/
Final big organizational refactor in the series (#218 plugin/, #220 cli/,
#221 scan/, #222 drop browser-poc, #223 dead-code). Carves the 68-file
src/utils/ dumping ground into purpose-built directories.
New layout:
src/cli/ presentation (26 files)
+ prompts.ts, highlighter.ts, spinner.ts, logger.ts
+ colorize-by-score.ts, format-error-chain.ts
+ indent-multiline-text.ts, wrap-indented-text.ts
+ build-hidden-diagnostics-summary.ts
+ should-auto-select-current-choice.ts, should-select-all-choices.ts
+ detect-agents.ts, to-display-name.ts
+ (existing cli/ files: index.ts, render-*.ts, etc.)
src/core/ core logic (7 files at root + 5 subdirs)
build-json-report.ts shared JSON reporting (CLI + public API)
build-json-report-error.ts shared JSON error envelope
group-by.ts, is-file.ts generic primitives
is-plain-object.ts, to-relative-path.ts
read-file-lines-node.ts
config/ (11) config loading, root-dir resolution, ignore patterns,
glob matching, gitattributes parsing, validation
detection/ (6) project discovery, react/tailwind major-version parsing,
package.json reading, monorepo root detection
diagnostics/ (12) combine/filter/suppress/merge, jsx-opener-span
lookup, disable-directive walks
scoring/ (6) local + remote score calc, score breakdown,
reduced-motion check, proxy fetch
runners/ (13) oxlint + knip + their config builders, include-path
computation, Node-version compat, diff-files
src/plugin/ (unchanged from #218)
scan.ts, index.ts, errors.ts, types.ts, constants.ts,
oxlint-config.ts, eslint-plugin.ts, install-skill.ts top-level
Driven by a one-shot codemod that performed the moves via `git mv` (so
all 68 file renames preserve blame/log history) and rewrote every
relative import in the package + tests (98 importer files touched) to
the new paths.
One non-mechanical fix needed after the move:
src/core/runners/run-oxlint.ts resolved its built plugin via
`../../dist/react-doctor-plugin.js` -- with 1 extra level of nesting
that needs to be `../../../dist/react-doctor-plugin.js`.
One test fixed:
tests/regressions/scan-resilience.test.ts read a source file by
hardcoded path (`src/utils/calculate-score-locally.ts`) to assert no
`fetch(` reference -- updated to the new location
(`src/core/scoring/calculate-score-locally.ts`).
Verification:
- 128 files changed, +237 / -222 (almost entirely import-path edits)
- typecheck/lint/format clean
- 734/734 tests pass
- build produces identical dist/cli.js, dist/index.js, etc.
- No public API change
- src/utils/ no longer exists
* refactor: move orchestrators into tiers + rename scan -> inspect (#225)
* refactor: move install-skill, scan, oxlint-config out of src/ root into their tiers
Top-level src/ was still holding three orchestrator-grade files that
logically belong inside the existing tier directories. Move them so the
src/ root only contains public-API entry points and cross-cutting types.
Moves (via git mv, history preserved):
src/install-skill.ts -> src/cli/install-skill.ts CLI subcommand
(already imported
6 things from cli/)
src/scan.ts -> src/core/scan.ts Orchestrator
(lives with the
logic it calls)
src/oxlint-config.ts -> src/core/runners/oxlint-config.ts Sibling of
run-oxlint.ts
(config + runner)
After: top-level src/ contains only the entry points and shared types:
src/
├── cli/ presentation
├── core/ logic
├── plugin/ rules
├── constants.ts
├── errors.ts
├── eslint-plugin.ts public-API entry
├── index.ts public-API entry
├── knip.d.ts type declaration
└── types.ts
Imports rewritten via a small codemod (11 files):
- 3 moved files' internal imports recomputed for new depth
- 4 src/ consumers of the moved files updated
- 4 test files updated
Verification:
- 11 files changed, +39/-39 (all import path edits)
- typecheck/lint/format clean
- 734/734 tests pass
- build produces identical dist/* output
- run-oxlint.ts's relative path to dist/react-doctor-plugin.js
(../../../) is unchanged -- run-oxlint.ts itself didn't move
Stacks on top of #224 (utils->core reorg). Once #224 merges, this PR's
base auto-rebases to main.
* refactor: rename scan -> inspect (function, file, types, test)
"scan" was a misnomer -- the function does much more than scan files: it
runs lint via oxlint, runs dead-code via knip, calculates a score,
merges/filters diagnostics, and renders CLI output. "inspect" describes
the role accurately and matches v2's chosen name.
Renames:
src/core/scan.ts -> src/core/inspect.ts (git mv)
tests/scan.test.ts -> tests/inspect.test.ts (git mv)
scan() function -> inspect() function
runScan() -> runInspect()
mergeScanOptions() -> mergeInspectOptions()
ResolvedScanOptions -> ResolvedInspectOptions
ScanResult -> InspectResult (src/types.ts)
ScanOptions -> InspectOptions (src/types.ts)
describe("scan", ...) -> describe("inspect", ...) (test)
Importers updated:
src/cli/index.ts 4 sites
src/core/build-json-report.ts 3 sites
tests/inspect.test.ts 5 sites
tests/regressions/cli-and-output.test.ts 4 sites
tests/build-json-report.test.ts 2 sites
Kept as English prose (not identifiers):
- "full scan", "scan output", "scan only files", "scan banner" in
comments, CLI option descriptions, and prompt text -- these describe
the action of scanning the codebase, not references to the function.
- constants.ts JSX_OPENER_SCAN_MAX_LINES (about lexical JSX scanning,
unrelated to inspect()).
Verification:
- 7 files changed, +54/-54 (all renames)
- typecheck/lint/format clean
- 734/734 tests pass
- No public API change (scan/ScanResult/ScanOptions weren't exported
from src/index.ts -- the public API still calls the underlying
function diagnose() which is unchanged)
Stacks on #225 (which moved scan.ts to src/core/). Once #225 merges,
this PR's base auto-rebases to main.
* fix: restore help-text example paths corrupted by the import codemod
The utils->core import-path codemod's regex matched `from '...'`
substrings inside string literals -- specifically the EXAMPLE code
shown to users in oxlint diagnostic help text. Three messages in
HELP_TEXT_MAP had their example paths mangled into the codemod's
target form (`../../utils/...`) even though they were supposed to
illustrate paths in the USER'S code, not paths internal to this
package.
Restored to the original example paths:
no-barrel-import:
`import { Button } from '../../utils/components/Button.js'`
-> `import { Button } from './components/Button'`
no-dynamic-import-path:
`import('../../utils/feature/heavy.js')`
-> `import('./feature/heavy.js')`
nextjs-no-css-link:
`import styles from '../../utils/Button.module.css.js'`
-> `import styles from './Button.module.css'`
Verified no other string literals were corrupted (grepped for
`["'`]../../(utils|core|cli)/` across src/ -- all remaining hits are
legitimate plugin/utils imports from the per-rule split in #218, which
is its own unrelated `utils/` directory).
Closes the Bugbot finding on PR #224.
* refactor: move format-error-chain / logger / highlighter to core/ (fix layering)
Addresses Bugbot's "Core modules depend backwards on CLI layer" finding
on PR #224. Several core/ files were importing from cli/, undermining
the layered structure the refactor was introducing:
core/build-json-report-error.ts -> cli/format-error-chain
core/runners/extract-failed-plugin-name -> cli/format-error-chain
core/config/load-config -> cli/logger
core/config/read-ignore-file -> cli/logger
core/config/resolve-config-root-dir -> cli/logger
The 3 utilities in question are all pure abstractions (no CLI-specific
behavior beyond using console.* for output), so they belong in core/
where both core AND cli consumers can import them without inversion.
Moves (git mv, history preserved):
cli/format-error-chain.ts -> core/format-error-chain.ts (Error.cause walker)
cli/logger.ts -> core/logger.ts (silent-mode logger)
cli/highlighter.ts -> core/highlighter.ts (picocolors wrapper)
22 files changed, +29/-29 -- all relative-import path rewrites driven
by a small codemod with a strict `^import ... from "..."` regex
(line-anchored to avoid the help-text-literal corruption bug from the
previous codemod, which triggered the bigger Bugbot follow-up).
After this:
- core/ no longer imports from cli/ for utilities -- only
core/inspect.ts still does (legitimately: it's the CLI orchestrator
that calls render-*.ts + resolve-oxlint-node prompt + spinner)
- cli/ files import the moved utilities via `../core/<name>.js`
Verification:
- typecheck/lint/format clean
- 734/734 tests pass
- `rg "from .+cli/" src/core` shows only inspect.ts's legitimate
imports of render-*, spinner, resolve-oxlint-node (all of which
are CLI presentation/interaction, not utilities)
|
||
|
|
ef2b7fe9f4 |
chore: dead-code sweep (16 -> 1 unused exports) (#223)
Audit after the refactor wave (#218 plugin/, #220 cli/, #221 scan/, #222 drop browser-poc) found 16 zero-reference exports across src/. 15 of them were genuinely dead; 1 was kept (clearCaches is public API). Removals: src/utils/is-rule-suppressed-at.ts ENTIRELY ORPHANED: no file imports it (it just wraps evaluateSuppression) src/constants.ts: REACT_19_DEPRECATION_MIN_MAJOR Defined for rule version-gating that inlined the numbers in oxlint-config.ts instead. REACT_DOM_LEGACY_API_MIN_MAJOR Same -- never wired up TAILWIND_SIZE_SHORTHAND_MIN_MAJOR Same TAILWIND_SIZE_SHORTHAND_MIN_MINOR Same (plus the orphaned HACK comment that described them) Un-exported (interface/helpers only used inside own file): src/cli/parse-file-line-argument.ts ParsedFileLineArgument src/oxlint-config.ts RuleMetadataEntry buildCapabilities shouldEnableRule src/utils/apply-ignore-overrides.ts CompiledIgnoreOverride src/utils/build-category-breakdown.ts CategoryBreakdownEntry src/utils/calculate-score-locally.ts ScoreBreakdown src/utils/evaluate-suppression.ts SuppressionEvaluation src/utils/load-config.ts LoadedReactDoctorConfig src/utils/parse-tailwind-major-minor.ts TailwindMajorMinor Kept: src/index.ts clearCaches Public-API entry, intentionally exposed even if no internal caller uses it. 10 files changed, +10 / -36. typecheck/lint/format/test all clean, 734/734 tests still pass. |
||
|
|
4fc784eb33 |
refactor(scan): extract CLI rendering from scan.ts into src/cli/render-*.ts (#221)
Splits the 899-line src/scan.ts god-orchestrator: keeps the actual scan
flow in scan.ts (313 lines) and lifts the CLI rendering helpers into
6 focused files under src/cli/.
scan.ts before: project detection prints, diagnostic group formatters,
score bar + face renderer, summary + share URL builder, tmpdir
diagnostics dump, Node-for-oxlint resolution prompt -- all interleaved
with the scan orchestrator.
scan.ts after: just the orchestrator -- option merge, runScan flow,
lint/dead-code Promise.all, score calculation, calls to the renderers.
Files extracted:
src/cli/render-diagnostics.ts 317 LOC printDiagnostics +
category/rule group
formatters + verbose
map + helpers
src/cli/render-score-header.ts 74 LOC printScoreHeader /
printBrandingOnlyHeader /
printNoScoreHeader (+
doctor face + score bar)
src/cli/render-summary.ts 82 LOC printSummary +
printCountsSummaryLine +
buildShareUrl
src/cli/render-project-detection.ts 50 LOC printProjectDetection
(spinner-driven "Found
React 19" lines)
src/cli/write-diagnostics-directory.ts 27 LOC writeDiagnosticsDirectory
(the /tmp dump)
src/cli/resolve-oxlint-node.ts 66 LOC resolveOxlintNode (Node
version detection +
"install via nvm?"
prompt)
No behavior change. Same orchestration, same output, same scan() signature.
Each extracted file only imports what it actually uses (no barrel re-exports).
Tests: 734 passing, typecheck/lint/format clean, build produces identical
dist/cli.js.
|
||
|
|
e2c925e172 |
refactor: drop browser-poc (src + fixtures + bippy/@oxc-parser/wasm deps) (#222)
Removes the browser-poc surface entirely (matches v2's decision). Frees
~1300 LOC of source + two runtime deps that nothing else in src/ uses.
Deletions:
src/browser-poc.ts 1083 LOC
src/wasm.d.ts 4 LOC (only typed
.wasm imports
for browser-poc)
tests/fixtures/browser-poc/ app.tsx + index.html
src/constants.ts drop 2 unused consts
(BROWSER_POC_FUNCTION_SOURCE_MAX_CHARS,
BROWSER_POC_HOST_SELECTOR_MAX_COUNT)
package.json:
- exports./browser-poc removed
- scripts.build drop trailing esbuild
step that bundled
dist/browser-poc.global.js
- scripts.poc:browser removed (vite dev
server for the demo)
- dependencies.@oxc-parser/wasm ^0.60.0 removed (only used
by browser-poc)
- dependencies.bippy ^0.5.39 removed (only used
by browser-poc)
vite.config.ts:
- drop react-doctor-wasm-binary-loader plugin (only converted .wasm
to base64 for browser-poc)
- drop WASM_FILE_SUFFIX constant
- drop the { entry: { "browser-poc": ... } } pack
Verified after deletion:
- typecheck/format/lint/test all clean
- 734 tests still pass (no tests targeted browser-poc directly --
only the fixture html/tsx that were html-loaded by the vite dev
server in `poc:browser`)
- dist size drops from ~1.4 MB to ~1.19 MB
- 3 packages removed from node_modules (bippy + @oxc-parser/wasm +
one transitive)
Net diff: 8 files changed, +1 / -1267.
|
||
|
|
6ca7c1eb64 |
refactor(cli): split src/cli.ts and CLI-only utils into src/cli/ (#220)
Same shape as the plugin/ split (#218) but for the CLI tier: carves the 743-line src/cli.ts orchestrator and its 6 CLI-only utility files out of src/utils/ and into a dedicated src/cli/ directory. Files moved (via `git mv` so history is preserved): src/cli.ts -> src/cli/index.ts src/utils/annotation-encoding.ts -> src/cli/annotation-encoding.ts src/utils/find-owning-project.ts -> src/cli/find-owning-project.ts src/utils/get-staged-files.ts -> src/cli/get-staged-files.ts src/utils/handle-error.ts -> src/cli/handle-error.ts src/utils/parse-file-line-argument.ts -> src/cli/parse-file-line-argument.ts src/utils/select-projects.ts -> src/cli/select-projects.ts Picked these specifically because they are the only utils with cli.ts as their sole non-test importer (per a classifier audit). Utils still used by scan.ts, index.ts (public API), eslint-plugin.ts, or install-skill.ts stay in src/utils/ since they aren't CLI-only. Import path rewrites: - Inside the moved files: ./<other-cli-util>.js stays (same dir now), ./<shared-util>.js becomes ../utils/<util>.js - Inside cli/index.ts: ./utils/<x>.js -> ../utils/<x>.js for shared utils, -> ./<x>.js for the 6 moved CLI utils; ./scan.js, ./install-skill.js, ./constants.js, ./types.js -> ../<file>.js - 4 test files updated to import the moved utils from src/cli/ instead of src/utils/ - vite.config.ts: cli entry "./src/cli.ts" -> "./src/cli/index.ts" No behavior change. 734 tests pass, lint clean, typecheck clean, build produces identical dist/cli.js output. |
||
|
|
4eab5ab7ae |
refactor(plugin): split rules into per-file modules with TSESTree-narrowed type layer (#218)
* refactor(plugin): split helpers/types into single-purpose files with TSESTree-narrowed isNodeOfType
Replaces the monolithic plugin/types.ts and plugin/helpers.ts (~600 lines combined)
with 50 single-purpose files under plugin/utils/, and wires up TSESTree as the
source of truth for AST node shapes so isNodeOfType<T> narrows to the real
typescript-eslint node interfaces (e.g. MemberExpression.computed: boolean,
CallExpression.arguments.length: number) instead of `any`.
The old plugin/types.ts and plugin/helpers.ts now re-export from plugin/utils
so no other file needs to change to import them. Adds a defineRule helper
in plugin/utils/define-rule.ts ready for the per-rule split in a follow-up.
- adds @typescript-eslint/types as a devDependency for type narrowing
- EsTreeNode keeps its loose `[key: string]: any` escape hatch so existing
rule bodies continue to compile unchanged
- EsTreeNodeOfType<T> resolves to Extract<TSESTree.Node, { type: T }> with
a relaxed `parent: EsTreeNode | null` (our walker assigns parent freely)
- 2 narrow null-tolerance fixes in plugin/rules/react-native.ts (parent param)
Tests: 734 passing, typecheck/lint/format clean.
* refactor(plugin): split each rule into its own file with defineRule wrapper
Splits the 16 monolithic plugin/rules/<bucket>.ts files (10,368 LOC,
248 rules) into 179 per-rule files under plugin/rules/<bucket>/<rule>.ts.
Each rule now lives in its own file, wraps its definition with
defineRule<Rule>({...}) from plugin/utils, and only imports the helpers
+ constants it actually uses. The bucket files become thin re-export
barrels (12-24 lines each) so plugin/index.ts and external consumers
keep working unchanged.
Private helpers (consts, functions, interfaces, type aliases) that were
only used by a single rule are inlined into that rule's file; helpers
shared across multiple rules are duplicated into each consumer (matches
the v2 plan: keep the file self-contained, lift to plugin/utils later
if it grows beyond a handful of copies).
Generated by a one-shot codemod (parsed each bucket with oxc-parser,
computed each rule's transitive helper closure, emitted per-rule files
with only the imports they need, replaced the bucket with a barrel).
- 16 bucket files: 10,368 LOC -> 179 LOC (barrels)
- 179 new per-rule files under plugin/rules/<bucket>/
- Each rule's `recommendation`/`examples` metadata is now ready to be
added per-file in a follow-up
- 3 react-native split files: re-apply parent param null-tolerance
(these helpers got copied into each consumer file by the codemod)
Tests: 734 passing, typecheck/lint/format clean.
* refactor(plugin): direct-import per-rule files + lift duplicated helpers
Addresses two reviewer comments on the per-rule split:
1. Bugbot found that the codemod inlined some helpers up to 13x across files
in the same category (e.g. getInlineStyleExpression x13 in design/,
resolveJsxElementName x8 in react-native/, collectUseStateBindings x6 in
state-and-effects/). A bug fix in any one copy risks missing the others.
2. React Review fired the package's own no-barrel-import rule 192 times
on the new per-rule files, which all imported from "../../utils/index.js".
Fixes:
- Per-rule files now import directly from individual util source files
(e.g. ../../utils/define-rule.js) instead of the utils/index.js barrel.
- Lifted 33 multi-use helpers from per-rule files into shared per-category
utils dirs at plugin/rules/<category>/utils/<helper>.ts:
architecture/utils/ 2 helpers (deprecated-react-import factory + options)
design/utils/ 8 helpers (style/color extraction)
react-native/utils/ 2 helpers (resolveJsxElementName, SCROLLVIEW_NAMES)
react-ui/utils/ 5 helpers (className/jsx parsing)
state-and-effects/utils/ 12 helpers (subscribe-shape, cleanup, dep-graph)
tanstack-start/utils/ 4 helpers (route options, server fn chain walker)
- Each lifted helper picks up only the imports it actually needs from
constants and plugin/utils.
- Pruned 53 unused-import warnings in rule files that no longer reference
utilities used only by the lifted helpers.
Tests: 734 passing, typecheck/lint/format clean, zero duplicates remaining.
* chore(plugin): unexport DefineRule interface (used only internally)
The DefineRule interface in plugin/utils/define-rule.ts is only needed
to type the defineRule const inside the same file -- no external file
imports it. Removing `export` from the interface keeps the file's public
surface minimal (just defineRule itself).
Found via dead-export audit before merge: this was the only truly dead
export in the new plugin/utils/ + plugin/rules/<bucket>/utils/ layout.
The other low-reference helpers (DeprecatedReactImportRuleOptions,
ServerFnChainInfo, isReleaseLikeCall, containsReleaseLikeCall) are all
legitimate transitive dependencies within their per-category utils dir.
|
||
|
|
8556b31d8e |
feat: address user feedback — reduce false positives, improve scoring transparency, and add config options (#208)
* feat(browser-poc): add AST normalizers for production React analysis Add 12 AST normalizers that transform minified production code into patterns the react-doctor plugin rules can analyze. Enables 30+ rules to fire on production bundles with 0 parse errors and 0 rule failures across 6 tested sites (vercel, notion, linear, discord, shopify, ami). Normalizers: SequenceExpression callee unwrap, OXC literal type normalization, boolean/void recovery, return/expression sequence splitting, JSX reconstruction (jsx/jsxs/createElement → JSXElement tree with Fragment, ExpressionContainer, key extraction), setter binding + reference rename, and component name uppercase recovery. Also adds parent reference tracking in visitAst, "use client" directive injection, WASM failure caching for CSP-blocked sites, truncated source skip, score calculation, and a null-safety fix for prefer-useReducer. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: address user feedback — reduce false positives, improve scoring transparency, and add config options - Add `offline`, `designRules`, and `entryFiles` config options - Suppress React 19 deprecation rules on React 18 (migration-hint gate) - Skip `rn-no-raw-text` for `.web.*` files (RN platform convention) - Add sleep/delay and paginated-fetch heuristics to `asyncAwaitInLoop` - Remove `noEmDashInJsxText` rule (em dashes are standard punctuation) - Add `designRules` toggle to disable opinionated design rules - Thread `entryFiles` to knip for dead-code false positive reduction - Export `calculateScoreBreakdown` and show formula in `--verbose` - Document scoring formula, diff/staged modes, and agent integration - Switch to `@changesets/changelog-github` for richer changelogs - Add GitHub Releases workflow Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: capabilities-based rule gating system Replace bespoke flags and filter functions with a unified capabilities + tags system: - Add buildCapabilities(project) that derives a flat Set<string> from ProjectInfo (react:19, nextjs, tanstack-query, etc.) - Add RULE_METADATA map with requires[] (capability gates) and tags (static classification like "design", "test-noise") - Replace filterRulesByReactMajor, filterDesignRules, VERSION_GATED_RULE_IDS, VersionGateMode, and conditional spreads with one shouldEnableRule predicate - Replace 9 individual fields on RunOxlintOptions/OxlintConfigOptions with project: ProjectInfo - Add ignore.tags to user config (replaces designRules boolean) - Cherry-pick from cursor/library-aware-deprecation-rules-ec3b: peerRangeSupportsLegacyReact, isTestFilePath, isLikelyBuildEntry, parseTailwindMajorMinor, isLikelyStringReceiver (js-set-map-lookups fix) - Compute effective React version from min(installed, peerRangeFloor) so library-targeting-legacy is handled by version gating alone Co-authored-by: Cursor <cursoragent@cursor.com> * fix: library peer-range detection + README designRules cleanup - Fix Bugbot: peerRangeMinMajor computes the floor major from the peer range so effective version is min(installed, peerFloor) instead of null - Fix Bugbot: replace designRules config key with ignore.tags in README - Add peerRangeMinMajor tests Co-authored-by: Cursor <cursoragent@cursor.com> * fix: wire isLikelyBuildEntry + isTestFilePath into post-scan suppression - Add auto-suppression in mergeAndFilterDiagnostics: suppress knip/files diagnostics when a matching build artifact exists, suppress test-noise tagged rules in test/fixture files - Tag deprecation and design rules with "test-noise" in RULE_METADATA - Fixes Bugbot: isLikelyBuildEntry is no longer dead code Co-authored-by: Cursor <cursoragent@cursor.com> * fix: remove dead DESIGN_TAGS constant, clear auto-suppression caches Co-authored-by: Cursor <cursoragent@cursor.com> * fix: remove dead peerRangeSupportsLegacyReact, unused ruleKey param, new Promise false negative Co-authored-by: Cursor <cursoragent@cursor.com> * fix: forward ignoredTags and entryFiles in diagnose() programmatic API Co-authored-by: Cursor <cursoragent@cursor.com> * fix: handle destructuring in loop-carried dependency detection Co-authored-by: Cursor <cursoragent@cursor.com> * fix: apply sleep/dependency heuristics to callback-based iteration The loopBodyHasOnlySleepLikeAwaits and hasLoopCarriedDependency checks were only applied in inspectLoopBody (for/while/do-while) but skipped for callback-based iteration (.forEach, .map, etc.), causing false positives on patterns like arr.forEach(async item => { await sleep(500) }). Co-authored-by: Cursor <cursoragent@cursor.com> * fix: guard framework-specific rules against missing RULE_METADATA Rules from framework-specific maps (NEXTJS_RULES, REACT_NATIVE_RULES, etc.) without a RULE_METADATA entry were unconditionally enabled for all projects. Now they are skipped at runtime, and validateRuleRegistration warns about the gap at dev time. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: scope RULE_METADATA validation to framework-specific rules only Global rules intentionally omit RULE_METADATA entries since they're unconditionally enabled. Extracted FRAMEWORK_SPECIFIC_RULE_KEYS to share the set between the runtime guard and the validation check. --------- |
||
|
|
052cd20aa4 | chore(readme): refresh leaderboard top 10 | ||
|
|
53fa4dffe8 |
feat(react-doctor): detect Tailwind version and gate size-N rule on it (#202)
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> |
||
|
|
30d851a287 | 0.1.6 | ||
|
|
e9e4217711 | fix | ||
|
|
d52e9a8b0f | 0.1.,5 | ||
|
|
5f0f9f9acc |
feat(react-doctor): typed errors + config rootDir for diagnose() targeting (#200)
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> |
||
|
|
ad850cb6ca |
feat(plugin): integrate eslint-plugin-react-you-might-not-need-an-effect (#201)
* feat(plugin): integrate eslint-plugin-react-you-might-not-need-an-effect (#187) Adds the upstream effects-anti-pattern plugin as an optional peer dependency alongside the existing eslint-plugin-react-hooks integration. When installed in the scanned project (or hoisted in a monorepo), its rules fire alongside react-doctor's native state-and-effects rules and count toward the score. Wiring follows the same pattern as react-hooks-js: - Optional peer (`peerDependenciesMeta.optional: true`), so users who don't need the extra surface aren't forced to install it. - Namespaced as `effect/` in oxlint's `jsPlugins` to keep the plugin's long upstream name out of rule keys (`effect/no-derived-state`, etc.). - Categorized under "State & Effects" in PLUGIN_CATEGORY_MAP so diagnostics show in the right output bucket. - Filtered through `filterRulesToAvailable` against the loaded plugin's `rules` export, so a future plugin upgrade that drops a rule silently skips just that rule instead of crashing the scan. - Suppressed when `customRulesOnly` is set, identical to the react-hooks-js gating. Adds three regression tests in `scan-resilience.test.ts`: - The plugin entry shows up in jsPlugins with the configured namespace, and its rules are emitted at "warn" severity. - `customRulesOnly: true` strips both the entry and its rules. - Every configured `effect/` rule key still exists in the loaded plugin's `rules` map. Verified end-to-end against a sample project — `effect/no-derived-state` fires four times alongside react-doctor's own rules in `--verbose` output. Documents the integration in the README under a new "Optional companion plugins" subsection so users discover the surface without reading the source. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * chore(docs): apply formatter to companion-plugins table 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> |
||
|
|
71a1043882 |
docs(actions): full workflow example and inputs reference (#188) (#199)
The composite action shipped at `action.yml` since v0.1.x already implements every option from the issue (PR annotations, score output, diff scans, fail-on levels, etc.). The README only showed a job-fragment, which can read as "this is one of three options" rather than "this is the supported integration." Replace the fragment with a complete copy-pasteable workflow file: documents the trigger, required `fetch-depth: 0` for diff scans, `pull-requests: write` permission for PR comments, and links the action.yml input list. The npx fallback is kept as a one-line escape hatch for projects that prefer no Marketplace dependency. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> |
||
|
|
b501c3bc20 |
docs(config): clarify ignore.overrides covers per-file rule ignore (#197)
* docs(config): clarify ignore.overrides covers per-file rule ignore (#160) The `ignore.overrides` config field already supports per-file rule ignores with the same shape requested in #160 (`{ files, rules }[]`) landing in #165 alongside multi-line JSX suppressions. This rewrites the configuration section so the three nested keys' relative scope is unambiguous and the example covers two of the scenarios from the issue (a glob with multiple rules, a specific file with a single-rule override). No code change needed — the feature is fully implemented and unit-tested in tests/filter-diagnostics.test.ts. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * chore(docs): apply formatter to README example 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> |
||
|
|
b2927fdc60 |
fix(suppressions): match disable comments with descriptive trailing text (#159) (#196)
Two small but related fixes for inline-suppression robustness:
1. The disable-line / disable-next-line regexes used a narrow rule-list
character class ([\\w/\\-.,\\s]) that excluded common comment
punctuation (`;` `:` `(` `'` `?` `!` …). Whenever a user
appended an explanatory tail like
// react-doctor-disable-next-line react-doctor/foo -- searchQuery initial; user can type
the regex stopped matching entirely and the suppression was silently
ignored. Broaden the capture to `[^\\r\\n]*?` (anything on the
same line) and split the rule section vs. description in a follow-up
step, so any prose after the rule ids is fine.
2. `isRuleListedInComment` now strips text after ` -- ` before
tokenizing the rule list, mirroring ESLint's convention that anything
after ` -- ` on a disable comment is a description rather than a
rule id. Without this, descriptive words could be tokenized
alongside the rule list, and any trailing punctuation kept polluting
equality checks.
Stacked single-rule comments (#159) already worked end-to-end after #165
landed multi-line / chain handling — the failing case in the report was
purely the regex rejecting comments with descriptive tails. Adds two
regression tests using the issue's exact reproduction (`useState(searchQuery)`
with stacked descriptive disables, plus the equivalent comma-separated
form), and documents both forms (and the chain-breaks-on-code rule) in
the README so the comma-separated syntax stops being undocumented.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
|
||
|
|
d95c939cab |
feat(cli): expose --why as documented public alias for --explain (#161) (#198)
The MVP requested by #161 — a single-site suppression diagnosis at `react-doctor --why <file:line>` — was already implemented as a hidden alias for `--explain`. Promote it to a documented option so users can reach it without reading the source. Updates the README's CLI reference and suppressions section to make the discovery story explicit: - `--explain <file:line>` (and its `--why` alias) classify what the scanner sees at a single location, matching every status in the issue's classifier table (MISSING, WRONG_RULE, GAP_CODE, etc.). - `--verbose` mode already surfaces the same suppression hint inline next to every flagged site, and `--json` mode includes it on each diagnostic as `suppressionHint`. A single scan therefore doubles as a suppression audit without a separate flag, which covers the "Bonus" and most of the "Maximum" goals from the request. Removes the unused `Option` import that the previous `addOption(...).hideHelp()` wiring left behind. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> |