mirror of
https://github.com/millionco/react-doctor.git
synced 2026-09-14 20:00:24 +08:00
codex/oxc-source-patch
62 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4ee14c5903 | perf(native): extract duplicate JSX candidates with Oxc | ||
|
|
6e6945b875 | feat(native): package react-doctor-rust | ||
|
|
0ce8d9b500 | fix(native): sync rule fixes from main | ||
|
|
290bb75169 | perf(native): port retained security scans | ||
|
|
1d3e4a6061 |
fix(deps): remove shameful hoisting (#1689)
Co-authored-by: Aiden Bai <aiden@million.dev> |
||
|
|
be8e2add3e | perf(native): port 16 rules to Rust | ||
|
|
1e12d5ff2c | feat: add source-patched native Oxlint path | ||
|
|
2b0f06ec70 | perf: profile and optimize rule execution (#1663) | ||
|
|
ffc2d14254 | chore: upgrade Oxc toolchain (#1651) | ||
|
|
8c2f03aea9 |
feat: make React cleanup first-class (#1624)
* feat: make React cleanup first-class * refactor: remove editor integrations * fix: harden React cleanup analysis * fix: detect default export duplication roots * fix: unwrap typed duplication roots * feat: add opt-in project analysis rules * fix: canonicalize project analysis paths * fix: harden project analysis precision * fix: recognize cross-platform project entries * fix: eliminate project analysis false positives * fix: harden project analysis reachability * fix: canonicalize project analysis inputs * fix: resolve project analysis review findings * fix: eliminate residual project analysis false positives * fix: ignore commented registry previews * fix: eliminate project analysis false positives * fix: normalize project analysis paths across platforms * fix: normalize Nextra theme path identity * test: canonicalize convention fixture paths * fix: preserve project analysis provenance * fix: harden project analysis precision * fix: honor project analysis boundaries * fix: recognize conditional config plugins * fix: recognize executable project references * fix: recognize Stencil tool contracts * fix: recognize nested tool references * fix: recognize project setup contracts * fix: recognize generated and local package consumers * fix: recognize static template package references * fix: recognize nested package runtime contracts * fix: close project analysis parser gaps * fix: parse project conventions structurally * refactor: replace structural scanners with parsers * fix: recognize functional Next CSS config * fix: close remaining project analysis gaps * fix: apply tag filters to project analysis * fix: preserve embedded source positions * fix: validate static config helper bindings * fix: bound runtime directory discovery * fix: close final dependency analysis gaps * fix: preserve declaration dependency references * chore: refresh generated rule metadata * fix: make project analysis portable and bounded * test: stabilize cleanup scaling guard * refactor: parse project syntax with oxc * fix: normalize native filesystem paths * fix: separate path identity from report paths * fix: match project files by filesystem identity * fix: match build glob files by package identity * fix: use native path keys for file identity * fix: canonicalize Windows file identities * fix: canonicalize package ownership paths * test: inspect Windows path identities * test: trace Windows package ownership * fix: keep Windows path identities consistent * fix: classify test contracts by normalized path * fix: scope test contracts by canonical package path * fix: keep test package graphs conservative * test: keep React complexity advisory |
||
|
|
51e198db8b |
perf: reuse source inventories across project scans (#1617)
* perf: reuse source inventories across project scans * chore: upgrade Oxc toolchain * fix: fall back for empty shared inventories |
||
|
|
13138a4af5 | refactor: simplify internals across the workspace (#1590) | ||
|
|
3728102af1 | chore: upgrade Oxc toolchain (#1467) | ||
|
|
599e30d9e1 |
fix: share safe built-in control-flow proof across async rules (#1422)
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Aiden Bai <aiden@million.dev> |
||
|
|
3d7ea66c3f | perf: speed up React Doctor scans (#1423) | ||
|
|
7c9dbeda0d | feat(evals): add Daytona corpus parity runner (#1384) | ||
|
|
76cd6bea69 |
perf: reduce cold scan startup and add V8 profiling (#1114)
* perf: reduce cold scan startup and add V8 profiling Add repeatable CPU and heap profiling so cold-run bottlenecks can be measured, then remove profile-proven startup, traversal, and security-scan overhead. * perf: add deterministic stress benchmark Exercise cold scans with reproducible diagnostics and remove redundant visitor-map allocations so regressions are measurable and behavior stays hash-verifiable. * fix(tests): normalize benchmark paths cross-platform Resolve the absolute fixture path through Node so the harness expectation matches Windows path semantics. * perf: avoid effect analysis parent traversal Reuse Oxc visitor keys with a parent-safe fallback so effect scope analysis no longer strips and restores every AST parent reference. * perf: reduce repeated semantic traversal Reuse host visitors and shared analysis caches to avoid redundant cold-scan AST passes while preserving diagnostic parity. * fix: harden performance regression coverage * fix(perf): support profiling on Node 20 * fix(plugin): skip CFGs for bodyless functions * fix: harden profiling and semantic compatibility Prevent benchmark artifacts and host differences from corrupting comparisons, while preserving host AST and React Compiler compatibility across optimized scan paths. * fix(plugin): preserve rule correctness in fast scan paths Keep security prefilters comment-tolerant and resolve React HOC wrappers by binding provenance so scan optimizations cannot hide valid diagnostics. * fix: address review findings and consolidate duplicated code - wrapWithSemanticContext copies the rule's visitors instead of mutating a possibly shared object; walkAst regains its null-root guard - no-multi-comp HOC identity accepts React-compat runtimes via REACT_RUNTIME_MODULE_SOURCES (now incl. @wordpress/element) across ESM, require(), and TS import-equals, with regression coverage in both directions - shared traversal core (forEachChildNode) replaces the walkChildren and containsJsx copies; isImportedFromReact deduped into is-react-api-call - performance harness consolidated (27 -> 22 files): shared commander options, shared profile-frame accumulation, record shape guards, dead BenchmarkSample.profileDirectory removed - CLI-spawning harness tests skip without a built dist; dead and tautological test assertions removed Net -282 LOC against the branch head. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugin): capture the Program root for every rule Rules can reach context.scopes through shared helpers and factories (createDeprecatedReactImportRule resolves namespace aliases via resolveConstIdentifierAlias), so the codegen'd requiresSemanticContext flag — a regex over the rule's own file — silently left factory-based rules on stub scope analyses: no-react-dom-deprecated-apis and no-react19-deprecated-apis stopped reporting namespace usages through the host wrapper. Delete the flag and its regex detector entirely and install the root-capture Program visitor on every rule. The analyses stay lazy and memoized per Program, so rules that never read them still pay only one call per rule per file, and no future helper-routed consumer can be missed. Regression test runs the real host-wrapped rule and fails against the gated wrapper. Fixes cursor bugbot review finding on PR #1114. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scripts): reject cyclic heap-profile node graphs during collection collectNodes flattened the parsed tree with no visited guard, so a cyclic or shared-node object graph would loop before reaching the duplicate-ID check. Unreachable through analyzeHeapProfiles (JSON.parse output is always a strict tree), but guard and throw like the CPU analyzer so a synthetic graph fails deterministically. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0eb5293c1d |
Fix API lint opt-out and deslop traversal cleanup (#1085)
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> |
||
|
|
9cb414905d | fix(oxlint-plugin): precision sweep — narrow 40+ rules against verified FPs from a 67k-diagnostic OSS audit (#1077) | ||
|
|
0748a4ba19 |
test: adversarial fuzzing harness for every rule (@react-doctor/fuzz) (#1022)
Co-authored-by: Aiden Bai <aiden.bai05@gmail.com> |
||
|
|
ea00b1ba14 |
chore: install formatting pre-commit hook on dependency install (#943)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8bbcca87da | chore: vendor deslop-js + deslop-cli into the monorepo (#880) | ||
|
|
ed0258caa2 |
test(oxlint-plugin): green the rule suite and run it in CI (#866)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
22268f70ac |
fix: cap oxlint below 1.67 to stop duplicate Vitest instances in pnpm repos (#791)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
94f9f4fe98 | fix: bump engines.node to ^20.19.0 || >=22.13.0 (#766) | ||
|
|
1ca6f0ead3 | Fix react-doctor npx engine warning (#731) | ||
|
|
915745ef7b |
feat(language-server): editor language server behind react-doctor experimental-lsp (#681)
Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
75c1f99e06 |
fix(oxlint-plugin-react-doctor): declare oxc-parser as a runtime dependency (#630)
Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
e9e71bbc2f |
feat(cli): deepen sentry observability -- source maps, tracing, anonymization, crash refs (#628)
Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
f1913f27f5 |
chore(skills): add truffler symbol-search tooling and deslop skill (#618)
Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
5f7cc7c36e |
feat: publish JSON Schema for react-doctor.config.json (#601)
Co-authored-by: materwelonDhruv <materwelondhruv@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ac14db31e2 | fix(cli): guard startup stdin unref on isTTY so prompts don't exit by themselves (#593) | ||
|
|
07b8a6c3e0 | fix: cross-platform spawn errors (#498, #501) and add Windows/macOS CI (#502) | ||
|
|
9dca7f6a4d | fix | ||
|
|
c944d3e55f | fix | ||
|
|
e642d461cc | fix | ||
|
|
a3539c9afb |
feat(api): new @react-doctor/api package — diagnose() backed by runInspect (#414)
Standing up packages/api/ as the home of the programmatic public API. Moves diagnose() into it as a thin Effect.runPromise shell around #412's runInspect orchestrator, with tagged-error translation back to legacy thrown classes (NoReactDependencyError / ProjectNotFoundError / AmbiguousProjectError) for backwards compat. inspect() stays in react-doctor/src/ for now (CLI rendering coupled in). PR 6 (cli package) moves it. ## Files - packages/api/ (private workspace package) - src/diagnose.ts: pre-resolves rootDir redirect + resolveDiagnoseTarget, then runInspect, translates tagged failures, returns DiagnoseResult. - src/index.ts: re-exports diagnose + public types + legacy errors. - tests/diagnose.test.ts: 4 tests — happy path, NoReactDependency, ProjectNotFound, elapsedMilliseconds positive. - packages/react-doctor/src/index.ts: deletes ~120-line local diagnose, re-exports from @react-doctor/api. - packages/core/src/run-inspect.ts: rejects projects without React via tagged NoReactDependency (used to happen in the legacy diagnose). ## Validation - pnpm typecheck (12/12) - pnpm test — 123 files / 1485 pass / 3 skipped - pnpm lint, format:check, build, smoke:json-report — all green Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
8e0652b910 |
feat(core): stand up 8 Effect v4 services [3/8] (#411)
* feat(core): stand up 8 Effect v4 services (Files, Project, Config, Linter, DeadCode, Score, Reporter, Progress) The architectural backbone for PR 4 (run-inspect orchestrator). Each service is a Context.Service with multiple Layer implementations — the orchestrator yields the service and gets whatever the call-site provides, never knowing whether it's the real Node-backed layer, a test layer with prebuilt diagnostics, or a future LSP / sandbox backend. ## Services | Service | Method | Returns | Layers | |----------|-------------------------------------------------------------------------|-----------------------------------------------|-----------------------------------------------------| | Files | readLines/listSourceFiles/isFile/isDirectory | Effect<...> | layerNode, layerInMemory(Map) | | Project | discover(directory) | Effect<ProjectInfo, ReactDoctorError> | layerNode, layerOf(info) | | Config | resolve(directory) | Effect<ResolvedConfig> | layerNode (Cache.make 16/5min), layerOf | | Linter | run(input) | Stream<Diagnostic, ReactDoctorError, Reporter>| layerOxlint, layerOf([]), layerComposite([...]) | | DeadCode | run(input) | Stream<Diagnostic, ReactDoctorError> | layerNode, layerOf([]) | | Score | compute(input) | Effect<ScoreResult \| null> | layerHttp, layerOf(result) | | Reporter | emit / partialFailure / finalize | Effect<void> | layerNoop (prod), layerCapture (test), layerNdjson | | Progress | start(text) -> ProgressHandle | Effect<ProgressHandle> | layerOra(factory), layerCapture, layerNoop | Project translates legacy class throws from `discoverProject` (NoReactDependencyError, ProjectNotFoundError, PackageJsonNotFoundError, AmbiguousProjectError) into the tagged-error vocabulary added in PR 1. DeadCode wraps `checkDeadCode` into a Stream — failures emit a DeadCodeAnalysisFailed leaf instead of throwing. ## Linter wraps the runOxlint subprocess Linter.layerOxlint is the only production backend today. It wraps runOxlint (now raising tagged errors after PR 2) into a Stream and routes per-batch partial failures through Reporter.partialFailure (replaces PR 304's separate LintPartialFailures service — Reporter is the single side-channel for all "things happened" events). HACK: runOxlint's onPartialFailure callback uses Effect.runSync to push into the reporter, because the callback is sync-shaped. Documented; follow-up turns runOxlint into a Stream natively. ## No "layerNoop" for analyzers Per the plan: Linter and DeadCode use `layerOf([])` for "no diagnostics" since the semantics is "empty output", not "discard input". Reporter and Progress keep `layerNoop` (they have void return + side-effect-only semantics where "discard" is the correct verb). ## Test infrastructure packages/core/ now has its own test script (vp test run) and vite.config.ts test section. Root pnpm test expands to include both react-doctor and @react-doctor/core via turbo --filter chain. Each service gets a focused test file in packages/core/tests/services/. Layer-driven (no vi.mock anywhere): tests provide the relevant layerOf/layerInMemory/layerCapture and assert against Refs or stream collections. ## Validation - pnpm typecheck (10/10) - pnpm test — 121 files, 1471 pass / 3 skipped (was 113/1442; +8 files, +29 new tests across the 8 services) - pnpm lint, format:check (1135 files) - pnpm build (7/7; core dist grew from 131kB -> 145kB) - pnpm smoke:json-report — schema-valid v1 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(core): Files.layerInMemory.isDirectory rejects file paths Bugbot caught: `absolute === filePath` made isDirectory return true for any file in the tree (e.g. isDirectory('/repo/src/index.ts') returned true). Drop the equality branch — only descendant prefix matching is semantically correct for inferring directory status from a file-only Map. Adds explicit regression test. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
83a70de91e |
feat(core): Effect v4 foundation — tagged errors, schemas, refs, paths (#405)
First PR of the Effect v4 rewrite. Adds the architectural primitives
that subsequent PRs build on, while preserving every public contract
(inspect(), diagnose(), CLI flags, JSON schemaVersion: 1, GitHub Action).
## What lands
- packages/core/src/errors.ts — 9 leaf Schema.TaggedErrorClass
(OxlintUnavailable, OxlintBatchExceeded, OxlintSpawnFailed,
OxlintOutputUnparseable, ConfigParseFailed, ProjectNotFound,
NoReactDependency, AmbiguousProject, DeadCodeAnalysisFailed) composed
into ReactDoctorError. Helpers formatReactDoctorError /
isReactDoctorError / isSplittableReactDoctorError (all _tag-based,
zero string-grepping).
- packages/core/src/schemas.ts — Diagnostic + Severity + JsonReport
(Schema.Union for forward-compat with a future v2) +
buildDiagnosticIdentity. Schemas mirror the existing @react-doctor/types
interfaces; ProjectInfo / ScoreResult stay Schema.Unknown until PR 3.
- packages/core/src/refs.ts — Context.Reference for env-derived ambient
config (OxlintSpawnTimeoutMs, OxlintOutputMaxBytes,
StagedFilesTempDirPrefix). Tests override via Layer.succeed.
- packages/core/src/paths.ts — Schema.brand for OxlintBinaryPath +
NodeBinaryPath. Catches the swap at compile time.
- packages/core/src/constants.ts — hoisted OXLINT_SPAWN_TIMEOUT_MS from
the inline IIFE in run-oxlint.ts with explanatory docstring.
## Wiring
- handle-error.ts and build-json-report-error.ts dispatch to the
tagged-error message getter when isReactDoctorError(error), else
fall back to the existing formatErrorChain. run-oxlint.ts still
throws plain Errors; PR 2 converts it.
- scripts/smoke-json-report.ts runs the built CLI against
tests/fixtures/basic-react and Schema.decodeUnknownSync's the stdout
against the new JsonReport schema. New CI step (must stay green
through PR 8). Verified locally that a full --no-offline run with
263 real diagnostics decodes cleanly.
## Effect v4 deps
- effect@4.0.0-beta.70 in packages/core/dependencies and
packages/react-doctor/dependencies. Marked neverBundle in
packages/react-doctor/vite.config.ts (~1MB+ of tree-shakable source;
installers pull it as a regular dep — matches react-doctor-evals).
- @effect/vitest@4.0.0-beta.70 as a devDependency of core for PR 3+.
## Patterns
Every new file matches react-doctor-evals conventions exactly:
- import * as X from "effect/X" (never the umbrella import)
- Schema.TaggedErrorClass<Self>()("Tag", { fields }) with get message()
delegating to Cause.pretty(Cause.fail(this.cause)) for opaque causes
- Context.Reference<T>("react-doctor/X", { defaultValue }) with env-var
reads in defaultValue
- Schema.brand("X") via .pipe()
- kebab-case file names (per AGENTS.md)
## Validation
- pnpm typecheck — 10/10 tasks green
- pnpm test — 113 files, 1442 pass / 3 skipped (up from 1198; +244 new
tests in errors.test.ts and schemas.test.ts)
- pnpm lint — clean
- pnpm format:check — clean across 1119 files
- pnpm build — all 7 packages produce dist/
- pnpm smoke:json-report — schema-valid v1 JsonReport
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
47772b7da4 |
feat(oxlint-plugin): native TypeScript ports of OXC react/* + jsx-a11y/* + react-hooks + you-might-not-need-an-effect (108+ rules) (#273)
* feat(oxlint-plugin): port OXC react/* + jsx-a11y/* (100 rules) and effect-rules (8 rules) onto main
Squashed-rebase of cursor/port-oxc-react-rules-1778917290 onto current main.
Covers all 30 commits previously on the branch:
- Native port of every `oxc_linter::rules::react` (44),
`react_perf` (4), and `jsx_a11y` (52) rule into
`oxlint-plugin-react-doctor` (`react-builtins/` and `a11y/`
buckets), driven by an oxc-parser harness running OXC's own
fixture vec.
* 5411 / 5574 fixture cases pass (97.1%).
* 163 documented divergences in `__fixtures__/oxc-divergences.ts`
(per-rule).
- Semantic infrastructure: `scope-analysis.ts`,
`control-flow-graph.ts`, `closure-captures.ts`, plus
`wrap-with-semantic-context.ts` lazy injection.
- 8 ported `react-doctor/*` effect rules from
`eslint-plugin-react-you-might-not-need-an-effect` (PR #278), with
the eslint-scope analyzer + 1:1 ports of upstream's
`util/{ast,react}.js`.
- Drop OXC's `react` + `jsx-a11y` plugins from oxlintrc;
`BUILTIN_REACT_RULES` / `BUILTIN_A11Y_RULES` /
`YOU_MIGHT_NOT_NEED_EFFECT_RULES` are now empty maps preserved
for back-compat with consumers that import them.
- Drop `eslint-plugin-react-you-might-not-need-an-effect` peer dep
from `@react-doctor/core` and `react-doctor`.
- Drop `resolveYouMightNotNeedEffectPlugin` from plugin-resolution.ts.
Adopts main's structural changes since branch creation: PR #277
(rule re-exports moved into oxlint-plugin-react-doctor), PR #284
(picomatch glob compiler), PR #281 (annotations input on action.yml),
PR #282 (PR-blocking docs), PR #283 (knip removal docs).
Verification:
* pnpm typecheck — 10/10 packages clean
* pnpm lint — 0 warnings, 0 errors (951 files)
* pnpm test — 1350 passed | 4 skipped (oxlint-plugin: 5411 / 5574,
+149 fixtures vs the fresh squash baseline)
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* feat(oxlint-plugin): upstream-parity test suite for rules-of-hooks + exhaustive-deps
Drives every `valid:` / `invalid:` case from the React team's
`eslint-plugin-react-hooks` v7 test fixtures
(`__tests__/ESLintRulesOfHooks-test.js` +
`__tests__/ESLintRuleExhaustiveDeps-test.js`) through our native
ports via the `runRule` harness.
Captured fixtures (committed JSON in
`src/plugin/rules/react-builtins/__upstream-fixtures__/`):
- rules-of-hooks: 58 valid + 77 invalid = 135 cases
- exhaustive-deps: 122 valid + 191 invalid = 313 cases
Total: 448 upstream cases.
Result:
- 241 / 448 upstream cases pass on our native port (53.8%).
- 207 documented divergences in `divergences.ts`. The largest gap is
in exhaustive-deps (135 invalid + 31 valid skipped) — upstream's
port has decade-old refined heuristics for useState-setter /
useRef stable-identity detection, useEffectEvent hoisting, deep
TS-aware unwrapping (typeof + as casts + satisfies), useMemo /
useCallback dep-array suggestion text, and React 19 `use()`
semantics inside dep arrays — none of which are replicated yet.
rules-of-hooks gaps: Flow `component` / `hook` syntax, classes
with hooks detection, useEffectEvent placement rules, deep
conditional/loop patterns from upstream's hermes-eslint scope walker.
Note on the "port all eslint-plugin-react-hooks rules" ask:
- `exhaustive-deps` and `rules-of-hooks`: native ports landed via
the OXC port; this commit adds upstream-fixture-driven parity
tests.
- The 16 React Compiler rules (`set-state-in-render`, `immutability`,
`refs`, `purity`, `hooks`, `set-state-in-effect`, `globals`,
`error-boundaries`, `preserve-manual-memoization`,
`unsupported-syntax`, `static-components`, `use-memo`,
`void-use-memo`, `incompatible-library`, `todo`,
`component-hook-factories`) are NOT individual ESLint rules — they
are dispatcher rules that run `babel-plugin-react-compiler`
internally and report its diagnostics filtered by category. These
CANNOT be ported natively without bundling the React Compiler.
React Doctor already loads them as external `react-hooks-js/*`
via `eslint-plugin-react-hooks` when React Compiler is in scope.
Test totals: 5411 → 5652 passing (+241 from the new parity suite),
163 → 370 skipped.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix(oxlint-plugin): rules-of-hooks gains broad upstream parity (24 → 19 divergences, 100% on valid cases)
After deep review of every divergent upstream test case:
`isHookCall` now matches upstream's stance:
- Bare `use*` callees are hooks unless they are parameter / catch-clause
bindings (the React-import filter is reserved for the React 19 `use`
hook only — that one is too general to flag without resolving to React).
- Member-expression hook calls fire on PascalCase namespaces or
call-expression chains (`Hook.useState`, `This.useHook`,
`FooStore.useFeatureFlag`, `someCall().useFoo`). Lowercase
namespaces (`jest.useFakeTimers`, `this.useHook`, `super.useHook`)
and non-hook-named members are NOT flagged.
`isInsideClassComponent` now correctly walks past class-method boundaries:
class-member function expressions don't terminate the walk, so
`class C { m() { useState() } }` is flagged.
Anonymous-function fallback now walks OUT to the enclosing context
instead of skipping unconditionally — when the outer function is a
component / hook, the inner anonymous callback's hook call is flagged
(catches `useEffect(() => { useHookInsideCallback() })` patterns).
`inferFunctionName` traverses transparent wrapper nodes
(AssignmentPattern for destructure defaults, TS as / satisfies /
non-null, ChainExpression) so cases like
`const {j = () => useState()}` correctly resolve to "j".
ExportDefaultDeclaration anonymous functions return null name
(treat as truly anonymous) — matches upstream's deliberate
non-enforcement on `export default () => {}`.
`use()` inside try/catch now flagged separately from the
conditional/loop checks (the React 19 `use` hook is allowed in
conditionals but NOT in try/catch).
CFG `computeUnconditionalSet` now treats:
- Dead-code blocks (statements after an unconditional return) as
vacuously unconditional — they're never reached so the rule
doesn't apply.
- ThrowStatement-to-exit edges as type "throw", excluded from the
reachability BFS — `if (x) throw; useState();` correctly evaluates
as unconditional because the throw branch isn't a normal completion.
OXC fixture pass[10] (`Sinon.useFakeTimers`) skipped: OXC's
pass-stance conflicts with upstream's flag-stance for
PascalCase-namespaced use-prefixed calls. We match upstream.
Upstream parity: 94 → 116 passing of 135 (was 70%, now 86%). All
valid cases pass. 19 remaining invalid divergences are useEffectEvent
placement (16) — separate rule layer not yet implemented — and
Flow-syntax (3) which require hermes-eslint.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix(oxlint-plugin): exhaustive-deps gains broad upstream parity (+22 fixtures)
Deep review of every divergent upstream test case:
`symbolHasStableHookOrigin` extended to cover the full upstream
stable-hook-origin set:
- useEffectEvent return values (RFC stable callback)
- Primitive literal initializers (number / string / boolean / null /
no-substitution template) on `const` bindings — `let`/`var`
bindings remain treated as mutable.
`isOutsideAllFunctions` walks the scope chain looking for ANY enclosing
function, so block-at-module-level constants (`{ const x = {}; useEffect... }`)
are correctly classified as stable. Imports / module-scope values are
NOT added to `stableCapturedNames` so the unnecessary-dep check still
fires for redundant imports listed in deps.
`unwrapExpression` strips TS `as` / `satisfies` / non-null / type-assertion
wrappers as well as `(...)` parens and ChainExpression — applied
both to the deps-array argument itself (so `[deps] as const` is
seen as an array) and to individual elements (so `[(props.x as Foo)]`
canonicalizes to `props.x`).
`computeDepKey` walks through ChainExpression wrappers when
collecting the outermost member-chain, so `props.foo?.bar` and
`props.foo.bar` both produce the same canonical key.
`stringifyMemberChain` standalone helper handles ThisExpression and
optional / TS-wrapped member chains.
New diagnostic surface:
- `buildMissingCallbackMessage`: `useEffect()` etc. with no callback.
- `buildMissingDepArrayMessage`: useMemo / useCallback / useImperativeHandle
without a deps array.
- `buildNonArrayDepsMessage`: a non-array second argument
(`useEffect(fn, dependencies)`).
- `buildLiteralDepMessage`: deps-array contains a non-string literal
(`[42, false, null]`). String literals are deliberately skipped —
upstream emits the missing-dep hint for those instead.
- `buildDuplicateDepMessage`: same dep listed twice (`[local, local]`).
- `buildRefCurrentDepMessage`: `<ref>.current` listed in deps where
`<ref>` resolves to a useRef binding — upstream's "depend on the
ref itself, not its mutable .current" warning.
null / undefined deps argument now treated as "no deps" for
useEffect-style hooks (silently OK) and as "missing deps array" for
useMemo / useCallback / useImperativeHandle.
`stableCapturedNames` set tracks bindings that the callback DID
capture but that we filtered out of the dep-keys for stability. The
unnecessary-dep check uses this set to suppress reports on
legal-but-redundant deps (e.g. `[local1]` where `local1 = 42` is
literal-stable).
CFG `computeUnconditionalSet` now treats:
- Dead-code blocks (statements after an unconditional return) as
vacuously unconditional.
- ThrowStatement-to-exit edges as a separate `throw` edge kind,
excluded from the reachability BFS — matches upstream's
"if (x) throw; useState();" → unconditional semantics.
Ported divergence count: 31 valid + 135 invalid = 166 fixed →
19 valid + 125 invalid = 144 documented divergences (-22 net).
Test totals: 5673 → 5695 passing (+22).
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* refactor(oxlint-plugin): tighten code quality on hooks rules per AGENTS.md
- Hoist inline type imports (`import("...").Foo`) to top-level imports
- Replace ASCII char-code magic numbers (65/90) with named constants
- Drop unused `HookContext.hookExpression` field
- Lift HOC-name set, transparent-wrapper-type set, and required-deps
hook sets to module-level `ReadonlySet` constants
- Rename short variables to descriptive names (decl→declarator,
init→initializer, obj→objectName, out→indices, etc.)
- Replace `A ? true : false` with `Boolean(A)`
- Inline trivial intermediate variables; tighten control flow
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix: address bugbot findings on PR #273
- Restore the `customRulesOnly` gate on rules ported from OXC's
`react/*` and `jsx-a11y/*` plugins. The generated registry now
carries an `originallyExternal: true` flag for each rule in the
`react-builtins` and `a11y` buckets; `createOxlintConfig` filters
those out when the user opts into `customRulesOnly`. Without this,
users who set `customRulesOnly: true` would have started receiving
~26 OXC-equivalent rules they explicitly opted out of.
- Drop the dead `BUILTIN_REACT_RULES` / `BUILTIN_A11Y_RULES` imports +
spreads from `createOxlintConfig`. Both maps are permanently empty
now that the rules are natively ported, so the
`customRulesOnly ? {} : MAP` ternaries always resolve to `{}`.
- Rename `isValidAriaProperty` in `dom-aria-properties.ts` →
`isValidDomAriaProperty` to disambiguate from the spec-strict
case-sensitive variant in `aria-properties.ts`. The DOM variant
remains case-insensitive (HTML attributes are case-insensitive) and
is the right helper for `no-unknown-property`; the spec-strict one
stays in `aria-props` for exact-match validation.
- Switch `wrap-with-semantic-context`'s `fallbackCfg` to return
`false` from `isUnconditionalFromEntry` / `dominatesExit`. The
fallback is unreachable in practice (the wrapper captures the
Program root before any visitor reads `context.cfg`), but if it
ever fires, `false` errs toward flagging a potential violation
instead of silently passing every hook call as unconditional.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix: address bugbot findings on PR #273 (round 2)
- extract-oxc-fixtures.mjs: `upstreamRelative` was computed as
`path.relative(path.dirname(oxcFilePath), oxcFilePath)`, which always
yielded just the filename. The header comment in each generated
fixture file therefore claimed `crates/oxc_linter/src/rules/<file>.rs`
— missing the bucket subdirectory. Compute the relative path against
`<oxcRoot>/crates/oxc_linter/src/rules` instead so the comment shows
`react/no_array_index_key.rs`, etc.
- aria-roles.ts: `"row"` was simultaneously listed in
`INTERACTIVE_ROLES` and `NON_INTERACTIVE_ROLES`. The is-interactive /
is-non-interactive checks would both return `true`, breaking
classification logic in a11y rules. Upstream `eslint-plugin-jsx-a11y`
classifies `row` as interactive (user-navigable inside a grid /
treegrid) and `rowgroup` as non-interactive — drop `row` from
`NON_INTERACTIVE_ROLES`.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* feat(oxlint-plugin): pin rules-of-hooks + exhaustive-deps to upstream eslint-plugin-react-hooks@7.1.1
Re-extracts upstream's RuleTester fixtures from
`facebook/react@eslint-plugin-react-hooks@7.1.1` and replays them
through our native TypeScript ports of the two pure-JS rules in the
plugin (`rules-of-hooks`, `exhaustive-deps`). The 16 React Compiler
rules the same package ships are runtime wrappers over
`babel-plugin-react-compiler`'s HIR analyzer (~30k LoC of bundled
compiler output) and aren't portable to a visitor-only TypeScript
plugin — those continue to ship via the optional peer dep on the npm
package, see plugin-resolution.ts.
Changes:
- scripts/extract-react-hooks-tests.mjs: NEW. Re-creates the upstream
fixture extractor that was lost in the squash-rebase. Reads any of
upstream's `allTests` / `tests` / `testsFlow` / `testsTypescript`
/ `testsTypescriptEslintParserV4` globals, dedupes by
`kind:code:JSON.stringify(options)`, and writes JSON. Sets
`process.env.CI=1` so upstream's not-in-CI filter (which deletes
`skip` flags from cases) is bypassed — we want every case the
upstream test suite asserts.
- exhaustive-deps.ts: `flattenReferenceRootName` now accepts
`JSXIdentifier` references, not just `Identifier`. This unlocks the
v7.1.1-added test case `<Component />` JSX usage inside an effect's
callback being detected as a missing dep. Verified locally:
invalid #191 (the new `function Foo({ Component }) { useEffect(() =>
console.log(<Component />), []) }`) now reports the correct missing
dep.
- rules-of-hooks.ts: `buildNonComponentMessage` now mirrors v7.1.1's
expanded diagnostic — appends 'React component names must start with
an uppercase letter. React Hook names must start with the word
"use".' This matches the changelog-noted message expansion in the
release.
- __upstream-fixtures__/README.md: NEW. Documents the v7.1.1 source
pin, the regeneration command, and a summary of why the
Compiler-backed rules aren't in scope for native porting.
- package.json: NEW `gen:react-hooks-fixtures` script.
- exhaustive-deps.json: regenerated; +1 case (the JSX-Component case)
and minor formatting changes.
Verification:
- pnpm format/lint/typecheck/test all green.
- Upstream parity scoreboard unchanged net of the new case (now
passing): 286 passed / 163 skipped (449 total upstream cases).
- Skip lists in divergences.ts unchanged — the skipped categories
(Flow `component`/`hook`, useEffectEvent placement, deep TS
type-aware unwrapping, mutation tracking, composite error counts)
remain documented as fundamental visitor-only limitations.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix(oxlint-plugin): scope-analysis records JSX-member-chain object as a reference
After exposing JSXIdentifier captures to the exhaustive-deps rule
(commit
|
||
|
|
f0198e2f2d | refactor: extract @react-doctor/{types,project-info,core} internal packages + cleanup (#249) | ||
|
|
4d622970b5 |
Revert "pub"
This reverts commit
|
||
|
|
4d9d1a5fcf | pub | ||
|
|
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. --------- |
||
|
|
914e5f3aee |
feat: leaderboard in README + dedicated /leaderboard page (#176)
* docs(readme): add leaderboard section with top 10 from benchmarks repo Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * ci(leaderboard): refresh README leaderboard from benchmarks json on schedule Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * feat(website): add /leaderboard page driven by react-doctor-benchmarks Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * chore(scripts): drop bench:scores in favor of leaderboard.json Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * docs(readme): trim leaderboard table to repo + score Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * docs(readme): drop raw-results link from leaderboard section Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * fix(scripts): make update-leaderboard idempotent against formatter Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * refactor(website): extract score thresholds, color, label, doctor face to shared utils 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> |
||
|
|
b3de062cc5 |
chore: upgrade oxlint to 1.63.0 via override (#175)
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> |
||
|
|
6afdc04ba7 | chore(package): add benchmark scores script to package.json | ||
|
|
d71a6bf6eb |
feat(react-doctor): adopt user lint config, ship as ESLint plugin, remove browser surface (#151)
Squashes work that closes #143 plus follow-on cleanup of the browser surface and dead-code. **`adoptExistingLintConfig` (default-on)** — when a project has a JSON-format `.oxlintrc.json` or `.eslintrc.json` at the scanned directory or any ancestor up to the nearest project boundary, that config is folded into the same scan via oxlint's `extends` field. Diagnostics from the user's existing rules count toward the 0–100 health score alongside the curated react-doctor rule set; if oxlint can't load the user config, react-doctor logs the reason on stderr and retries the scan once without `extends`. JS / TS configs are silently skipped (oxlint's `extends` can't evaluate them). Coverage broadened to `.ts` and `.js` files (previously the parser dropped non-JSX). Closes #143. **`react-doctor/eslint-plugin` flat-config export** — the same react-doctor rule set is shipped as an ESLint flat-config plugin so projects that already run ESLint can wire it up without depending on the CLI. Includes presets (`recommended`, `next`, `react-native`, `tanstack-start`, `tanstack-query`, `all`) plus cherry-picking. **Removed browser entrypoints, browser CLI, and `react-doctor-browser` package** — the in-browser diagnostics pipeline (`react-doctor/browser` + `/worker`), the `react-doctor browser …` CLI subcommand (`start` / `stop` / `status` / `snapshot` / `screenshot` / `playwright`), and the standalone `react-doctor-browser` workspace package (Playwright + CDP + cookie extraction) are gone. Nothing in the monorepo consumed them and they pulled in a heavy dep footprint (`playwright`, `libsql`). Source is preserved on the `archive/browser` branch. **`diagnose-core` engine inlined into `index.ts::diagnose`** — the dependency-injection shape was designed for browser sharing. Only one caller now, so collapsed into a flat orchestrator. Score helpers moved from `src/core/` to `src/utils/`; `src/core/` directory removed. **Smaller cleanups** — drop unused `matchGlobPattern` wrapper, `isMemberProperty` import in nextjs rule, `fileContainsPattern` helper, and various dead test locals; ignore `tests/fixtures/**` from workspace lint and add `.oxlintignore` so `lint --fix` against fixture paths can't strip the `debugger;` statements that `adoptExistingLintConfig` tests assert on. Tests: 452 / 452 pass on Node 22 + 24. Build smoke green. |
||
|
|
3f5c031474 |
feat(react-doctor): add browser CLI subcommand and 11 new lint rules (#148)
|
||
|
|
03a94351c7 | Address review-report.md findings + self-review regressions (#140) |