Skip to content

feat(react-doctor): add TanStack Start (14), TanStack Query (6), and Vercel Best Practices (2) rules#124

Open
aidenybai wants to merge 14 commits intomainfrom
cursor/tanstack-start-rules-35ea
Open

feat(react-doctor): add TanStack Start (14), TanStack Query (6), and Vercel Best Practices (2) rules#124
aidenybai wants to merge 14 commits intomainfrom
cursor/tanstack-start-rules-35ea

Conversation

@aidenybai
Copy link
Copy Markdown
Member

@aidenybai aidenybai commented Apr 13, 2026

What

Adds 22 new rules to react-doctor: 14 TanStack Start + 6 TanStack Query + 2 Vercel Best Practices, all with tests and edge case validation. Also upgrades oxc tooling and fixes 7 bugs found during review.

  • TanStack Start rules auto-activate when @tanstack/react-start is detected
  • TanStack Query rules are always-on (useQuery is used across all React frameworks)
  • Vercel Best Practices rules are always-on (from vercel-labs/agent-skills)

Oxc Upgrades

  • oxlint: 1.59.0 → 1.60.0
  • oxfmt: 0.44.0 → 0.45.0

All Bugs Fixed

# Rule Severity Description
1 tanstackStartRedirectInTryCatch High Used node.parent which oxlint JS plugins don't populate — rule never fired
2 tanstackStartServerFnValidateInput High Fragile chain-walking with infinite loop risk
3 getRouteOptionsObject High Wrong argument index ([1] instead of [0]) disabled 4 rules for createRootRoute/createRoute
4 tanstackStartNoNavigateInRender Medium componentDepth leaked for arrow components; dead eventHandlerDepth variable
5 tanstackStartGetMutation Medium Only allowed "POST" as non-GET; missed PUT/PATCH/DELETE
6 renderingScriptDeferAsync Medium False-positived on <script type="application/ld+json">
7 queryStableQueryClient Medium False-positived on non-component utility functions

Validation

  • 190/190 tests pass (was 165, +25 new including 4 false-positive-freedom edge case tests)
  • Build succeeds (3 bundles)
  • Lint clean (0 errors), format clean

Rules

See individual rule examples in previous PR description revision — all 22 rules are documented with bad/good code examples.

Open in Web Open in Cursor 

…middleware, data loading, SEO, navigation, linting, and project structure rules

Researched from official TanStack docs, GitHub repos (tanstack/router),
ESLint plugin source, and community patterns. Covers:

- File-based routing conventions and naming patterns
- createServerFn patterns with validation and file organization
- Middleware composition, auth patterns, context management
- Data loading with route property order (critical for TS inference)
- Document head management and SEO meta tags
- Type-safe navigation with Link and useNavigate
- ESLint plugin-router rules (property order, param names)
- Project structure, app config, deployment presets

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

vercel bot commented Apr 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 Apr 14, 2026 7:11am

Add 9 TanStack Start-specific rules to react-doctor, following the same
pattern as NEXTJS_RULES and REACT_NATIVE_RULES:

- tanstack-start-route-property-order: enforces inference-sensitive property
  order (params/validateSearch → beforeLoad → loader → head)
- tanstack-start-no-direct-fetch-in-loader: flags raw fetch() in loaders,
  recommends createServerFn() for type-safe RPC
- tanstack-start-server-fn-validate-input: warns when .handler() accesses
  data without .inputValidator() — data crosses a network boundary
- tanstack-start-no-useeffect-fetch: flags fetch() inside useEffect in route
  files — should use route loader or createServerFn()
- tanstack-start-missing-head-content: warns when __root route is missing
  <HeadContent /> — without it, route head() meta tags are dropped
- tanstack-start-no-anchor-element: flags <a href="/..."> in route files,
  recommends <Link> from @tanstack/react-router
- tanstack-start-server-fn-method-order: enforces correct chain order
  (.middleware → .inputValidator → .client → .server → .handler)
- tanstack-start-no-navigate-in-render: flags navigate() during render,
  recommends redirect() in beforeLoad/loader
- tanstack-start-no-dynamic-server-fn-import: flags dynamic import() of
  .functions.ts files — bundler needs static imports for RPC stubs

Also adds:
- tanstack-start Framework type detection via @tanstack/react-start dep
- TanStack Start category and help entries in run-oxlint.ts
- Removes previous .agents/skills approach (wrong location)

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor bot changed the title feat: add TanStack Start agent skill feat(react-doctor): add TanStack Start oxlint rules Apr 14, 2026
…verage

New rules:
- tanstack-start-no-use-server-in-handler (error): flags 'use server' inside
  createServerFn handlers — TanStack Start handles this automatically, the
  directive causes compilation errors (GitHub issue #2849)
- tanstack-start-no-secrets-in-loader (error): flags process.env.SECRET in
  loaders/beforeLoad — loaders are isomorphic and leak secrets to the client
  bundle (GitHub issue TanStack/cli#287)
- tanstack-start-get-mutation (warn): flags GET server functions that perform
  mutations — should use POST to prevent CSRF and prefetch triggers
- tanstack-start-redirect-in-try-catch (warn): flags throw redirect()/notFound()
  inside try-catch — the router catches these internally
- tanstack-start-loader-parallel-fetch (warn): flags sequential awaits in loaders
  — should use Promise.all() to avoid waterfalls

Total TanStack Start rules: 14

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Bugs fixed:
- tanstackStartRedirectInTryCatch: was using node.parent which oxlint JS
  plugins don't populate — rule never fired. Rewrote to use ThrowStatement
  visitor with tryCatchDepth tracking (matches existing nextjs pattern)
- tanstackStartServerFnValidateInput: fragile chain-walking logic with
  potential infinite loop. Extracted shared walkServerFnChain() helper,
  removed unused serverFnChains variable
- tanstackStartNoNavigateInRender: componentDepth never decremented for
  arrow components (no VariableDeclarator:exit), eventHandlerDepth was
  declared but never modified. Removed dead variable, simplified

AGENTS.md violations fixed:
- Removed unused hasDirective import
- Moved SEQUENTIAL_AWAIT_THRESHOLD_FOR_LOADER to constants.ts
- Moved TANSTACK_REDIRECT_FUNCTIONS to constants.ts
- Moved TANSTACK_SERVER_FN_FILE_PATTERN to constants.ts
- Replaced inline /^[A-Z]/ with existing UPPERCASE_PATTERN constant
- Replaced duplicate MUTATION_CALLEE_NAMES with existing MUTATION_METHOD_NAMES
- Removed 'as string' type cast, replaced with typeof guard

Upgraded oxc:
- oxlint: 1.59.0 → 1.60.0
- oxfmt: 0.44.0 → 0.45.0

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@aidenybai aidenybai marked this pull request as ready for review April 14, 2026 00:35
… cases

Add tanstack-start-app fixture with:
- __root.tsx missing HeadContent (triggers missing-head-content)
- route-issues.tsx with 8 bad patterns (property order, direct fetch,
  useEffect fetch, anchor element, secrets in loader, redirect in
  try-catch, parallel fetch, navigate in render)
- server-fn-issues.tsx with 4 bad patterns (missing validation,
  use-server in handler, GET mutation, dynamic import)

Tests verify all rules fire with correct severity and category.
Total tests: 165 → 178 (+13 new)

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Add always-on TanStack Query rules (not framework-gated — useQuery is
used across all React frameworks):

- query-stable-query-client (error): flags new QueryClient() inside a
  function body — recreates the cache on every render. Should be at
  module scope or wrapped in useState()
- query-no-rest-destructuring (warn): flags ...rest on useQuery() result —
  subscribes to all fields and causes unnecessary re-renders
- query-no-void-query-fn (warn): flags empty queryFn body — must return
  a value for the cache. Use enabled option to disable instead
- query-no-query-in-effect (warn): flags refetch() inside useEffect —
  React Query manages refetching via queryKey and enabled
- query-mutation-missing-invalidation (warn): flags useMutation without
  invalidateQueries — stale data may remain cached
- query-no-usequery-for-mutation (warn): flags useQuery with POST/PUT/DELETE
  fetch — use useMutation() for write operations

All 6 rules have test coverage via query-issues.tsx fixture.
Total tests: 178 → 184 (+6 new)

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor bot changed the title feat(react-doctor): add TanStack Start oxlint rules feat(react-doctor): add TanStack Start (14) and TanStack Query (6) rules Apr 14, 2026
…update

New rules sourced from vercel-labs/agent-skills react-best-practices:

- js-flatmap-filter (warn): flags .map().filter(Boolean) chains —
  iterates the array twice and creates an intermediate array. Suggests
  .flatMap() for a single-pass transform+filter
- rendering-script-defer-async (warn): flags <script src> without defer
  or async — blocks HTML parsing and delays First Contentful Paint

Both rules have test fixtures and test cases.
Total tests: 184 → 186 (+2 new)

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor bot changed the title feat(react-doctor): add TanStack Start (14) and TanStack Query (6) rules feat(react-doctor): add TanStack Start (14), TanStack Query (6), and Vercel Best Practices (2) rules Apr 14, 2026
…oute, remove duplicate helper

Bugbot findings:

1. getRouteOptionsObject used arguments[1] for direct calls like
   createRootRoute({...}) and createRoute({...}), but these functions
   take the options object as arguments[0]. This silently disabled
   route-property-order, no-direct-fetch-in-loader, no-secrets-in-loader,
   and loader-parallel-fetch for any route using createRootRoute or
   createRoute. Fixed to use arguments[0] for both direct and curried
   call patterns.

2. getChainCalleeName was an exact duplicate of the already-exported
   getCalleeName in helpers.ts. Replaced all usages with the shared
   helper per AGENTS.md DRY rule.

Added createRootRoute test case to verify the fix.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
async ({ data }: any) => {
return data;
},
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Server function method order rule has no triggering test

Medium Severity

The wrongMethodOrder fixture only chains .handler() directly on createServerFn() — a single method with nothing to compare against. The tanstack-start-server-fn-method-order rule requires at least two order-sensitive methods (e.g., .handler().inputValidator()) to detect a violation. This fixture never triggers the rule, and the rule is entirely absent from the test suite, leaving it untested.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b2b5f30. Configure here.

Systematic analysis of all 22 rules found 3 bugs and 1 redundancy:

1. tanstackStartGetMutation only allowed 'POST' as a non-GET method,
   but PUT, PATCH, DELETE are also valid mutating methods. Now uses
   MUTATING_HTTP_METHODS constant to check all non-GET methods.

2. renderingScriptDeferAsync false-positived on non-executable script
   types like <script type="application/ld+json" src="...">. Now
   skips scripts with non-executable type attributes (matches the
   existing nextjsNoNativeScript pattern using EXECUTABLE_SCRIPT_TYPES).

3. queryStableQueryClient false-positived on non-component utility
   functions. Now restricts to uppercase-named functions (component
   convention) instead of all function scopes.

Also:
- Removed redundant condition in jsFlatmapFilter isFilterBoolean check
- Replaced inline ["POST","PUT",...] array in queryNoUseQueryForMutation
  with existing MUTATING_HTTP_METHODS constant (DRY)
- Removed unnecessary 'as string' type cast
- Added edge-cases.tsx fixture testing false-positive freedom:
  correct property order, PUT/DELETE methods, validated server fns,
  JSON-LD scripts, deferred scripts
- Added 4 false-positive-freedom tests

Total tests: 186 → 190 (+4 edge case tests)

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
- Remove dead empty ArrowFunctionExpression visitor in
  tanstackStartNoNavigateInRender (violated 'Remove unused code')
- Move STABLE_HOOK_WRAPPERS to constants.ts (was inline in
  tanstack-query.ts, violated 'Put all magic numbers in constants.ts')
- Move SCRIPT_LOADING_ATTRIBUTES to constants.ts (was inline in
  performance.ts, same violation)

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Bugbot finding #3 — navigate-in-render ignores arrow components (Medium):
  Rewrote tanstackStartNoNavigateInRender to drop componentDepth tracking
  entirely. Since the rule is already scoped to /routes/ files, it now
  flags any navigate() call that isn't inside useEffect or a JSX event
  handler (onClick, onChange, etc.). This correctly catches arrow
  function components like const MyPage = () => { navigate(...) }.

Bugbot finding #4 — server-fn-method-order has no triggering test (Medium):
  The wrongMethodOrder fixture only had .handler() (single method, nothing
  to compare). Changed to .handler().inputValidator() which has two
  order-sensitive methods in the wrong order. Added test case. Also
  simplified the createServerFn mock to use a Proxy for chainability.

Total tests: 190 → 192 (+2 previously untested rules now covered)

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

Bugbot finding: componentDepth was incremented for arrow function
components (const App = () => {...}) but never decremented — no
VariableDeclarator:exit handler. This caused componentDepth to leak,
so any new QueryClient() at module scope after an arrow component
would be falsely flagged.

Added the exit handler matching the enter condition, same pattern
used by architecture.ts for component tracking.

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

Bugbot finding: tanstackStartRedirectInTryCatch flagged throw redirect()
inside catch blocks, but that's safe — a throw inside catch propagates
normally and isn't re-caught. The rule's own message recommended
're-throw in the catch' which would trigger the same warning. Fixed by
tracking CatchClause depth separately and skipping reports inside catch.

Bugbot finding: tanstackStartServerFnMethodOrder fired on every
CallExpression in a chain, producing duplicate diagnostics for 3+
method chains. Fixed by only reporting from the outermost call (where
the last collected method matches the current node's method).

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

Bugbot: isFilterBoolean matched any single-param arrow whose body was
any Identifier (e.g. .filter(x => someExternalVar)), not just identity
functions like .filter(x => x). Fixed by verifying the body identifier
name matches the parameter name.

Bugbot: .map().filter(Boolean) triggered both jsFlatmapFilter and
jsCombineIterations. Added early return in jsCombineIterations to skip
.map().filter() pairs — the more specific jsFlatmapFilter handles
those with a better suggestion (.flatMap instead of generic reduce).

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

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

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

There are 3 total unresolved issues (including 1 from previous review).

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 40e65d2. Configure here.

const innerMethod = innerCall.callee.property.name;
if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;

if (innerMethod === "map" && outerMethod === "filter") 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.

Overly broad exclusion drops .map().filter() lint coverage

Medium Severity

The new exclusion if (innerMethod === "map" && outerMethod === "filter") return in jsCombineIterations skips all .map().filter() chains, but the new jsFlatmapFilter rule only catches the subset .map().filter(Boolean) (or identity arrow). This creates a regression where .map(transform).filter(customPredicate) is no longer flagged by either rule, losing previously-existing lint coverage. The exclusion needs to be narrowed to only skip when the filter argument is Boolean or an identity function.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 40e65d2. Configure here.

}
},
}),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

False positive on type="module" scripts (deferred by default)

Medium Severity

The renderingScriptDeferAsync rule treats type="module" scripts as executable (via EXECUTABLE_SCRIPT_TYPES) and requires an explicit defer or async attribute. However, per the HTML spec, module scripts are deferred by default — the defer attribute has no effect on them. A <script type="module" src="..."> without defer or async will be incorrectly flagged, producing a false positive.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 40e65d2. 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