fix(nextjs): scope useSearchParams/Suspense rule to page files only (#695)#714
Merged
aidenybai merged 13 commits intoJun 7, 2026
Merged
Conversation
…695) Non-page component files are expected to be composed with <Suspense> by their consumers. Firing on every file that lacks an in-file Suspense produced false positives when the boundary lives in a parent module. Now the rule only reports on files matching PAGE_FILE_PATTERN (page.tsx), where the developer IS responsible for providing their own boundary. Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
commit: |
Contributor
|
No React Doctor issues found. 🎉 Reviewed by React Doctor for commit |
Layout files are framework entry points with the same useSearchParams + Suspense requirement. Use PAGE_OR_LAYOUT_FILE_PATTERN to cover both. Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
When a page/layout renders an imported component NOT wrapped in <Suspense>, resolve the import and check whether the exported component calls useSearchParams(). Reports at the render site. Uses existing cross-file infrastructure: parseSourceFile, resolveRelativeImportPath, findExportedFunctionBody. Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Address review on #714: - Drop the whole-file fallback in the cross-file check. When an export doesn't bind to a function (memo()/forwardRef()/class/barrel re-export) we now bail instead of scanning the whole module, which previously flagged a component for an UNRELATED sibling export's useSearchParams() call (false positive). - Recognize <React.Suspense> (member form) and aliased `import { Suspense as X }` wrappers in the per-element boundary check, removing false positives on correctly-wrapped pages. - Restore the dropped Suspense-heuristic rationale and document the relative-only / barrel cross-file limitations as deliberate false-negative-over-false-positive trade-offs. - Reuse resolveImportedExportName instead of duplicating import-specifier name resolution. - Import useSearchParams from next/navigation in the nextjs-app fixtures so the integration tests exercise the real Next hook, not a local stub.
…useSearchParams rule (#714) Close the deferred cross-file gaps in nextjs-no-use-search-params-without-suspense: - H1: resolve tsconfig `@/` path aliases (+ baseUrl) so alias-imported consumers are checked, not just relative imports (the dominant Next.js convention). New resolve-tsconfig-alias (JSONC + extends) + combined resolve-module-path (relative -> alias). - M4: follow barrel / re-export chains by reusing the canonical cross-file resolver. Extracted resolve-cross-file-function-export from the reducer rule (now shared by both), deleting the duplicate. - Suppress false positives when an ancestor layout.tsx wraps `{children}` in <Suspense> (new find-ancestor-suspense-layout) since that boundary covers the whole page. - Extract the file-level Suspense heuristic into ast-mentions-suspense, shared by the rule and the layout walk. Tests: tsconfig-alias unit tests (baseUrl / no-baseUrl / JSONC / extends) plus cross-file tests for alias, barrel, and ancestor-layout cases.
CodeQL flagged `target.replace("*", capture)` as incomplete string
replacement (only the first `*`). A tsconfig `paths` target has at most
one `*`, so behavior is unchanged, but replaceAll is complete + robust
and clears the high-severity code-scanning alert.
…xtended tsconfig paths
Address Bugbot review on the cross-file work:
- `astMentionsSuspense` now also matches the `<React.Suspense>` member
form, so an ancestor `layout.tsx` that wraps `{children}` via a
namespace-imported `React.Suspense` is recognized as a boundary (was a
false positive).
- `readResolvedTsconfig` now inherits `paths` from the `extends` chain
when the child config has no `paths` field (a present `paths` — even
empty — still replaces, matching TypeScript). A child declaring only
`baseUrl` no longer drops the base's `@/` aliases.
Tests: ancestor `<React.Suspense>` layout, extends-inherits-paths, and
explicit-empty-paths-replaces cases.
…lution Drop the relative-only guard in resolveReducerFunction so imported reducers behind tsconfig path aliases (`@/reducers/x`) are followed too. The shared resolveModulePath returns null for bare node-module specifiers that match no alias, so packaged code (`react-redux`, etc.) is still skipped without an explicit guard. Tests: mutation flagged through an `@/`-aliased reducer; a bare node-module import that matches no alias is not followed.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f7cd4b3. Configure here.
The per-directory cache in findNearestTsconfig wasn't mtime-checked, so a long-lived process (the language server) could serve stale `@/` alias resolution after a tsconfig edit or a newly-added config. Removed it and rely on the mtime-keyed per-file cache; the directory walk re-stats cheaply and reuses parsed configs. Added a regression test that rewrites a tsconfig mid-process and asserts the new mapping is picked up.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Fixes #695 —
nextjs-no-use-search-params-without-suspensefalse positive on non-page components, plus new cross-file detection.Why
Non-page component files are composed by parents that provide the Suspense boundary. Flagging them produces noise. But silently skipping them loses the diagnostic entirely — the page that renders them without Suspense is the real bug site.
Before (false positive on every component file):
After — two-layer detection:
What changed
PAGE_OR_LAYOUT_FILE_PATTERNfiles.<ImportedComponent />NOT inside a<Suspense>ancestor, resolves the relative import viaresolveRelativeImportPath→parseSourceFile→findExportedFunctionBody, then checks if the component body callsuseSearchParams(). Reports at the render site with a component-specific message.astContainsUseSearchParams,detectSuspenseAwareness,collectRelativeImports,isInsideSuspenseBoundary,exportedComponentUsesSearchParams.ImportedComponentEntryinterface to carry bothsourceandexportedName.cross-file/page.tsx+cross-file/search-consumer.tsx).oxlint-plugin-react-doctor.Test plan
npx vp test run ...nextjs-no-use-search-params-without-suspense.cross-file.test.ts— 5/5 passednpx vp test run ...run-oxlint/nextjs.test.ts— 33/33 passedturbo run typecheck --filter=oxlint-plugin-react-doctor— passedLink to Devin session: https://app.devin.ai/sessions/62d04f36734d4500b609e250430b8469
Requested by: @aidenybai
Note
Medium Risk
Adds filesystem walks, tsconfig parsing, and cross-file AST resolution on lint runs; behavior changes which files get diagnostics and could miss edge cases, but bounds and null-safe resolution limit blast radius.
Overview
Fixes #695 by tightening
nextjs-no-use-search-params-without-suspense: it now runs only on App Router page/layout files, so leaf components that calluseSearchParams()are no longer flagged.On those page/layout files, the rule adds cross-file checks: for each rendered imported component not inside a local
<Suspense>ancestor, it resolves the export (relative paths, tsconfig@/aliases, barrel re-exports) and reports at the JSX site if that component body usesuseSearchParams(). Ancestorlayout.tsxfiles with Suspense (including<React.Suspense>and aliased imports) suppress diagnostics for the whole route segment.Shared infrastructure:
resolveTsconfigAliasPath,resolveModulePath, extractedresolveCrossFileFunctionExport(also used byno-mutating-reducer-statefor@/imports),astMentionsSuspense,hasAncestorSuspenseLayout, and bounded directory-walk constants. Extensive unit and integration tests plus a patch changeset foroxlint-plugin-react-doctor.Reviewed by Cursor Bugbot for commit 92f255a. Bugbot is set up for automated code reviews on this repo. Configure here.