eslint-plugin-zod-v4 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,6 +12,7 @@ ESLint plugin for Zod v4 best practices and migration from v3.
12
12
  - Auto-fix support for most rules
13
13
  - Educational error messages explaining the correct approach
14
14
  - Full ESLint 9+ flat config support
15
+ - **React-aware**: Recognizes `useMemo`/`useCallback` for schema memoization
15
16
 
16
17
  ## Installation
17
18
 
@@ -68,18 +69,18 @@ These rules detect Zod v3 patterns that will break in v4.
68
69
 
69
70
  | Rule | Description | Fixable |
70
71
  |------|-------------|---------|
71
- | [no-deprecated-string-format](docs/rules/no-deprecated-string-format.md) | Disallow `z.string().email()` etc. Use `z.email()` instead. | Yes |
72
- | [no-record-single-arg](docs/rules/no-record-single-arg.md) | Require `z.record(keySchema, valueSchema)` with two arguments. | No |
73
- | [no-deprecated-error-params](docs/rules/no-deprecated-error-params.md) | Disallow `invalid_type_error`/`required_error`. Use `error` param. | Yes |
74
- | [no-deprecated-format-methods](docs/rules/no-deprecated-format-methods.md) | Disallow `.format()`/`.flatten()` on ZodError. Use `z.treeifyError()`. | No |
75
- | [no-merge-method](docs/rules/no-merge-method.md) | Disallow `.merge()`. Use `.extend()` instead. | No |
76
- | [no-superrefine](docs/rules/no-superrefine.md) | Disallow `.superRefine()`. Use `.check()` instead. | No |
77
- | [no-errors-property](docs/rules/no-errors-property.md) | Disallow `error.errors`. Use `error.issues` instead. | Yes |
78
- | [no-deprecated-object-methods](docs/rules/no-deprecated-object-methods.md) | Disallow `.strict()`/`.passthrough()`/`.strip()`. Use top-level functions. | No |
79
- | [no-native-enum](docs/rules/no-native-enum.md) | Disallow `z.nativeEnum()`. Use `z.enum()` instead. | No |
80
- | [no-deep-partial](docs/rules/no-deep-partial.md) | Disallow `.deepPartial()` (removed in v4). | No |
81
- | [no-deprecated-ip-methods](docs/rules/no-deprecated-ip-methods.md) | Disallow `.ip()`/`.cidr()`. Use `.ipv4()`/`.ipv6()` variants. | No |
82
- | [no-promise-schema](docs/rules/no-promise-schema.md) | Disallow `z.promise()`. Await before parsing. | No |
72
+ | [no-deprecated-string-format](docs/rules/no-deprecated-string-format.md) | Disallow `z.string().email()` etc. Use `z.email()` instead. | ✅ Yes |
73
+ | [no-record-single-arg](docs/rules/no-record-single-arg.md) | Require `z.record(keySchema, valueSchema)` with two arguments. | ❌ No |
74
+ | [no-deprecated-error-params](docs/rules/no-deprecated-error-params.md) | Disallow `invalid_type_error`/`required_error`. Use `error` param. | ✅ Yes |
75
+ | [no-deprecated-format-methods](docs/rules/no-deprecated-format-methods.md) | Disallow `.format()`/`.flatten()` on ZodError. Use `z.treeifyError()`. | ❌ No |
76
+ | [no-merge-method](docs/rules/no-merge-method.md) | Disallow `.merge()`. Use `.extend()` instead. | ❌ No |
77
+ | [no-superrefine](docs/rules/no-superrefine.md) | Disallow `.superRefine()`. Use `.check()` instead. | ✅ Yes |
78
+ | [no-errors-property](docs/rules/no-errors-property.md) | Disallow `error.errors`. Use `error.issues` instead. | ✅ Yes |
79
+ | [no-deprecated-object-methods](docs/rules/no-deprecated-object-methods.md) | Disallow `.strict()`/`.passthrough()`/`.strip()`. Use top-level functions. | ❌ No |
80
+ | [no-native-enum](docs/rules/no-native-enum.md) | Disallow `z.nativeEnum()`. Use `z.enum()` instead. | ❌ No |
81
+ | [no-deep-partial](docs/rules/no-deep-partial.md) | Disallow `.deepPartial()` (removed in v4). | ❌ No |
82
+ | [no-deprecated-ip-methods](docs/rules/no-deprecated-ip-methods.md) | Disallow `.ip()`/`.cidr()`. Use `.ipv4()`/`.ipv6()` variants. | ❌ No |
83
+ | [no-promise-schema](docs/rules/no-promise-schema.md) | Disallow `z.promise()`. Await before parsing. | ❌ No |
83
84
 
84
85
  ### Best Practices (severity: warn)
85
86
 
@@ -87,9 +88,62 @@ These rules enforce Zod v4 best practices for optimal code quality.
87
88
 
88
89
  | Rule | Description | Fixable |
89
90
  |------|-------------|---------|
90
- | [prefer-safeParse](docs/rules/prefer-safeParse.md) | Prefer `.safeParse()` over `.parse()` for explicit error handling. | Yes |
91
- | [no-schema-in-render](docs/rules/no-schema-in-render.md) | Disallow creating schemas inside functions/components. | No |
92
- | [prefer-error-param](docs/rules/prefer-error-param.md) | Prefer `error` param over deprecated `message` param. | Yes |
91
+ | [prefer-safeParse](docs/rules/prefer-safeParse.md) | Prefer `.safeParse()` over `.parse()` for explicit error handling. | ✅ Yes |
92
+ | [no-schema-in-render](docs/rules/no-schema-in-render.md) | Disallow creating schemas inside functions/components. **React-aware**: allows `useMemo`/`useCallback`. | ❌ No |
93
+ | [prefer-error-param](docs/rules/prefer-error-param.md) | Prefer `error` param over deprecated `message` param. | ✅ Yes |
94
+
95
+ ## React Integration
96
+
97
+ ### Schema Creation in Components
98
+
99
+ The `no-schema-in-render` rule is React-aware and recognizes memoization patterns:
100
+
101
+ ```tsx
102
+ // ❌ Bad - Schema recreated every render
103
+ const MyComponent = () => {
104
+ const schema = z.object({ name: z.string() }) // Error!
105
+ return <Form schema={schema} />
106
+ }
107
+
108
+ // ✅ Good - Schema at module level
109
+ const schema = z.object({ name: z.string() })
110
+ const MyComponent = () => {
111
+ return <Form schema={schema} />
112
+ }
113
+
114
+ // ✅ Good - Schema memoized with useMemo (for translated schemas)
115
+ const MyComponent = () => {
116
+ const t = useTranslations()
117
+ const schema = useMemo(() => z.object({
118
+ email: z.email(t('invalidEmail')),
119
+ }), [t])
120
+ return <Form schema={schema} />
121
+ }
122
+
123
+ // ✅ Good - Factory function with useMemo
124
+ const createSchema = (t) => z.object({ email: z.email(t('error')) })
125
+ const MyComponent = () => {
126
+ const t = useTranslations()
127
+ const schema = useMemo(() => createSchema(t), [t])
128
+ return <Form schema={schema} />
129
+ }
130
+ ```
131
+
132
+ ### TypeScript Workaround for `.check()` ctx.addIssue
133
+
134
+ When using `.check()`, TypeScript may incorrectly flag `ctx.addIssue()` calls:
135
+
136
+ ```typescript
137
+ // If you see: @typescript-eslint/no-unsafe-call on ctx.addIssue()
138
+ // Add this comment to suppress the false positive:
139
+ .check((ctx) => {
140
+ const { value: data } = ctx;
141
+ if (!data.name) {
142
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Zod v4 ctx.addIssue is type-safe at runtime
143
+ ctx.addIssue({ code: 'custom', message: 'Name required' });
144
+ }
145
+ })
146
+ ```
93
147
 
94
148
  ## Migration Guide
95
149
 
@@ -124,9 +178,11 @@ z.record(z.string(), z.string())
124
178
  ```javascript
125
179
  // Before (deprecated)
126
180
  z.string({ invalid_type_error: "Must be string", required_error: "Required" })
181
+ z.string().refine(fn, { message: "Error" })
127
182
 
128
183
  // After (v4)
129
184
  z.string({ error: "Must be string" })
185
+ z.string().refine(fn, { error: "Error" })
130
186
  // Or with function
131
187
  z.string({ error: (iss) => `Error: ${iss.code}` })
132
188
  ```
@@ -164,7 +220,7 @@ schema1.extend(schema2.shape)
164
220
  z.object({ ...schema1.shape, ...schema2.shape })
165
221
  ```
166
222
 
167
- #### 7. Super Refine
223
+ #### 7. Super Refine → Check
168
224
 
169
225
  ```javascript
170
226
  // Before (deprecated)
@@ -175,6 +231,14 @@ schema.superRefine((val, ctx) => {
175
231
  })
176
232
 
177
233
  // After (v4)
234
+ schema.check((ctx) => {
235
+ const { value: val } = ctx;
236
+ if (!isValid(val)) {
237
+ ctx.addIssue({ code: "custom", message: "Invalid" })
238
+ }
239
+ })
240
+
241
+ // Or for simple cases
178
242
  schema.check((val) => isValid(val) || "Invalid")
179
243
  ```
180
244
 
@@ -235,6 +299,22 @@ schema.partial() // shallow only
235
299
  // For deep partial, manually create nested partial schemas
236
300
  ```
237
301
 
302
+ ## Changelog
303
+
304
+ ### v0.2.0 (2025-12-16)
305
+
306
+ **New Features:**
307
+ - `no-superrefine`: Now has **auto-fix** support! Transforms `.superRefine((data, ctx) => {...})` to `.check((ctx) => { const { value: data } = ctx; ...})`
308
+ - `no-schema-in-render`: Now **recognizes `useMemo` and `useCallback`** - schemas inside memoized callbacks are allowed
309
+ - Improved error messages with migration examples
310
+
311
+ **Bug Fixes:**
312
+ - Fixed false positives in `no-schema-in-render` when using React memoization hooks
313
+
314
+ ### v0.1.1 (2025-12-15)
315
+
316
+ - Initial release with 15 rules (12 breaking changes + 3 best practices)
317
+
238
318
  ## Contributing
239
319
 
240
320
  Contributions are welcome! Please read our contributing guidelines before submitting a PR.
package/dist/index.cjs CHANGED
@@ -11685,16 +11685,6 @@ function isZodChainCall(node) {
11685
11685
  }
11686
11686
  return false;
11687
11687
  }
11688
- function isInsideFunction(node) {
11689
- let current = node.parent;
11690
- while (current) {
11691
- if (current.type === "FunctionDeclaration" || current.type === "FunctionExpression" || current.type === "ArrowFunctionExpression") {
11692
- return true;
11693
- }
11694
- current = current.parent;
11695
- }
11696
- return false;
11697
- }
11698
11688
  function isLikelyZodSchemaCall(node) {
11699
11689
  if (node.callee.type !== "MemberExpression") return false;
11700
11690
  if (isZodChainCall(node)) return true;
@@ -11707,6 +11697,27 @@ function isLikelyZodSchemaCall(node) {
11707
11697
  }
11708
11698
  return false;
11709
11699
  }
11700
+ var REACT_MEMO_HOOKS = ["useMemo", "useCallback"];
11701
+ function isInsideMemoizedCallback(node) {
11702
+ if (node.type !== "ArrowFunctionExpression" && node.type !== "FunctionExpression") {
11703
+ return false;
11704
+ }
11705
+ const parent = node.parent;
11706
+ if (!parent || parent.type !== "CallExpression") {
11707
+ return false;
11708
+ }
11709
+ if (parent.arguments[0] !== node) {
11710
+ return false;
11711
+ }
11712
+ const callee = parent.callee;
11713
+ if (callee.type === "Identifier") {
11714
+ return REACT_MEMO_HOOKS.includes(callee.name);
11715
+ }
11716
+ if (callee.type === "MemberExpression" && callee.property.type === "Identifier") {
11717
+ return REACT_MEMO_HOOKS.includes(callee.property.name);
11718
+ }
11719
+ return false;
11720
+ }
11710
11721
 
11711
11722
  // src/rules/no-deprecated-string-format.ts
11712
11723
  var noDeprecatedStringFormat = createRule({
@@ -11962,9 +11973,10 @@ var noSuperRefine = createRule({
11962
11973
  docs: {
11963
11974
  description: "Disallow deprecated .superRefine() method in favor of .check()"
11964
11975
  },
11976
+ fixable: "code",
11965
11977
  schema: [],
11966
11978
  messages: {
11967
- deprecatedSuperRefine: ".superRefine() is deprecated in Zod v4. Use .check() instead for custom validations."
11979
+ deprecatedSuperRefine: ".superRefine() is deprecated in Zod v4. Use .check() instead.\n\nMigration pattern:\n// Before (Zod v3):\n.superRefine((data, ctx) => {\n ctx.addIssue({ code: 'custom', message: '...' });\n})\n\n// After (Zod v4):\n.check((ctx) => {\n const { value: data } = ctx;\n ctx.addIssue({ code: 'custom', message: '...' });\n})"
11968
11980
  }
11969
11981
  },
11970
11982
  defaultOptions: [],
@@ -11975,9 +11987,60 @@ var noSuperRefine = createRule({
11975
11987
  const methodName = getMethodName(node.callee);
11976
11988
  if (methodName !== "superRefine") return;
11977
11989
  if (!isLikelyZodSchemaCall(node)) return;
11990
+ const callback = node.arguments[0];
11978
11991
  context.report({
11979
11992
  node,
11980
- messageId: "deprecatedSuperRefine"
11993
+ messageId: "deprecatedSuperRefine",
11994
+ fix(fixer) {
11995
+ if (!callback) return null;
11996
+ const memberExpr = node.callee;
11997
+ const property = memberExpr.property;
11998
+ if (property.type !== "Identifier") return null;
11999
+ if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression") {
12000
+ return null;
12001
+ }
12002
+ const params = callback.params;
12003
+ if (params.length === 0) {
12004
+ return fixer.replaceText(property, "check");
12005
+ }
12006
+ const sourceCode = context.sourceCode;
12007
+ const firstParam = params[0];
12008
+ const secondParam = params[1];
12009
+ if (params.length === 1 && firstParam) {
12010
+ const paramName = sourceCode.getText(firstParam);
12011
+ if (paramName === "ctx" || paramName === "context") {
12012
+ return fixer.replaceText(property, "check");
12013
+ }
12014
+ if (callback.body.type === "BlockStatement") {
12015
+ const bodyStart = callback.body.range[0] + 1;
12016
+ const dataDecl = ` const { value: ${paramName} } = ctx;`;
12017
+ return [
12018
+ fixer.replaceText(property, "check"),
12019
+ fixer.replaceText(firstParam, "ctx"),
12020
+ fixer.insertTextAfterRange([bodyStart, bodyStart], dataDecl)
12021
+ ];
12022
+ }
12023
+ return fixer.replaceText(property, "check");
12024
+ }
12025
+ if (params.length >= 2 && firstParam && secondParam) {
12026
+ const dataParamName = sourceCode.getText(firstParam);
12027
+ const ctxParamName = sourceCode.getText(secondParam);
12028
+ if (callback.body.type === "BlockStatement") {
12029
+ const bodyStart = callback.body.range[0] + 1;
12030
+ const useCtxName = ctxParamName === "ctx" ? "ctx" : ctxParamName;
12031
+ const dataDecl = ` const { value: ${dataParamName} } = ${useCtxName};`;
12032
+ const paramsStart = firstParam.range[0];
12033
+ const paramsEnd = secondParam.range[1];
12034
+ return [
12035
+ fixer.replaceText(property, "check"),
12036
+ fixer.replaceTextRange([paramsStart, paramsEnd], useCtxName),
12037
+ fixer.insertTextAfterRange([bodyStart, bodyStart], dataDecl)
12038
+ ];
12039
+ }
12040
+ return fixer.replaceText(property, "check");
12041
+ }
12042
+ return fixer.replaceText(property, "check");
12043
+ }
11981
12044
  });
11982
12045
  }
11983
12046
  };
@@ -12270,7 +12333,7 @@ var noSchemaInRender = createRule({
12270
12333
  },
12271
12334
  schema: [],
12272
12335
  messages: {
12273
- schemaInRender: "Avoid creating Zod schemas inside functions or React components. Schemas are recreated on every render/call, causing performance issues. Move the schema to module scope or use useMemo."
12336
+ schemaInRender: "Avoid creating Zod schemas inside functions or React components. Schemas are recreated on every render/call, causing performance issues. Move the schema to module scope, use useMemo(() => z.object({...}), [deps]), or extract to a factory function called with useMemo."
12274
12337
  }
12275
12338
  },
12276
12339
  defaultOptions: [],
@@ -12315,7 +12378,7 @@ var noSchemaInRender = createRule({
12315
12378
  "custom",
12316
12379
  "preprocess",
12317
12380
  "coerce",
12318
- // Top-level format functions
12381
+ // Top-level format functions (Zod v4)
12319
12382
  "email",
12320
12383
  "url",
12321
12384
  "uuid",
@@ -12325,13 +12388,28 @@ var noSchemaInRender = createRule({
12325
12388
  "ip",
12326
12389
  "ipv4",
12327
12390
  "ipv6",
12391
+ "cidr",
12392
+ "cidrv4",
12393
+ "cidrv6",
12328
12394
  "datetime",
12329
- "base64"
12395
+ "base64",
12396
+ "base64url",
12397
+ "jwt",
12398
+ "emoji",
12399
+ "nanoid",
12400
+ "ascii",
12401
+ "utf8",
12402
+ "hexadecimal",
12403
+ "e164",
12404
+ "bic",
12405
+ "iban",
12406
+ "time",
12407
+ "duration"
12330
12408
  ];
12331
12409
  const object = node.callee.object;
12332
12410
  if (object.type !== "Identifier" || object.name !== "z") return;
12333
12411
  if (!schemaCreatingMethods.includes(methodName)) return;
12334
- if (!isInsideFunction(node)) return;
12412
+ if (!isInsideFunctionButNotMemoized(node)) return;
12335
12413
  context.report({
12336
12414
  node,
12337
12415
  messageId: "schemaInRender"
@@ -12340,6 +12418,52 @@ var noSchemaInRender = createRule({
12340
12418
  };
12341
12419
  }
12342
12420
  });
12421
+ function isInsideFunctionButNotMemoized(node) {
12422
+ let current = node.parent;
12423
+ const functionStack = [];
12424
+ while (current) {
12425
+ if (current.type === "FunctionDeclaration" || current.type === "FunctionExpression" || current.type === "ArrowFunctionExpression") {
12426
+ functionStack.push(current);
12427
+ }
12428
+ current = current.parent;
12429
+ }
12430
+ if (functionStack.length === 0) {
12431
+ return false;
12432
+ }
12433
+ const innermostFunction = functionStack[0];
12434
+ if (isInsideMemoizedCallback(innermostFunction)) {
12435
+ return false;
12436
+ }
12437
+ for (let i = 1; i < functionStack.length; i++) {
12438
+ const func = functionStack[i];
12439
+ if (func && isInsideMemoizedCallback(func)) {
12440
+ return true;
12441
+ }
12442
+ }
12443
+ const outermostFunction = functionStack[functionStack.length - 1];
12444
+ if (isModuleLevelFactoryFunction(outermostFunction)) {
12445
+ return false;
12446
+ }
12447
+ return true;
12448
+ }
12449
+ function isModuleLevelFactoryFunction(node) {
12450
+ if (node.type !== "ArrowFunctionExpression") {
12451
+ return false;
12452
+ }
12453
+ const arrowFunc = node;
12454
+ if (arrowFunc.body.type === "BlockStatement") {
12455
+ return false;
12456
+ }
12457
+ const parent = node.parent;
12458
+ if (parent?.type === "VariableDeclarator") {
12459
+ const varDecl = parent.parent;
12460
+ if (varDecl?.type === "VariableDeclaration") {
12461
+ const grandParent = varDecl.parent;
12462
+ return grandParent?.type === "Program" || grandParent?.type === "ExportNamedDeclaration";
12463
+ }
12464
+ }
12465
+ return false;
12466
+ }
12343
12467
 
12344
12468
  // src/rules/prefer-error-param.ts
12345
12469
  var preferErrorParam = createRule({
@@ -12384,7 +12508,7 @@ var preferErrorParam = createRule({
12384
12508
  // src/index.ts
12385
12509
  var meta = {
12386
12510
  name: "eslint-plugin-zod-v4",
12387
- version: "0.1.0"
12511
+ version: "0.1.1"
12388
12512
  };
12389
12513
  var rules = {
12390
12514
  // Breaking Changes