Skip to content

refactor(plugin): split rules into per-file modules with TSESTree-narrowed type layer#218

Merged
aidenybai merged 4 commits into
mainfrom
refactor/per-rule-files-and-oxc-types
May 13, 2026
Merged

refactor(plugin): split rules into per-file modules with TSESTree-narrowed type layer#218
aidenybai merged 4 commits into
mainfrom
refactor/per-rule-files-and-oxc-types

Conversation

@aidenybai
Copy link
Copy Markdown
Member

@aidenybai aidenybai commented May 13, 2026

Refactor-only port of the code-organization ideas from #217 to main. Zero behavior drift, no rules added/removed, no scoring or CLI changes, no v2 surface.

Summary

Two stacked commits, each independently testable:

  1. refactor(plugin): split helpers/types into single-purpose files with TSESTree-narrowed isNodeOfType (06e9687)

    • Replaces monolithic plugin/helpers.ts (~580 LOC) and plugin/types.ts (~30 LOC) with 50 single-purpose files under plugin/utils/
    • Adds @typescript-eslint/types as a devDependency and wires it through EsTreeNodeOfType<T> = Extract<TSESTree.Node, { type: T }> so isNodeOfType(node, "MemberExpression") narrows to the real estree shape (node.computed: boolean, node.arguments.length: number, etc.) instead of any
    • Old plugin/helpers.ts and plugin/types.ts become 8- and 25-line re-export shims so nothing else needs to change to import them
    • Adds defineRule helper in plugin/utils/define-rule.ts (identity function, used by PR 2)
    • Two narrow EsTreeNode | null | undefined widenings in react-native.ts local helpers (forced by PR 1's tighter parent type)
  2. refactor(plugin): split each rule into its own file with defineRule wrapper (5f689e7)

    • Splits 16 monolithic plugin/rules/<bucket>.ts files (10,368 LOC) into 179 per-rule files under plugin/rules/<bucket>/<rule>.ts
    • Each rule wraps with defineRule<Rule>({...}) and only imports the helpers it uses
    • Private helpers used by only one rule are inlined into that rule's file; helpers shared across rules are duplicated per-consumer
    • Bucket files become 12-24 line re-export barrels so plugin/index.ts and external consumers keep working unchanged
    • Each rule's recommendation / examples metadata fields are now ready to be added per-file in a follow-up

What this is NOT

Of the v2 PR (#217), only the code-organization ideas came across. None of these are in this branch:

  • ~70 new rules (i18n, mobx, shadcn, storybook, swr, tanstack-ai, react-hook-form, react-three-fiber, testing-library)
  • Score formula changes (per-category cap, log amplification, info-issue exclusion)
  • Rule false-positive tweaks (radix-aschild, rn-no-raw-text, no-eval test-noise, etc.)
  • CLI rewrite (--unstaged, --changed, annotations behavior changes)
  • SDK / createReactDoctor / inspectReactProject surface
  • Codebase analyzer rewrite (shared module graph, plugins)
  • Hierarchical typed errors
  • Removal of browser-poc, knip, bippy, wasm parser
  • Removal of eslint-plugin-react-you-might-not-need-an-effect

Verification

A whitespace + type-only-widening normalized byte-comparison of every rule body confirms:

  • 179 of 179 rules byte-identical (after normalizing Prettier reformatting and the one EsTreeNode | null | undefined widening)
  • plugin/index.ts rule registry: byte-identical (same rule IDs, same identifiers, same order)
  • plugin/constants.ts: byte-identical
  • No rules added, no rules removed

Branch totals vs main: 251 files changed, +13,241 / −11,058. 734 tests pass, lint clean, format clean, typecheck clean. No new runtime deps.

Test plan

  • pnpm typecheck clean on both commits
  • pnpm test — 734/734 tests pass on both commits
  • pnpm lint — 0 warnings, 0 errors
  • pnpm format — clean
  • Rule body byte-comparison vs main shows zero behavior drift

Note

Medium Risk
Large-scale refactor that moves/rewires many rule implementations and helper imports; risk is mainly in missed exports/path resolution or subtle type-narrowing fallout despite intended no behavior change.

Overview
Refactors react-doctor’s ESLint-style plugin code by extracting the previously monolithic helpers.ts and several rule “bucket” modules into many single-purpose files under src/plugin/utils/ and src/plugin/rules/<bucket>/, while keeping the public entrypoints as thin re-export shims.

Adds a shared createDeprecatedReactImportRule utility for React 19 deprecation checks and wraps individual rule modules with defineRule, plus a devDependency on @typescript-eslint/types to support a tighter, TSESTree-narrowed node/type layer.

Reviewed by Cursor Bugbot for commit 6002723. Bugbot is set up for automated code reviews on this repo. Configure here.

aidenybai added 2 commits May 13, 2026 09:21
…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.
…rapper

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.
@reactreview
Copy link
Copy Markdown

reactreview Bot commented May 13, 2026

🔴 React Review0/100 (unchanged) · 0 ❌ errors · 13 ⚠️ warnings

Copy prompt for agent
Check if these React Review issues are valid. If so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.

Run this before and after your changes to verify the result:
npx react-doctor@latest --verbose --diff

Do not modify the react-doctor configuration unless explicitly asked.
Fix the underlying code issues instead of changing or suppressing the rules.

React Review found 0 errors and 13 warnings. This PR leaves the React health score unchanged.

<file name="packages/react-doctor/src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts">

<violation number="1" location="packages/react-doctor/src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts:204">
Severity: Warning

array.find() in a loop is O(n*m) — build a Map for O(1) lookups

Build an index `Map` once outside the loop instead of `array.find(...)` inside it

Rule: `js-index-maps`
</violation>

</file>

<file name="packages/react-doctor/src/plugin/rules/performance/rerender-transitions-scroll.ts">

<violation number="1" location="packages/react-doctor/src/plugin/rules/performance/rerender-transitions-scroll.ts:70">
Severity: Warning

cursor.callee.name is read 3 times inside this loop — hoist into a const at the top of the loop body

Hoist the deep member access into a const at the top of the loop body: `const { x, y } = obj.deeply.nested`

Rule: `js-cache-property-access`
</violation>

</file>

<file name="packages/react-doctor/src/plugin/rules/state-and-effects/no-mirror-prop-effect.ts">

<violation number="1" location="packages/react-doctor/src/plugin/rules/state-and-effects/no-mirror-prop-effect.ts:133">
Severity: Warning

array.find() in a loop is O(n*m) — build a Map for O(1) lookups

Build an index `Map` once outside the loop instead of `array.find(...)` inside it

Rule: `js-index-maps`
</violation>

</file>

<file name="packages/react-doctor/src/plugin/rules/state-and-effects/no-effect-event-handler.ts">

<violation number="1" location="packages/react-doctor/src/plugin/rules/state-and-effects/no-effect-event-handler.ts:23">
Severity: Warning

.filter().map() iterates the array twice — combine into a single loop with .reduce() or for...of

Combine `.map().filter()` (or similar chains) into a single pass with `.reduce()` or a `for...of` loop to avoid iterating the array twice

Rule: `js-combine-iterations`
</violation>

</file>

<file name="packages/react-doctor/src/plugin/rules/state-and-effects/no-derived-state-effect.ts">

<violation number="1" location="packages/react-doctor/src/plugin/rules/state-and-effects/no-derived-state-effect.ts:87">
Severity: Warning

.filter().map() iterates the array twice — combine into a single loop with .reduce() or for...of

Combine `.map().filter()` (or similar chains) into a single pass with `.reduce()` or a `for...of` loop to avoid iterating the array twice

Rule: `js-combine-iterations`
</violation>

</file>

<file name="packages/react-doctor/src/plugin/utils/are-expressions-structurally-equal.ts">

<violation number="1" location="packages/react-doctor/src/plugin/utils/are-expressions-structurally-equal.ts:32">
Severity: Warning

.every() over an array compared to another array — short-circuit with `a.length === b.length && a.every(...)` so unequal-length arrays exit immediately

Short-circuit with `a.length === b.length && a.every((x, i) => x === b[i])` — unequal-length arrays exit immediately

Rule: `js-length-check-first`
</violation>

</file>

<file name="packages/react-doctor/src/plugin/rules/server/server-sequential-independent-await.ts">

<violation number="1" location="packages/react-doctor/src/plugin/rules/server/server-sequential-independent-await.ts:21">
Severity: Warning

declarator.id.type is read 3 times inside this loop — hoist into a const at the top of the loop body

Hoist the deep member access into a const at the top of the loop body: `const { x, y } = obj.deeply.nested`

Rule: `js-cache-property-access`
</violation>

</file>

<file name="packages/react-doctor/src/plugin/rules/tanstack-start/tanstack-start-server-fn-method-order.ts">

<violation number="1" location="packages/react-doctor/src/plugin/rules/tanstack-start/tanstack-start-server-fn-method-order.ts:43">
Severity: Warning

array.indexOf() in a loop is O(n) per call — convert to a Set for O(1) lookups

Use a `Set` or `Map` for repeated membership tests / keyed lookups — `Array.includes`/`find` is O(n) per call

Rule: `js-set-map-lookups`
</violation>

</file>

<file name="packages/react-doctor/src/plugin/rules/tanstack-start/tanstack-start-route-property-order.ts">

<violation number="1" location="packages/react-doctor/src/plugin/rules/tanstack-start/tanstack-start-route-property-order.ts:30">
Severity: Warning

array.indexOf() in a loop is O(n) per call — convert to a Set for O(1) lookups

Use a `Set` or `Map` for repeated membership tests / keyed lookups — `Array.includes`/`find` is O(n) per call

Rule: `js-set-map-lookups`
</violation>

</file>

<file name="packages/react-doctor/src/plugin/rules/react-ui/no-space-on-flex-children.ts">

<violation number="1" location="packages/react-doctor/src/plugin/rules/react-ui/no-space-on-flex-children.ts:23">
Severity: Warning

array.includes() in a loop is O(n) per call — convert to a Set for O(1) lookups

Use a `Set` or `Map` for repeated membership tests / keyed lookups — `Array.includes`/`find` is O(n) per call

Rule: `js-set-map-lookups`
</violation>

</file>

<file name="packages/react-doctor/src/plugin/rules/design/no-inline-bounce-easing.ts">

<violation number="1" location="packages/react-doctor/src/plugin/rules/design/no-inline-bounce-easing.ts:24">
Severity: Warning

array.includes() in a loop is O(n) per call — convert to a Set for O(1) lookups

Use a `Set` or `Map` for repeated membership tests / keyed lookups — `Array.includes`/`find` is O(n) per call

Rule: `js-set-map-lookups`
</violation>

</file>

<file name="packages/react-doctor/src/plugin/rules/design/no-gradient-text.ts">

<violation number="1" location="packages/react-doctor/src/plugin/rules/design/no-gradient-text.ts:27">
Severity: Warning

array.includes() in a loop is O(n) per call — convert to a Set for O(1) lookups

Use a `Set` or `Map` for repeated membership tests / keyed lookups — `Array.includes`/`find` is O(n) per call

Rule: `js-set-map-lookups`
</violation>

</file>

<file name="packages/react-doctor/src/plugin/rules/react-ui/no-bold-heading.ts">

<violation number="1" location="packages/react-doctor/src/plugin/rules/react-ui/no-bold-heading.ts:55">
Severity: Warning

new RegExp() inside a loop — hoist to a module-level constant

Hoist `new RegExp(...)` (or large regex literals) to a module-level constant so it isn't recompiled on every loop iteration

Rule: `js-hoist-regexp`
</violation>

</file>

Reviewed by react-review for commit 6002723. Configure here.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-doctor-website Ready Ready Preview, Comment May 13, 2026 4:40pm

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5f689e7. Configure here.

Comment thread packages/react-doctor/src/plugin/rules/design/no-dark-mode-glow.ts Outdated
Comment thread packages/react-doctor/src/plugin/rules/architecture/no-react19-deprecated-apis.ts Outdated
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.
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.
@aidenybai aidenybai merged commit 4eab5ab into main May 13, 2026
6 checks passed
aidenybai added a commit that referenced this pull request May 13, 2026
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.
aidenybai added a commit that referenced this pull request May 13, 2026
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.
aidenybai added a commit that referenced this pull request May 13, 2026
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.
aidenybai added a commit that referenced this pull request May 13, 2026
…s,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)
cursor Bot pushed a commit that referenced this pull request May 13, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
#221, #223, #224, #226, #227) split the monolithic src/utils +
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request May 13, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request May 14, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request May 14, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request May 14, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request May 14, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant