feat(architecture): add prefer-schema-validation rule#740
Conversation
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>
commit: |
|
No React Doctor issues found. 🎉 Reviewed by React Doctor for commit |
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 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); |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 1df103d. Configure here.


Why
Catches hand-rolled runtime type/shape validation — a function that checks an object's structure with several
typeofcomparisons 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:
After:
What changed
react-doctor/prefer-schema-validation(architecture bucket →Maintainability,warn).value is User,asserts input is Config) or a validator-named binding (isUser,hasFoo,validateConfig,assertX,check*,ensure*,verify*,guard*).typeof param.member === "<tag>"checks on a single parameter.typeof value === "string") — not a member check, so serializers and theno-polymorphic-childrensurface are untouched;typeofchecks rooted at a different object;value[key]) and comparisons against non-typeofstring literals;ascasts, parentheses, optional chaining, and non-null assertions, and resolves the function's binding name from declarations,constinitializers, object/class members, and assignments.as/optional-chain roots, nested paths, anonymous guards; plus the full valid/non-goal matrix).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.6(shadcn/ui, formbricks, trigger.dev, novu, twenty, cal.com)~15,800prefer-schema-validation10unique (0shadcn ·2formbricks ·4trigger.dev ·1novu ·3twenty ·0cal.com)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 passedpnpm exec vp test run packages/react-doctor/tests/rule-metadata.test.ts— title/recommendation conventions passpnpm --filter oxlint-plugin-react-doctor typecheck— passes (regenerates registry +tsc --noEmit)pnpm lint/pnpm format:check— passv1 non-goals
typeofmember inspection without a type predicate or validator-like name.Array.isArray,"x" in obj,obj.x === undefined,instanceof) —typeofis the v1 surface.