Skip to content

improve v2 rasmus#217

Open
aidenybai wants to merge 49 commits into
mainfrom
improve-v2-rasmus
Open

improve v2 rasmus#217
aidenybai wants to merge 49 commits into
mainfrom
improve-v2-rasmus

Conversation

@aidenybai
Copy link
Copy Markdown
Member

@aidenybai aidenybai commented May 13, 2026

Preview release

Try this PR without waiting for an npm release:

npm i https://pkg.pr.new/react-doctor@217

Updates automatically on every push. Pin to a specific commit with react-doctor@<sha> (e.g. @840a6d5).


  • fix
  • fix
  • fix
  • fix
  • fix
  • fix
  • improve v2 rule precision based on user feedback
  • wip: parity work, score module, proxy fetch
  • improve v2 rule precision, score formula, CLI features, and parity
  • fix radix-aschild-single-child FP on JSXExpressionContainer children
  • fix
  • fix rn-no-raw-text FP on web-only directories (docusaurus, docs, storybook)
  • fix
  • fix radix-aschild-single-child FP on JSX comments
  • fix effect-needs-cleanup FP: add 'remove' to unsubscription methods
  • add test-noise tags to no-nested-component-definition and no-eval
  • extend OG_IMAGE_FILE_PATTERN for Remix-style opengraph routes
  • fix FPs found in deep issue-by-issue audit across 100 repos
  • fix client-event-listeners + js-request-idle-callback FPs
  • fix
  • fix
  • fix

Note

Medium Risk
Medium risk due to a major surface-area shift (package exports, CLI rewrite, scoring/annotation behavior) that can affect consumers and CI behavior, though changes are largely additive/compat-focused with documented opt-ins.

Overview
Moves react-doctor into a v2 SDK-first beta: the main export now points at the SDK, legacy diagnose() lives under react-doctor/api (with v1 error classes re-exported for compatibility), and a new react-doctor/score subpath is added while the browser-poc export is removed.

Reworks the CLI implementation and defaults: adds new changed-file modes (--unstaged, --changed), improves staged scanning by materializing needed config files safely, skips info issues in GitHub Actions annotations, and temporarily turns off codebase graph checks (dead-code/deps/architecture) by default unless explicitly enabled.

Fixes categorization/false positives by routing oxlint diagnostics through resolveOxlintDiagnosticCategory() and adjusting codebase analyzer behavior (e.g., treating new URL(..., import.meta.url) asset references and ensuring vite/nextjs config files are treated as support—not runtime—entrypoints). Adds a preview-release GitHub workflow, bumps package version to 0.2.0-beta.1, trims/adjusts dependencies, and introduces a test:regression script that packs a tarball and drives sandbox tests via the sibling react-review repo.

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

aidenybai and others added 22 commits May 12, 2026 00:45
- Recategorize Tailwind-specific rules (size/padding shorthand, space-on-flex, default palette) from design-* to tailwind-* so they're ecosystem rules, not core React rules
- Tighten js-length-check-first to require actual element-wise comparison and handle IfStatement/ConditionalExpression guards with branch-awareness
- Broaden async-parallel test file skip pattern to cover __tests__/ dirs, e2e/integration files, mocks, and fixtures
- Make preventDefault form message framework-agnostic (remove server action suggestion)
- Add web-only workspace directory detection for RN rules (apps/web/, packages/web-app/, etc.)
- Add 20 tests covering all changes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fix score formula saturation: cap per-rule log amplification at 4x
- Fix async-suspense-boundaries: skip page.tsx/layout.tsx/route.tsx files entirely
- Fix .prettierignore regression: filter source extension globs (*.ts etc) that
  would exclude all source files (fixes refinedev/refine 0-issue regression)
- Port --score, --full, --annotations CLI flags from v1
- Fix JSON stdout pollution from selectProjects in --json mode
- Fix parity script: normalize v2 tailwind-* rule keys back to v1 design-* keys
- Add 30 regression test repos to parity script
- Deep-validate all outlier fixtures (react-router, astro, vite, immich, refine)
- Update progress.md and PARITY_CHECKLIST.md with Phase 7-8 results

Parity: 19 exact matches across 60 repos, mean |Δ| = 1.38, max |Δ| = 9
(astro/vite — non-React repos where v2 discovers more sub-projects)
The rule only accepted literal JSXElement as a valid single child for
asChild. A variable reference like {groupContent} wrapped in a
JSXExpressionContainer was incorrectly flagged even when it resolves
to a single element at runtime.

Now also accepts JSXExpressionContainer as a valid single-child pattern.
Verified: react-review accordion.tsx went from 4 FPs to 0.
…ybook)

RN rules were firing on Docusaurus documentation pages in cross-platform
libraries like downshift-js/downshift (which has react-native as a devDep
for cross-platform support). Extended isWebOnlyPath to also skip
docusaurus/, docs/, documentation/, website/, storybook/, stories/ dirs.

Verified: downshift went from 66 rn-no-raw-text errors to 0.
getMeaningfulJsxChildren was counting JSX comments ({/* ... */}) as
meaningful children. A component like:
  <Trigger asChild>
    {/* comment */}
    <span>{element}</span>
  </Trigger>
was flagged as having 2 children when it really has 1.

Fixed by filtering out JSXExpressionContainer with JSXEmptyExpression
(the AST shape for JSX comments).

Verified: PostHog went from 54 radix-aschild FPs to 0.
React Native's AppState.addEventListener returns a subscription with
a .remove() method. The cleanup pattern `return () => subscription.remove()`
was not recognized because 'remove' wasn't in UNSUBSCRIPTION_METHOD_NAMES.

Verified: TanStack/query useAppState.ts no longer falsely flagged.
Both rules produce noise on test/storybook files:
- no-nested-component-definition: inline components in stories are standard practice
- no-eval: eval() in tests to verify generated script output is legitimate

Tagged with test-noise so the auto-suppression system drops them on
test/storybook/stories files, matching v1's behaviour.

Verified: eliminates 20 no-nested-component-definition and 16 no-eval
FPs across 100 repos (ant-design, MUI, adobe/react-spectrum, etc).
Documenso uses share.$slug.opengraph.tsx (Remix route convention) for
OG image generation with satori. The existing pattern only matched
Next.js conventions (opengraph-image.tsx). Added .opengraph.[jt]sx?
suffix matching.

Verified: Documenso no-inline-exhaustive-style OG FPs eliminated.
testing-no-container-query: only fire in test files — was flagging
production code using container.querySelector for DOM manipulation.

js-set-map-lookups: skip String.includes() via suffix-based identifier
detection (spanText, routeName, etc.) and skip inline array literals
with <8 elements where Set overhead exceeds linear scan.

no-uncontrolled-input: recognize onInput as valid control handler
alongside onChange and readOnly — React's onInput fires identically
to onChange.

OG_IMAGE_FILE_PATTERN: match Remix .opengraph.tsx route convention
in addition to Next.js opengraph-image.tsx.

Verified across react-review, expect, react-scan, react-grab:
- testing-no-container-query: 2 FPs eliminated
- js-set-map-lookups: ~12 FPs eliminated
- no-uncontrolled-input: 1 FP eliminated
- no-inline-exhaustive-style on OG: 2 FPs eliminated
client-event-listeners: skip when the addEventListener call is at
module scope (not inside a React component or custom hook). Module-level
event registration is intentional setup code, not a per-instance leak.
Uses ancestor traversal to find enclosing component/hook functions.

js-request-idle-callback: skip server/CLI/test files where
requestIdleCallback doesn't exist. Detects /server/, /cli/, /bin/,
/scripts/, /workers/, /commands/, /api/ path segments.

Verified: react-scan module-level init FPs eliminated, expect CLI
analytics FPs eliminated.
@reactreview
Copy link
Copy Markdown

reactreview Bot commented May 13, 2026

React Review87/100 (+87) · 0 ❌ errors · 557 ⚠️ 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 557 warnings. This PR makes the React health score 87 points better.

<file name="packages/react-doctor/src/core/rules/lint/react-native/rn-prefer-content-inset-adjustment.ts">

<violation number="1" location="packages/react-doctor/src/core/rules/lint/react-native/rn-prefer-content-inset-adjustment.ts:2">
Severity: Warning

Import from barrel/index file — import directly from the source module for better tree-shaking

Import from the direct path: `import { Button } from './components/Button'` instead of `./components`

Rule: `no-barrel-import`
</violation>

</file>

<file name="packages/react-doctor/src/core/rules/lint/server/utils/contains-auth-check.ts">

<violation number="1" location="packages/react-doctor/src/core/rules/lint/server/utils/contains-auth-check.ts:1">
Severity: Warning

Import from barrel/index file — import directly from the source module for better tree-shaking

Import from the direct path: `import { Button } from './components/Button'` instead of `./components`

Rule: `no-barrel-import`
</violation>

</file>

<file name="packages/react-doctor/src/core/rules/lint/performance/js-tosorted-immutable.ts">

<violation number="1" location="packages/react-doctor/src/core/rules/lint/performance/js-tosorted-immutable.ts:2">
Severity: Warning

Import from barrel/index file — import directly from the source module for better tree-shaking

Import from the direct path: `import { Button } from './components/Button'` instead of `./components`

Rule: `no-barrel-import`
</violation>

</file>

<file name="packages/react-doctor/src/core/rules/lint/react-native/rn-no-web-dom-elements.ts">

<violation number="1" location="packages/react-doctor/src/core/rules/lint/react-native/rn-no-web-dom-elements.ts:2">
Severity: Warning

Import from barrel/index file — import directly from the source module for better tree-shaking

Import from the direct path: `import { Button } from './components/Button'` instead of `./components`

Rule: `no-barrel-import`
</violation>

</file>

<file name="packages/react-doctor/src/core/rules/lint/performance/js-set-map-lookups.ts">

<violation number="1" location="packages/react-doctor/src/core/rules/lint/performance/js-set-map-lookups.ts:2">
Severity: Warning

Import from barrel/index file — import directly from the source module for better tree-shaking

Import from the direct path: `import { Button } from './components/Button'` instead of `./components`

Rule: `no-barrel-import`
</violation>

</file>

<file name="packages/react-doctor/src/core/rules/lint/react-native/rn-no-single-element-style-array.ts">

<violation number="1" location="packages/react-doctor/src/core/rules/lint/react-native/rn-no-single-element-style-array.ts:2">
Severity: Warning

Import from barrel/index file — import directly from the source module for better tree-shaking

Import from the direct path: `import { Button } from './components/Button'` instead of `./components`

Rule: `no-barrel-import`
</violation>

</file>

<file name="packages/react-doctor/src/core/rules/lint/server/utils/collect-identifier-params.ts">

<violation number="1" location="packages/react-doctor/src/core/rules/lint/server/utils/collect-identifier-params.ts:1">
Severity: Warning

Import from barrel/index file — import directly from the source module for better tree-shaking

Import from the direct path: `import { Button } from './components/Button'` instead of `./components`

Rule: `no-barrel-import`
</violation>

</file>

<file name="packages/react-doctor/src/core/rules/lint/performance/js-request-idle-callback.ts">

<violation number="1" location="packages/react-doctor/src/core/rules/lint/performance/js-request-idle-callback.ts:2">
Severity: Warning

Import from barrel/index file — import directly from the source module for better tree-shaking

Import from the direct path: `import { Button } from './components/Button'` instead of `./components`

Rule: `no-barrel-import`
</violation>

</file>

<file name="packages/react-doctor/src/core/rules/lint/react-native/rn-no-scrollview-mapped-list.ts">

<violation number="1" location="packages/react-doctor/src/core/rules/lint/react-native/rn-no-scrollview-mapped-list.ts:2">
Severity: Warning

Import from barrel/index file — import directly from the source module for better tree-shaking

Import from the direct path: `import { Button } from './components/Button'` instead of `./components`

Rule: `no-barrel-import`
</violation>

</file>

<file name="packages/react-doctor/src/core/rules/lint/server/utils/collect-declared-names.ts">

<violation number="1" location="packages/react-doctor/src/core/rules/lint/server/utils/collect-declared-names.ts:1">
Severity: Warning

Import from barrel/index file — import directly from the source module for better tree-shaking

Import from the direct path: `import { Button } from './components/Button'` instead of `./components`

Rule: `no-barrel-import`
</violation>

</file>

<file name="packages/react-doctor/src/core/rules/lint/react-native/rn-no-scroll-state.ts">

<violation number="1" location="packages/react-doctor/src/core/rules/lint/react-native/rn-no-scroll-state.ts:2">
Severity: Warning

Import from barrel/index file — import directly from the source module for better tree-shaking

Import from the direct path: `import { Button } from './components/Button'` instead of `./components`

Rule: `no-barrel-import`
</violation>

</file>

<file name="packages/react-doctor/src/core/rules/lint/performance/js-min-max-loop.ts">

<violation number="1" location="packages/react-doctor/src/core/rules/lint/performance/js-min-max-loop.ts:2">
Severity: Warning

Import from barrel/index file — import directly from the source module for better tree-shaking

Import from the direct path: `import { Button } from './components/Button'` instead of `./components`

Rule: `no-barrel-import`
</violation>

</file>

<file name="packages/react-doctor/src/core/rules/lint/server/utils/call-reads-handler-args.ts">

<violation number="1" location="packages/react-doctor/src/core/rules/lint/server/utils/call-reads-handler-args.ts:1">
Severity: Warning

Import from barrel/index file — import directly from the source module for better tree-shaking

Import from the direct path: `import { Button } from './components/Button'` instead of `./components`

Rule: `no-barrel-import`
</violation>

</file>

<file name="packages/react-doctor/src/core/rules/lint/react-native/rn-no-raw-text.ts">

<violation number="1" location="packages/react-doctor/src/core/rules/lint/react-native/rn-no-raw-text.ts:2">
Severity: Warning

Import from barrel/index file — import directly from the source module for better tree-shaking

Import from the direct path: `import { Button } from './components/Button'` instead of `./components`

Rule: `no-barrel-import`
</violation>

</file>

<file name="packages/react-doctor/src/core/rules/lint/performance/js-length-check-first.ts">

<violation number="1" location="packages/react-doctor/src/core/rules/lint/performance/js-length-check-first.ts:2">
Severity: Warning

Import from barrel/index file — import directly from the source module for better tree-shaking

Import from the direct path: `import { Button } from './components/Button'` instead of `./components`

Rule: `no-barrel-import`
</violation>

</file>

Showing 15 of 557 issues.

Reviewed by react-review for commit c9c3cc9. 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 2:46pm

Comment thread PROMPT.md Outdated
Comment thread PARITY_CHECKLIST.md Outdated
Comment thread packages/react-doctor-v2/scripts/parity.ts Outdated
Comment thread packages/react-doctor-v2/scripts/parity.ts Outdated
Comment thread packages/react-doctor-v2/scripts/parity.ts Outdated
Comment thread packages/react-doctor-v2/scripts/parity.ts Outdated
Comment thread packages/react-doctor/src/core/rules/codebase/react-architecture.ts
Comment thread packages/react-doctor/src/core/rules/codebase/react-architecture.ts
Comment thread packages/react-doctor/src/core/rules/codebase/dependencies.ts
Comment thread packages/react-doctor-v2/scripts/parity.ts Outdated
Comment thread packages/react-doctor-v2/src/cli/index.ts Outdated
const cliValue = getCliOptionOverride(command, optionName, flagValue);
if (cliValue !== undefined) return cliValue;
return configValue === undefined ? defaultValue : undefined;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Config boolean values silently dropped as undefined

Medium Severity

resolveBooleanInspectOption returns undefined when configValue is defined (e.g., false), instead of returning the config value itself. The intent appears to be deferring to the SDK's config fallback, but if the SDK resolves options.lint ?? true without consulting options.config?.lint, a user's explicit "lint": false in their config file will be silently ignored and lint will run anyway. The old CLI resolved config values eagerly (e.g., userConfig?.lint ?? true), so this changes the contract.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2b5310e. Configure here.

…ored

resolveBooleanInspectOption previously returned undefined when the config
provided a value, relying on the SDK's mergeConfig fallback to recover
it. That coupled the CLI to internal SDK behavior — if mergeConfig ever
stopped consulting options.config.lint, a user's explicit "lint": false
would silently be ignored. Resolve the value eagerly to a concrete
boolean so the contract is explicit at the call site.
silentLogs: isQuiet,
config,
loadedConfig,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Staged mode redirects scan away from temp directory

High Severity

When rootDir is set in the config and --staged mode is used, the loadedConfig object (with its original sourceDirectory) is passed through to inspectReactProjectCore. Inside that function, getLoadedConfig returns the passed loadedConfig directly, then resolveConfigRootDirectory resolves rootDir relative to the original sourceDirectory — not the temp directory. This redirects the scan from the staged snapshot back to the original working tree, defeating the purpose of materialized staged files entirely.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 00a7cc7. Configure here.

…napshot

Passing the original loadedConfig into the SDK in staged mode caused
resolveConfigRootDirectory to re-resolve rootDir against
loadedConfig.sourceDirectory — the original working tree — which
defeated the whole point of materializing staged files. Now we pass
loadedConfig: null and a copy of config without rootDir, while still
forwarding ignore/ignoredTags/textComponents/adoptExistingLintConfig
through the config spread so they continue to apply.
return null;
}
currentDirectory = parentDirectory;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Config walk skips parent when start directory is boundary

Medium Severity

In loadReactDoctorConfig, the walk loop checks isProjectBoundary(currentDirectory) after failing to find a config in currentDirectory. If the starting directory is itself a project boundary (e.g. has .git), the loop returns null immediately without ever checking the starting directory's package.json for a reactDoctor key. Wait — actually loadConfigFromDirectory IS called first. Let me re-read…

Actually on re-inspection this is correct — loadConfigFromDirectory is called first, and only if it returns null does the boundary check fire. Withdrawing this.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6d8fb70. Configure here.

process.stdout.write(
`${flags.jsonCompact ? JSON.stringify(report) : JSON.stringify(report, null, 2)}\n`,
);
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Inconsistent JSON schema between single and multi-project output

Medium Severity

When --json is used, single-project output uses buildReactDoctorJsonReport producing a flat schema with project (singular) and no projects array, while multi-project output uses toAggregateJsonReport producing a schema with projects (array). Consumers parsing the JSON cannot rely on a stable shape — the top-level keys differ depending on whether one or multiple projects are scanned. This breaks programmatic integrations that expect a consistent JSON contract.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6d8fb70. Configure here.

flags.full ||
isJsonMode ||
isNonInteractiveEnvironment() ||
!process.stdin.isTTY;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Score-only mode missing from prompt-skip condition

Medium Severity

The shouldSkipPrompts check includes isJsonMode but not isScoreOnly (flags.score). Since isQuiet is isScoreOnly || isJsonMode, the old CLI included score-only mode in prompt suppression. Now when --score is passed without --json, the user may be prompted interactively (e.g. project selection, diff mode), which breaks script consumption of the numeric-only stdout output.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 880d6f2. Configure here.

console.log(highlighter.dim("No staged source files found."));
}
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Score-only mode not handled for staged empty files

Low Severity

When --staged finds no staged files, the early return handles isJsonMode and !isScoreOnly branches, but never outputs anything when isScoreOnly is true. A --score --staged invocation with no staged files exits silently with no output instead of printing the perfect score (100).

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 880d6f2. Configure here.

const includePathsFlags: CliFlags =
wantsDiffMode && !isDiffMode ? { ...effectiveFlags, diff: false } : effectiveFlags;
includePaths = resolveIncludePaths(scanRootDirectory, includePathsFlags);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Dead guard condition after early return from staged block

Low Severity

The else if (!effectiveFlags.staged) condition on the non-diff branch is always true at this point in the code. The staged mode block (starting around line 1350) unconditionally returns early, so execution only reaches this line when effectiveFlags.staged is false. The guard adds a misleading suggestion that the staged flag could still be active here.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7797985. Configure here.

.split("\0")
.map((filePath) => filePath.trim())
.filter((filePath) => filePath.length > 0 && isSourceFile(filePath));
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing maxBuffer in getGitFiles may silently truncate results

Low Severity

getGitFiles calls spawnSync without setting maxBuffer, defaulting to Node's 1 MB limit. The sibling function getStagedFilePaths in get-staged-files.ts correctly sets maxBuffer: GIT_SHOW_MAX_BUFFER_BYTES (50 MB). In repositories with a very large number of changed files, spawnSync will set result.error, causing getGitFiles to return []. This cascades into shouldSkipSourceChecks becoming true, silently skipping all source checks and producing a misleading perfect score with no user-facing warning.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7797985. Configure here.

Comment thread packages/react-doctor/src/core/runners/oxlint.ts
startedAt: report.startedAt,
completedAt: report.completedAt,
durationMilliseconds: report.durationMilliseconds,
})),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Aggregate JSON report missing score/scoreLabel at project level

Medium Severity

In toAggregateJsonReport, each entry in the projects array copies summary from the per-project report, but the per-project ReactDoctorJsonReport type has summary which includes score and scoreLabel. The aggregate summary at the top level (lines 818-825) computes worstScore by searching results for a matching score.value, but worstScoreLabel looks up the label via results.find(...)?.score?.label. When all projects have the same worst score, the find returns the first match — that's fine. But when scores is empty (all projects scored null), worstScore becomes null and worstScoreLabel also becomes null, which is correct. The actual issue is subtler: durationMilliseconds at line 828 sums all project durations, but if projects ran in parallel via Promise.all (line 1595), this sum overstates wall-clock time. The single-project path via buildReactDoctorJsonReport reports accurate wall-clock duration per project, but the aggregate reports an inflated total. This makes the durationMilliseconds field misleading for multi-project JSON consumers comparing timing across runs.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3abd2a0. Configure here.

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 1 potential issue.

There are 17 total unresolved issues (including 16 from previous reviews).

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 c9c3cc9. Configure here.

const pending: string[] = [rootDirectory];

while (pending.length > 0) {
const current = pending.shift();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

BFS directory walk uses O(n²) array shift

Low Severity

discoverReactProjectsByFilesystem uses pending.shift() inside a while loop for a BFS traversal. Array.shift() is O(n) because it re-indexes the entire array, making the overall directory walk O(n²) in the number of directories. For large monorepos with thousands of nested directories, this quadratic behavior can become noticeable. Using an index variable to track the current position (or a proper queue) would keep it O(n).

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c9c3cc9. Configure here.

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.

2 participants