Skip to content

feat(architecture): add prefer-schema-validation rule#740

Open
NisargIO wants to merge 2 commits into
mainfrom
cursor/prefer-schema-validation-rule-72f6
Open

feat(architecture): add prefer-schema-validation rule#740
NisargIO wants to merge 2 commits into
mainfrom
cursor/prefer-schema-validation-rule-72f6

Conversation

@NisargIO

@NisargIO NisargIO commented Jun 8, 2026

Copy link
Copy Markdown
Member

Why

Catches hand-rolled runtime type/shape validation — a function that checks an object's structure with several typeof comparisons is a schema written by hand. Defining the shape once with a schema validator (Zod, Valibot, Yup) and parsing the value keeps the TypeScript type and the runtime check in sync, narrows the value, and removes plumbing that drifts as the shape grows.

Before:

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof value.id === "string" &&
    typeof value.name === "string" &&
    typeof value.age === "number"
  );
}

After:

import { z } from "zod";

const userSchema = z.object({ id: z.string(), name: z.string(), age: z.number() });

const parseUser = (value: unknown): User => userSchema.parse(value);

What changed

  • Added react-doctor/prefer-schema-validation (architecture bucket → Maintainability, warn).
  • Detects functions whose intent is validation — a TS type predicate / assertion (value is User, asserts input is Config) or a validator-named binding (isUser, hasFoo, validateConfig, assertX, check*, ensure*, verify*, guard*).
  • Reports when such a function performs two or more distinct typeof param.member === "<tag>" checks on a single parameter.
  • Allows (stays quiet on):
    • polymorphic dispatch on the parameter itself (typeof value === "string") — not a member check, so serializers and the no-polymorphic-children surface are untouched;
    • non-validator functions even with several member checks (serializers/visitors are a v1 non-goal);
    • typeof checks rooted at a different object;
    • checks inside nested functions (counted against their own validation, not the enclosing one);
    • repeated checks on the same property (optional-field guards dedupe to one);
    • dynamic computed members (value[key]) and comparisons against non-typeof string literals;
    • destructured-parameter validators (v1 non-goal).
  • Resolves the validated-parameter root through as casts, parentheses, optional chaining, and non-null assertions, and resolves the function's binding name from declarations, const initializers, object/class members, and assignments.
  • Adds a 24-case adversarial test suite (type guards, assertion functions, JS validators, object/class methods, member-assigned validators, as/optional-chain roots, nested paths, anonymous guards; plus the full valid/non-goal matrix).
  • Regenerates the rule registry and adds a changeset.

Eval results

Scanned six large public React/TypeScript monorepos with the built CLI (--json --no-score --no-dead-code) and filtered to the rule. Every hit was inspected by hand; all were genuine hand-rolled type guards / validators (e.g. isAppendStreamOptions, isVercelApiErrorShape, isNext12ApiResponse, isValidAuthTokenPair, isDroppableData). No false positives. Repos that lean on Zod (cal.com) or are pure UI (shadcn/ui) produced zero hits, confirming the rule targets the right anti-pattern.

Check Result
Repos scanned 6 (shadcn/ui, formbricks, trigger.dev, novu, twenty, cal.com)
Total diagnostics ~15,800
Target rule prefer-schema-validation
Diagnostics 10 unique (0 shadcn · 2 formbricks · 4 trigger.dev · 1 novu · 3 twenty · 0 cal.com)
False positives found 0 (all 10 inspected manually)

Test plan

  • pnpm exec vp test run packages/oxlint-plugin-react-doctor/src/plugin/rules/architecture/prefer-schema-validation.test.ts — 24 passed
  • pnpm exec vp test run packages/react-doctor/tests/rule-metadata.test.ts — title/recommendation conventions pass
  • pnpm --filter oxlint-plugin-react-doctor typecheck — passes (regenerates registry + tsc --noEmit)
  • pnpm lint / pnpm format:check — pass

v1 non-goals

  • Non-validator functions (serializers/visitors) doing typeof member inspection without a type predicate or validator-like name.
  • Destructured-parameter validators.
  • Other validation idioms (Array.isArray, "x" in obj, obj.x === undefined, instanceof) — typeof is the v1 surface.
Open in Web Open in Cursor 

cursoragent and others added 2 commits June 8, 2026 05:39
Flags hand-rolled runtime type/shape validation — a type-predicate /
assertion function or a validator-named function (isUser, validateConfig,
assertX) that checks an object's shape with two or more distinct typeof
member checks — and recommends parsing with a schema validator (Zod,
Valibot, Yup) instead.

Only `typeof param.member === "<tag>"` checks count, so polymorphic
dispatch on the parameter itself, serializers without a validator name,
nested-function checks, and dynamic computed members stay quiet.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>
Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>
@pkg-pr-new

pkg-pr-new Bot commented Jun 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/eslint-plugin-react-doctor@740
npm i https://pkg.pr.new/oxlint-plugin-react-doctor@740
npm i https://pkg.pr.new/react-doctor@740

commit: 1df103d

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

No React Doctor issues found. 🎉

Reviewed by React Doctor for commit 1df103d.

@aidenybai aidenybai marked this pull request as ready for review June 8, 2026 06:27

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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.

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 1df103d. Configure here.

const VALIDATOR_WORD_PREFIX_PATTERN = /^(validate|assert|ensure|verify|guard|check)/i;

const looksLikeValidatorName = (name: string): boolean =>
VALIDATOR_CAMEL_PREFIX_PATTERN.test(name) || VALIDATOR_WORD_PREFIX_PATTERN.test(name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Validator prefix substring false positives

Low Severity

VALIDATOR_WORD_PREFIX_PATTERN treats any function name that merely starts with substrings like guard, check, verify, or assert as a validator. Names such as guardian, checkbox, or verification can be misclassified and warned when they use two unrelated typeof member checks, despite not being validation helpers.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1df103d. 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