43 Commits

Author SHA1 Message Date
Aiden Bai 922616f08d perf: keep TypeScript, git, and worker boot off the scan's critical path (#1803)
Seventeen measured, byte-identical speedups: pool-aware batch planning, interned sidecar probes shared with idle workers, one sized source listing, plugin pre-import at worker boot, oxc raw transfer for cross-file parses (JSON fallback on Windows), concurrent cache-identity git calls, lazy conf/prompts/agent-install/TypeScript, a scan preamble that starts git, the oxlint workers and a React Compiler detection worker before the CLI bundle evaluates, JSONC tsconfig parsing, and a text gate on the auto-import config parse. ant-design lint: no-cache -20%, cold -45%, hot -37%; stress project: -26% / -16% / -17%. Parity over 14 repositories: 0 added, 0 removed.
2026-09-13 23:44:28 +08:00
Aiden Bai dfcde1035a perf: persistent oxlint worker pool, parent-side overlap, and plugin hot-path cuts (2.4–5.5x on the large-repo corpus) (#1781) 2026-09-09 22:59:45 +08:00
Aiden Bai 013f7373f9 fix(action): clean npm output from JSON reports (#1714) 2026-08-29 21:16:01 -07:00
Aiden Bai 2b0f06ec70 perf: profile and optimize rule execution (#1663) 2026-08-20 22:05:20 -07:00
Aiden Bai 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
2026-08-13 16:26:08 -07:00
Aiden Bai 13138a4af5 refactor: simplify internals across the workspace (#1590) 2026-08-08 00:20:14 -07:00
Aiden Bai 60e4d8e102 fix(cli): isolate interactive React runtime (#1577)
Bundle the Ink and React renderer so inspected projects cannot supply a missing or incompatible React runtime. Exercise the packed CLI in a real PTY against a React 18 fixture.
2026-08-04 15:01:08 -07:00
Aiden Bai 48ec9a8020 fix(cli): keep large workspace scans responsive (#1549)
* fix: keep large workspace scans responsive

* fix: harden large workspace scans

* test: normalize workspace path on Windows

* fix: resolve workspace exclusions after root redirects

* fix: honor workspace scan boundaries

* fix: keep bounded workspace scans responsive

* fix: preserve dead-code reachability

* refactor: tighten workspace scan controls

* fix(cli): bound staged workspace scans

* fix: eliminate pathological large-file scans

* fix: handle incomplete and conflicted scans

* test: configure conflict merge identity

* perf: cache repeated module resolution probes

* fix(cli): show all incomplete scan warnings

* fix(core): preserve cooperative scan semantics

* perf(cli): streamline workspace scan startup

* perf(cli): harden large workspace scans

* fix(cli): keep scan progress inline

* fix(scan): correct workspace result completeness

* fix(cli): simplify live scan status

* fix(core): make scan deadlines opt-in

* fix(scan): preserve complete workspace results

* perf(scan): accelerate very large workspaces

* fix(scan): yield CPU to foreground work

* perf(scan): avoid unnecessary project indexing

* fix(cli): keep final report inline

* test(cli): stabilize terminal recording setup

* fix(core): honor scan deadline during index probe

* fix(core): skip lint after incomplete index probe

* fix(core): preserve scoped lint ownership

* fix(cli): restore project picker on cancellation
2026-08-03 04:59:45 -07:00
Aiden Bai c3683a2850 feat(cli): make the interactive report the default (#1542)
Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com>
2026-08-02 01:09:19 -07:00
Aiden Bai 1d01dd33d1 fix(ci): stabilize matrix-sensitive tests 2026-07-31 02:18:32 +00:00
Aiden Bai 3f3197f8e3 fix(cli): improve interactive startup and short-terminal TUI (#1426) 2026-07-26 21:00:36 -07:00
Skosh 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>
2026-07-22 04:15:57 -07:00
Aiden Bai 5915a58231 chore(delta-audit): refresh baseline after precision fixes [skip ci]
Refresh the pinned delta-audit baseline after reviewed detector precision changes. The corpus, pinned SHAs, thresholds, and runner are unchanged.
2026-07-15 11:21:29 -07:00
Aiden Bai 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>
2026-07-11 21:40:52 -04:00
Aiden Bai 938008119a feat(report): add versioned JSON v3 metadata (#1120)
* feat(report): add versioned JSON v3 metadata

Expose deterministic diagnostic identities and exact scan completeness while preserving v1 and v2 decoding.

* fix(report): make v3 identities and coverage exact

Keep workspace diagnostics distinct and align JSON completeness, telemetry verdicts, and fallback output with the versioned report contract.
2026-07-10 23:49:19 -07:00
Aiden Bai 9cb414905d fix(oxlint-plugin): precision sweep — narrow 40+ rules against verified FPs from a 67k-diagnostic OSS audit (#1077) 2026-07-07 18:35:55 -07:00
Ray Arayilakath 88a5c3cb49 fix(action): surface degraded baseline scans + scaffold full-history checkout (#1019)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 05:09:28 -04:00
devin-ai-integration[bot] 4728f2da91 FABLE_CLEANUP: fix(action): 20-day audit fixes for the GitHub Action and workflows (#1015)
Co-authored-by: Aiden Bai <aiden.bai05@gmail.com>
Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com>
2026-07-02 01:29:50 -07:00
Ray Arayilakath 0b4f4f4bd2 fix(action,core): address Cursor Bugbot findings on the CI-speedup PR (#909)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 00:06:17 -04:00
Ray Arayilakath 2cadd3fe2c perf(action,core): CI speedups — install cache, persistent scan caches, local diff scope (plans 09–11) (#908)
* feat(action,core): CI speedups — install cache, persistent scan caches, local diff scope (plans 09–11)

The GitHub Action's dominant cost on a PR run is the uncached install (~15s of
an ~18s step), not the scan. These three plans target the CI experience:

Plan 09 — cache the install (biggest CI win). A resolve-version step pins the
concrete published version (so the cache key is stable even for `latest`;
scripts/resolve-package-spec.mjs), an actions/cache step restores the install
keyed on version+node+os+arch (no fuzzy fallback — native ABI safety), and the
scan installs into the cached `--prefix` only on a miss. A non-cacheable
local-path spec keeps the npx path. ~15s install → ~1-2s restore on a hit.

Plan 11 — derive PR changed files locally + lock the diff fast path. The base
step now runs `git diff --name-only --diff-filter=AMR <base>...HEAD` (faster, no
API rate limit, works on forks), falling back to the GitHub API only when the
base isn't reachable; both share scripts/normalize-changed-files.mjs. A
regression test locks that diff mode skips dead-code + supply-chain (the
fast-path guarantee). A clearer degraded-mode warning points at `fetch-depth: 0`.

Plan 10 — persist scan caches across CI runs. `REACT_DOCTOR_CACHE_DIR` lets the
action point the engine's caches at a stable `${runner.temp}` path an
actions/cache step persists, so the per-file content-addressed lint cache (#900)
restores across commits — a PR re-lints only its changed files. A new
supply-chain per-PURL on-disk cache (24h TTL, fail-open, NO_CACHE-bypassed) skips
the Socket network for unchanged deps.

Unit/integration tests cover all three (version classification, changed-file
normalization, diff-fast-path skip, cache-dir override, supply-chain cache
hit/bypass); changeset for the npm-facing surface (REACT_DOCTOR_CACHE_DIR +
supply-chain cache). Action releases (tags) + the self-test cacheable-install
job (a workflow-file change needing `workflow` push scope) + R3 sparse-checkout
docs + plan-09's `--print-cache-key` follow-up are noted for a workflow-scoped
push / dogfooding pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(action): self-test the cacheable install path (plan 09)

Adds an `action-cacheable-install` job that runs the action with a published
`version:` (latest) so CI exercises the resolve-version + actions/cache +
prefix-install branch (the local-path job covers the npx branch). Advisory
(`blocking: none`). Split out from the main plan-09 commit because pushing a
`.github/workflows/` change requires `workflow` token scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cli): migrate mutable @main action refs to @v2 (action change → migration)

Since plans 09-11 change the action, register a once-per-repo project migration
(the framework's "action updates" path) that pins a mutable `@main` / `@master`
React Doctor action reference in `.github/workflows/*.yml` to the recommended
floating major `@v2`.

An unpinned `@main` runs whatever the action's HEAD points to with the
workflow's write permissions — a supply-chain risk (#299) — and the rewrite also
moves the workflow onto the install- and scan-cached release. Only mutable refs
are rewritten; pinned tags / SHAs are deliberate and untouched, a different
action on `@main` is ignored, and only the ref changes (owner, comments, and the
`version:` input are preserved). Runs once per repo like the legacy-config
migration and logs the change for review/commit (or revert if intentionally
tracking main). No-op (stays pending) when there's no mutable ref.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(action): update the action contract test for the plans 09-11 restructure

github-action.test.ts asserts the literal content of action.yml's steps; plans
09/11 moved that content into shared scripts, so two assertions broke:
- the inline `directoryPrefix` prefix-stripping is now in the shared
  normalize-changed-files.mjs (used by both the local-diff base step and the API
  fallback) — assert the wiring instead, with the behavior locked by
  normalize-changed-files.test.ts.
- the inline `PACKAGE_SPEC="react-doctor@$INPUT_VERSION"` derivation moved to the
  resolve-version step (resolve-package-spec.mjs) — assert it's read from that
  step's output.
Adds a test for the new contract (resolve-version + the toolchain/scan
actions/cache steps + the cached prefix-install). Behavior unchanged; the
contract test now matches the reworked action.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 23:38:48 -04:00
Aiden Bai 8bbcca87da chore: vendor deslop-js + deslop-cli into the monorepo (#880) 2026-06-19 01:02:54 -07:00
Ray Arayilakath 242bf69451 ci: action version-bump guard + faster CI (turbo remote cache, concurrency) (#885)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 01:49:22 -04:00
Ray Arayilakath 740211cf20 feat(action): skip the PR comment + mark the status "skipped" when no React files change (#863)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:20:34 -04:00
Aiden Bai eacdcf2e65 Add project-level security scan rules (#744)
Co-authored-by: Aiden Bai <aiden.bai05@gmail.com>
Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com>
2026-06-13 03:57:25 -07:00
Aiden Bai 1ca6f0ead3 Fix react-doctor npx engine warning (#731) 2026-06-09 18:23:20 -07:00
Ray Arayilakath 9a8ad6e40d feat(ci): blocking gate, PR-introduced-issues baseline, inline review comments + CLI flag cleanup (#663)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 17:56:22 -04:00
Aiden Bai cbdff6203d feat(cli): "Add to CI" handoff option + default CI setup in install (#658)
Co-authored-by: Aiden Bai <aiden.bai05@gmail.com>
Co-authored-by: Nisarg Patel <awesomenisarg@gmail.com>
2026-06-04 04:15:05 -07:00
Ray Arayilakath 75c1f99e06 fix(oxlint-plugin-react-doctor): declare oxc-parser as a runtime dependency (#630)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 07:02:17 -04:00
Ray Arayilakath e9e71bbc2f feat(cli): deepen sentry observability -- source maps, tracing, anonymization, crash refs (#628)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 06:19:25 -04:00
Ray Arayilakath 9777f1ac45 feat(cli): add rules management commands and doctor-explain skill (#617)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 05:55:47 -04:00
Ray Arayilakath b612664043 fix(action): degrade to full-project scan when PR files are unreadable (#611)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 03:04:46 -04:00
Ray Arayilakath 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>
2026-05-30 18:27:08 -04:00
Aiden Bai ac14db31e2 fix(cli): guard startup stdin unref on isTTY so prompts don't exit by themselves (#593) 2026-05-29 21:11:50 -07:00
Ray Arayilakath f4035fce9a refactor(action): rebuild PR reporting flow (#577)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:24:17 -04:00
Ray Arayilakath 4e23e5dd34 fix(ci): point smoke test at core fixture (#486)
* fix(ci): point smoke test at core fixture

Co-authored-by: Ray Arayilakath <me@rayhanadev.com>

* style: format JSON smoke fixture path

Co-authored-by: Ray Arayilakath <me@rayhanadev.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-05-25 16:37:29 -04:00
Aiden Bai 26b66b45d5 0.2.4 2026-05-24 02:49:37 -07:00
Aiden Bai e501cc693d fix 2026-05-24 00:05:15 -07:00
Aiden Bai aa61efae28 refactor: collapse @react-doctor/types and @react-doctor/project-info into core (#440)
* refactor: collapse @react-doctor/types and @react-doctor/project-info into core

Eliminates two private workspace packages whose only role was to
namespace shared TS types and project-discovery helpers — neither
was a real consumer boundary, and the indirection cost a `workspace:*`
edge per leaf package without saving any code.

**Layout**

- `packages/types/` → `packages/core/src/types/` (10 files, pure TS types)
- `packages/project-info/` → `packages/core/src/project-info/`
  (27 files, discoverProject + framework detection + monorepo helpers)
- `packages/types/src/react-native-dependency-names.ts` →
  `packages/oxlint-plugin-react-doctor/src/react-native-dependency-names.ts`
  (the canonical RN-aware-package list lives next to its heaviest
  consumer — the rule gate — and is re-exported from core to avoid
  the rule-plugin ↔ core dependency cycle the previous types package
  was specifically designed to break).

The two packages are deleted from the workspace; every consumer
(`react-doctor`, `api`, `oxlint-plugin-react-doctor`, `actions/review`,
+130 source/test files) now imports from `@react-doctor/core` directly.

**Errors cleanup**

Dropped the legacy plain-class `ReactDoctorError` and `isReactDoctorError`
from `project-info`. The four narrow errors (`ProjectNotFoundError`,
`NoReactDependencyError`, `PackageJsonNotFoundError`,
`AmbiguousProjectError`) now extend `Error` directly — the only valid
hierarchy is the tagged `Schema.TaggedErrorClass` `ReactDoctorError`
from `core/errors.ts`, which has been the canonical one since the
Effect v4 refactor. Added `isProjectDiscoveryError` as a narrow type
guard for the four leaves.

The discovery errors are thrown synchronously BEFORE the Effect
runtime takes over (at `diagnose()` / CLI entry points); inside the
runtime, the Project service's `translateProjectInfoError` converts
each one to its tagged-reason equivalent.

**Schema vs type name collisions**

`schemas.ts` exports `Diagnostic` / `JsonReport` / `JsonReportMode` /
… as Schema.Class values, while `types/` exports the same names as
TS types. The core barrel re-exports the TS-type versions
(broad-strokes API) and exposes the Schema classes via a new
`@react-doctor/core/schemas` subpath export for the two callers
that need them (`Reporter` service, smoke validator, schema tests).

**Validation**

- `pnpm typecheck` ✓
- `pnpm lint` ✓
- `pnpm format` ✓
- `pnpm test` ✓ (1449 passing, 3 skipped — no regressions)
- `pnpm smoke:json-report` ✓

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(core): keep RN-dependency constants out of types barrel to avoid pulling oxlint plugin into discovery

Bugbot caught (medium severity): re-exporting `isReactNativeDependencyName` from
`oxlint-plugin-react-doctor` via the core types barrel meant every consumer of
`@react-doctor/core` (including `discoverProject` / `discoverReactSubprojects`)
would load the entire 286-rule plugin at module-init.

Adds a tiny standalone leaf at `core/src/project-info/internal-rn-dependency-names.ts`
(zero imports, just the constants the workspace-discovery gate needs) and switches
`is-package-json-react-native-aware.ts` to use it. The types barrel no longer
re-exports the plugin-side constants — rule consumers import them directly from
`oxlint-plugin-react-doctor`.

The duplication is intentional and documented at both files; the two lists are
short enough that a future regression test in the plugin can observe the union.

Validated: typecheck + test (1449 pass) + lint + format all green.
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 01:32:44 -07:00
Aiden Bai 0a231000bb fix(security): four pre-existing findings from a deep audit (rebased onto Effect v4) (#435)
* fix(scripts): escape leaderboard entries before writing them into README

`update-leaderboard.ts` interpolated `entry.name` and
`entry.githubUrl` directly into a markdown table that the workflow
later commits to the canonical README. A compromise of the upstream
benchmarks repo could land arbitrary markdown (or HTML that GitHub's
renderer accepts, like `<img>`) into the README and from there
onto npm via the published `react-doctor` package's README field.

Escape repo names against the markdown table grammar (backslash, pipe,
brackets, backtick, angle brackets, newlines), reject entries whose
`githubUrl` doesn't parse as an `https://github.com/...` URL, and
`encodeURI` the URL before substituting so a stray space or paren
can't terminate the markdown link target.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* fix(website): validate leaderboard github URLs to prevent XSS via href

The hosted leaderboard rendered `<a href={entry.githubUrl}>` with
no protocol check. `entry.githubUrl` is read from
`react-doctor-benchmarks` (a separate repo), so a compromise of
that repo could land a `javascript:` href on react.doctor. React
18+ no longer blocks `javascript:` URLs in href, so this was a
live XSS path even without a same-origin cookie story.

Parse each URL through the WHATWG URL constructor and only keep
entries whose protocol is `https:` and whose host is exactly
`github.com`. Filter happens at fetch time so every downstream
render uses the sanitized list.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>

* fix(security): zip-slip + git-flag-injection defenses for the Effect-based services

Re-applies the two source-side security fixes from #428 onto the
current Effect v4 services (the original commits were authored
against the pre-refactor sync helpers in get-staged-files.ts /
get-diff-files.ts that no longer exist).

## 1. Zip-Slip: staged path traversal

`StagedFiles.materialize` was resolving each staged path with
`path.join(tempDirectory, relativePath)`, which happily expands
`..` segments outside the temp dir. A deliberately crafted index
entry (via `git update-index --add`, a malicious pack, or a
symlinked working tree) could write attacker-controlled files
anywhere the running user can write.

Added `isPathInsideDirectory(child, parent)` helper, resolved
`tempDirectory` up front, and the per-file loop now resolves the
candidate against the resolved temp dir and `continue`s when the
result falls outside. Same shape as the standard Zip-Slip defense.

## 2. CVE-2018-17456: git-flag injection via --diff <base>

`Git.diffSelection` was forwarding `explicitBaseBranch` to
`git rev-parse --verify <branch>` and `git merge-base <branch> HEAD`
unchanged. A value starting with `-` (e.g. `--upload-pack=evil`)
gets parsed by git as an option instead of a refname — the
CVE-2018-17456 shape.

Added `isSafeGitRevision(candidate)` at the top of the Git service
and a check at the start of `diffSelection`. The validation runs
BEFORE any subprocess spawn, so a malicious ref never reaches
`ChildProcess`. Rejects: empty, leading `-`, leading/trailing `.`,
embedded `..`, `@{` reflog suffix, or any character outside
`[A-Za-z0-9_./-]`.

In the canonical CI path the GitHub composite action already
guards with `case "$DIFF_BASE" in -* )` before the CLI is invoked,
so today's exposure is limited to local CLI runs where the user
is attacking themselves. Hardening the library boundary anyway
so other callers don't have to re-implement the check.

## Regression tests

- `Git.diffSelection — git-flag injection` (6 cases): leading dash,
  `..` rev-range, `@{` reflog, shell metacharacters, leading dot,
  empty. Uses `Git.layerNode` because validation runs before any
  spawn, so the suite never reaches `git`.
- `StagedFiles.layerNode — Zip-Slip defense` (1 case): a staged
  index containing `../escaped.ts` writes only the inside-the-temp
  file and never touches the parent dir.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
2026-05-22 19:11:34 -07:00
Aiden Bai 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>
2026-05-22 16:20:18 -07:00
Aiden Bai 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>
2026-05-08 05:58:36 -07:00
Aiden Bai 4fb4d27e85 fix(react-doctor): forward reactMajorVersion in programmatic diagnose() + cleanups (#174)
* fix(react-doctor): forward reactMajorVersion in programmatic diagnose() + cleanups

H1 (regression introduced by #172) — `diagnose()` in `src/index.ts`
forgot to forward `reactMajorVersion` to `runOxlint`. After the
directional version-gating change in #172, that meant every
"prefer-newer-api" rule (today: `prefer-use-effect-event`) was silently
skipped for every programmatic API consumer, even on React 19+
projects. The CLI (`scan.ts`) was unaffected because it always passed
the version explicitly.

Fix is one line + one import — mirror what `scan.ts` already does.
Added `tests/diagnose.test.ts` with a regression test that asserts
`prefer-use-effect-event` fires on a React 19 fixture, plus a
symmetric guard that it stays skipped when the React version can't
be parsed (e.g. a github: range).

H2 — updated the stale docstring on `runOxlint`'s `reactMajorVersion`
field. The doc still claimed "`null` means unknown — leave those
rules enabled" but after #172 the null branch is directional
(deprecation-warning rules stay on, prefer-newer-api rules go off).

M1 — `SUB_HANDLER_DIRECT_CALLEE_NAMES` was just an alias of
`TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES`. Both names existed for
narrative reasons in different files; knip flagged the duplicate
export. Collapsed to the canonical name and updated the one consumer
(`isCallExpressionWithSubHandlerCallee` in state-and-effects).

L1 — moved `walkInsideStatementBlocks` from inline in
`state-and-effects.ts` to `plugin/helpers.ts` next to its sibling
`walkAst`. It already had four call sites and is the natural
"synchronous-only" walker for any rule asking what runs inside an
effect's own body — colocating with `walkAst` makes future rules
discover it.

L2 (audit finding) — proposed receiver-gating
`post`/`put`/`patch` in `EVENT_TRIGGERED_SIDE_EFFECT_CALLEES`
INTENTIONALLY NOT TAKEN. The canonical "You Might Not Need an Effect"
§6 example is `post(jsonToSubmit)` as a bare callee, so removing
those names breaks textbook detection (3 existing tests / fixtures).
Documented the trade-off in the constants.ts docstring.

Validation
- 642 tests passing (640 baseline + 2 new diagnose-API regressions)
- typecheck / lint / format clean

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: format scripts/benchmark-scores.ts

Pre-existing formatting issue introduced in 6afdc04 — the script was
committed unformatted, breaking `pnpm format:check` on every PR
branched off main since. Picked up by rebasing this PR.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 03:53:18 -07:00
Nisarg Patel 6afdc04ba7 chore(package): add benchmark scores script to package.json 2026-05-08 03:39:21 -07:00