@schemashift/zod-v3-v4 0.3.0 → 0.8.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 ADDED
@@ -0,0 +1,98 @@
1
+ # @schemashift/zod-v3-v4
2
+
3
+ Zod v3 to v4 upgrade transformer for SchemaShift. Handles 17+ breaking changes between Zod v3 and v4 with auto-transforms and runtime behavior warnings.
4
+
5
+ **Tier:** Individual
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @schemashift/zod-v3-v4
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import { createZodV3ToV4Handler } from '@schemashift/zod-v3-v4';
17
+ import { TransformEngine } from '@schemashift/core';
18
+
19
+ const engine = new TransformEngine();
20
+ engine.registerHandler('zod-v3', 'v4', createZodV3ToV4Handler());
21
+ ```
22
+
23
+ ## Breaking Changes Handled
24
+
25
+ ### Auto-Transforms
26
+
27
+ These changes are automatically applied to your code:
28
+
29
+ | v3 Pattern | v4 Pattern | Notes |
30
+ |-----------|-----------|-------|
31
+ | `z.record(valueSchema)` | `z.record(z.string(), valueSchema)` | Explicit key type required |
32
+ | `schemaA.merge(schemaB)` | `schemaA.extend(schemaB.shape)` | `.merge()` deprecated |
33
+ | `z.nativeEnum(MyEnum)` | `z.enum(MyEnum)` | `nativeEnum` renamed |
34
+ | `.superRefine(fn)` | `.check(fn)` | Callback signature identical |
35
+ | `error.errors` | `error.issues` | Property renamed |
36
+ | `{ invalid_type_error: 'msg' }` | `{ error: 'msg' }` | Unified error param |
37
+ | `{ required_error: 'msg' }` | `{ error: 'msg' }` | Unified error param |
38
+ | Both error params | `{ error: (issue) => ... }` | Function-based error |
39
+
40
+ ### Deprecation Warnings
41
+
42
+ These methods still work but are deprecated in v4:
43
+
44
+ | Method | Replacement | Warning |
45
+ |--------|-------------|---------|
46
+ | `.superRefine()` | `.check()` | Auto-renamed, warning emitted |
47
+ | `.strict()` | `z.strictObject()` | Structural change, review needed |
48
+ | `.passthrough()` | `z.looseObject()` | Structural change, review needed |
49
+
50
+ ### Runtime Behavior Warnings
51
+
52
+ These changes do not cause compile-time errors but silently change runtime behavior:
53
+
54
+ | Pattern | Issue |
55
+ |---------|-------|
56
+ | `instanceof Error` on ZodError | `ZodError` no longer extends `Error` in v4 |
57
+ | `.refine()` followed by `.transform()` | `.transform()` now runs even when `.refine()` fails |
58
+ | `.default()` + `.optional()` | `.default()` always provides a value, making `.optional()` a no-op |
59
+ | `.pipe()` | Stricter type checking, may need explicit type annotations |
60
+
61
+ ### Other Detected Changes
62
+
63
+ | Pattern | Warning |
64
+ |---------|---------|
65
+ | `.flatten()` | Removed in v4, use `.format()` instead |
66
+ | `z.function()` | Input/output parameter changes |
67
+ | `.uuid()` | Stricter RFC 4122 validation |
68
+ | `z.discriminatedUnion()` | Stricter discriminator requirements |
69
+
70
+ ## Mappings Reference
71
+
72
+ ### Behavior Changes Set
73
+
74
+ Methods with changed behavior in v4: `default`, `uuid`, `record`, `discriminatedUnion`, `function`, `pipe`.
75
+
76
+ ### Deprecated Methods Set
77
+
78
+ Methods deprecated in v4: `merge`, `superRefine`, `strict`, `passthrough`.
79
+
80
+ ### Auto-Transformable Methods
81
+
82
+ Methods that are automatically renamed: `superRefine` → `.check()`.
83
+
84
+ ### Error Property Renames
85
+
86
+ | v3 | v4 |
87
+ |----|----|
88
+ | `.errors` | `.issues` |
89
+ | `.formErrors` | `.formErrors` (unchanged) |
90
+ | `.fieldErrors` | `.fieldErrors` (unchanged) |
91
+
92
+ ### Error Parameter Changes
93
+
94
+ v3 accepted `invalid_type_error` and `required_error` as separate properties. v4 unifies these under a single `error` property (string or function).
95
+
96
+ ## License
97
+
98
+ MIT
package/dist/index.cjs CHANGED
@@ -48,8 +48,37 @@ var BEHAVIOR_CHANGES = /* @__PURE__ */ new Set([
48
48
  // Requires key type
49
49
  "discriminatedUnion",
50
50
  // Stricter discriminator requirements
51
- "function"
51
+ "function",
52
52
  // Input/output parameter changes
53
+ "pipe",
54
+ // Stricter type checking in v4
55
+ "catch"
56
+ // .catch() on optional properties always returns catch value in v4
57
+ ]);
58
+ var DEPRECATED_METHODS = /* @__PURE__ */ new Set([
59
+ "merge",
60
+ // Use .extend() instead
61
+ "superRefine",
62
+ // Use .check() instead
63
+ "strict",
64
+ // Use z.strictObject() instead
65
+ "passthrough"
66
+ // Use z.looseObject() instead
67
+ ]);
68
+ var AUTO_TRANSFORMABLE = /* @__PURE__ */ new Set([
69
+ "superRefine",
70
+ // -> .check()
71
+ "flatten",
72
+ // error.flatten() -> z.flattenError(error)
73
+ "format"
74
+ // error.format() -> z.treeifyError(error)
75
+ ]);
76
+ var UTILITY_TYPES = /* @__PURE__ */ new Set([
77
+ "ZodType",
78
+ "ZodSchema",
79
+ "ZodRawShape",
80
+ "ZodTypeAny",
81
+ "ZodFirstPartyTypeKind"
53
82
  ]);
54
83
 
55
84
  // src/transformer.ts
@@ -63,8 +92,21 @@ var ZodV3ToV4Transformer = class {
63
92
  const originalCode = sourceFile.getFullText();
64
93
  try {
65
94
  this.transformRecordCalls(sourceFile);
95
+ this.transformMerge(sourceFile);
96
+ this.transformNativeEnum(sourceFile);
97
+ this.transformSuperRefine(sourceFile);
66
98
  this.transformErrorProperties(sourceFile);
99
+ this.transformErrorParams(sourceFile);
100
+ this.transformFlatten(sourceFile);
101
+ this.transformFormat(sourceFile);
102
+ this.transformDefAccess(sourceFile);
103
+ this.checkDeprecatedMethods(sourceFile);
67
104
  this.checkBehaviorChanges(sourceFile);
105
+ this.checkInstanceofError(sourceFile);
106
+ this.checkTransformRefineOrder(sourceFile);
107
+ this.checkDefaultOptional(sourceFile);
108
+ this.checkCatchOptional(sourceFile);
109
+ this.checkUtilityTypeImports(sourceFile);
68
110
  return {
69
111
  success: this.errors.length === 0,
70
112
  filePath,
@@ -113,21 +155,394 @@ var ZodV3ToV4Transformer = class {
113
155
  });
114
156
  }
115
157
  /**
116
- * Transform error.errors to error.issues
158
+ * Build a set of variable names that are likely ZodError instances.
159
+ * Used by transformErrorProperties, transformFlatten, and transformFormat.
160
+ */
161
+ buildZodErrorVarSet(sourceFile) {
162
+ const zodErrorVars = /* @__PURE__ */ new Set();
163
+ sourceFile.forEachDescendant((node) => {
164
+ if (import_ts_morph.Node.isVariableDeclaration(node)) {
165
+ const typeNode = node.getTypeNode();
166
+ if (typeNode?.getText().includes("ZodError")) {
167
+ zodErrorVars.add(node.getName());
168
+ }
169
+ }
170
+ if (import_ts_morph.Node.isCatchClause(node)) {
171
+ const param = node.getVariableDeclaration();
172
+ if (param) {
173
+ const block = node.getBlock();
174
+ const blockText = block.getText();
175
+ if (blockText.includes("instanceof ZodError") || blockText.includes("ZodError")) {
176
+ zodErrorVars.add(param.getName());
177
+ }
178
+ }
179
+ }
180
+ if (import_ts_morph.Node.isVariableDeclaration(node)) {
181
+ const initializer = node.getInitializer();
182
+ if (initializer?.getText().includes(".safeParse(")) {
183
+ zodErrorVars.add(`${node.getName()}.error`);
184
+ }
185
+ }
186
+ if (import_ts_morph.Node.isNewExpression(node)) {
187
+ if (node.getExpression().getText() === "ZodError") {
188
+ const parent = node.getParent();
189
+ if (parent && import_ts_morph.Node.isVariableDeclaration(parent)) {
190
+ zodErrorVars.add(parent.getName());
191
+ }
192
+ }
193
+ }
194
+ });
195
+ return zodErrorVars;
196
+ }
197
+ /**
198
+ * Transform error.errors to error.issues using AST-based detection
199
+ * instead of heuristic name matching.
117
200
  */
118
201
  transformErrorProperties(sourceFile) {
202
+ const filePath = sourceFile.getFilePath();
203
+ const zodErrorVars = this.buildZodErrorVarSet(sourceFile);
119
204
  sourceFile.forEachDescendant((node) => {
120
205
  if (import_ts_morph.Node.isPropertyAccessExpression(node)) {
121
206
  const name = node.getName();
122
207
  if (name === "errors") {
123
208
  const object = node.getExpression();
124
209
  const objectText = object.getText();
125
- if (objectText.toLowerCase().includes("error") || objectText.toLowerCase().includes("err")) {
210
+ const isZodError = zodErrorVars.has(objectText) || // Also check if accessing .error.errors (safeParse pattern)
211
+ objectText.endsWith(".error") && zodErrorVars.has(`${objectText.replace(/\.error$/, "")}.error`);
212
+ if (isZodError) {
126
213
  const fullText = node.getText();
127
214
  const newText = fullText.replace(/\.errors$/, ".issues");
128
215
  node.replaceWithText(newText);
129
216
  this.warnings.push(
130
- `${sourceFile.getFilePath()}:${node.getStartLineNumber()}: .errors renamed to .issues (ZodError property change)`
217
+ `${filePath}:${node.getStartLineNumber()}: .errors renamed to .issues (ZodError property change)`
218
+ );
219
+ }
220
+ }
221
+ }
222
+ });
223
+ }
224
+ /**
225
+ * Auto-transform error.flatten() to z.flattenError(error) in v4.
226
+ */
227
+ transformFlatten(sourceFile) {
228
+ const filePath = sourceFile.getFilePath();
229
+ const zodErrorVars = this.buildZodErrorVarSet(sourceFile);
230
+ const nodesToTransform = [];
231
+ sourceFile.forEachDescendant((node) => {
232
+ if (import_ts_morph.Node.isCallExpression(node)) {
233
+ const expression = node.getExpression();
234
+ if (import_ts_morph.Node.isPropertyAccessExpression(expression) && expression.getName() === "flatten") {
235
+ const objectText = expression.getExpression().getText();
236
+ const isZodError = zodErrorVars.has(objectText) || objectText.endsWith(".error") && zodErrorVars.has(`${objectText.replace(/\.error$/, "")}.error`);
237
+ if (isZodError) {
238
+ nodesToTransform.push({
239
+ node,
240
+ objectText,
241
+ lineNumber: node.getStartLineNumber()
242
+ });
243
+ } else {
244
+ this.warnings.push(
245
+ `${filePath}:${node.getStartLineNumber()}: .flatten() is removed in v4. Use z.flattenError(error) or access error.issues directly. See: https://zod.dev/v4/changelog`
246
+ );
247
+ }
248
+ }
249
+ }
250
+ });
251
+ nodesToTransform.sort((a, b) => b.node.getStart() - a.node.getStart());
252
+ for (const { node, objectText, lineNumber } of nodesToTransform) {
253
+ node.replaceWithText(`z.flattenError(${objectText})`);
254
+ this.warnings.push(
255
+ `${filePath}:${lineNumber}: .flatten() auto-transformed to z.flattenError(). Removed in v4 \u2014 now a standalone function.`
256
+ );
257
+ }
258
+ }
259
+ /**
260
+ * Auto-transform error.format() to z.treeifyError(error) in v4.
261
+ */
262
+ transformFormat(sourceFile) {
263
+ const filePath = sourceFile.getFilePath();
264
+ const zodErrorVars = this.buildZodErrorVarSet(sourceFile);
265
+ const nodesToTransform = [];
266
+ sourceFile.forEachDescendant((node) => {
267
+ if (import_ts_morph.Node.isCallExpression(node)) {
268
+ const expression = node.getExpression();
269
+ if (import_ts_morph.Node.isPropertyAccessExpression(expression) && expression.getName() === "format") {
270
+ const objectText = expression.getExpression().getText();
271
+ const isZodError = zodErrorVars.has(objectText) || objectText.endsWith(".error") && zodErrorVars.has(`${objectText.replace(/\.error$/, "")}.error`);
272
+ if (isZodError) {
273
+ nodesToTransform.push({
274
+ node,
275
+ objectText,
276
+ lineNumber: node.getStartLineNumber()
277
+ });
278
+ }
279
+ }
280
+ }
281
+ });
282
+ nodesToTransform.sort((a, b) => b.node.getStart() - a.node.getStart());
283
+ for (const { node, objectText, lineNumber } of nodesToTransform) {
284
+ node.replaceWithText(`z.treeifyError(${objectText})`);
285
+ this.warnings.push(
286
+ `${filePath}:${lineNumber}: .format() auto-transformed to z.treeifyError(). Removed in v4 \u2014 now a standalone function.`
287
+ );
288
+ }
289
+ }
290
+ /**
291
+ * Auto-transform ._def property access to ._zod.def in v4.
292
+ */
293
+ transformDefAccess(sourceFile) {
294
+ const filePath = sourceFile.getFilePath();
295
+ const nodesToTransform = [];
296
+ sourceFile.forEachDescendant((node) => {
297
+ if (import_ts_morph.Node.isPropertyAccessExpression(node) && node.getName() === "_def") {
298
+ nodesToTransform.push({ node, lineNumber: node.getStartLineNumber() });
299
+ }
300
+ });
301
+ nodesToTransform.sort((a, b) => b.node.getStart() - a.node.getStart());
302
+ for (const { node, lineNumber } of nodesToTransform) {
303
+ const objectText = node.getExpression().getText();
304
+ node.replaceWithText(`${objectText}._zod.def`);
305
+ this.warnings.push(
306
+ `${filePath}:${lineNumber}: ._def auto-transformed to ._zod.def. Internal API \u2014 structure may change between releases.`
307
+ );
308
+ }
309
+ }
310
+ /**
311
+ * Transform .merge(otherSchema) to .extend(otherSchema.shape)
312
+ */
313
+ transformMerge(sourceFile) {
314
+ const filePath = sourceFile.getFilePath();
315
+ const nodesToTransform = [];
316
+ sourceFile.forEachDescendant((node) => {
317
+ if (import_ts_morph.Node.isCallExpression(node)) {
318
+ const expression = node.getExpression();
319
+ if (import_ts_morph.Node.isPropertyAccessExpression(expression) && expression.getName() === "merge") {
320
+ nodesToTransform.push({ node, lineNumber: node.getStartLineNumber() });
321
+ }
322
+ }
323
+ });
324
+ nodesToTransform.sort((a, b) => b.node.getStart() - a.node.getStart());
325
+ for (const { node, lineNumber } of nodesToTransform) {
326
+ const args = node.getArguments();
327
+ if (args.length === 1) {
328
+ const argText = args[0]?.getText();
329
+ if (argText) {
330
+ const expression = node.getExpression();
331
+ if (import_ts_morph.Node.isPropertyAccessExpression(expression)) {
332
+ const objectText = expression.getExpression().getText();
333
+ node.replaceWithText(`${objectText}.extend(${argText}.shape)`);
334
+ this.warnings.push(
335
+ `${filePath}:${lineNumber}: .merge() renamed to .extend() with .shape access. Verify the merged schema exposes .shape.`
336
+ );
337
+ }
338
+ }
339
+ }
340
+ }
341
+ }
342
+ /**
343
+ * Transform z.nativeEnum(X) to z.enum(X)
344
+ */
345
+ transformNativeEnum(sourceFile) {
346
+ const filePath = sourceFile.getFilePath();
347
+ const fullText = sourceFile.getFullText();
348
+ const newText = fullText.replace(/\bz\.nativeEnum\(/g, "z.enum(");
349
+ if (newText !== fullText) {
350
+ sourceFile.replaceWithText(newText);
351
+ this.warnings.push(
352
+ `${filePath}: z.nativeEnum() renamed to z.enum() in v4. Verify enum values are compatible.`
353
+ );
354
+ }
355
+ }
356
+ /**
357
+ * Check for deprecated methods and add warnings
358
+ */
359
+ checkDeprecatedMethods(sourceFile) {
360
+ const filePath = sourceFile.getFilePath();
361
+ sourceFile.forEachDescendant((node) => {
362
+ if (import_ts_morph.Node.isCallExpression(node)) {
363
+ const expression = node.getExpression();
364
+ if (import_ts_morph.Node.isPropertyAccessExpression(expression)) {
365
+ const name = expression.getName();
366
+ if (DEPRECATED_METHODS.has(name) && !AUTO_TRANSFORMABLE.has(name)) {
367
+ const lineNumber = node.getStartLineNumber();
368
+ switch (name) {
369
+ case "strict":
370
+ this.warnings.push(
371
+ `${filePath}:${lineNumber}: .strict() is deprecated in v4. Use z.strictObject() instead.`
372
+ );
373
+ break;
374
+ case "passthrough":
375
+ this.warnings.push(
376
+ `${filePath}:${lineNumber}: .passthrough() is deprecated in v4. Use z.looseObject() instead.`
377
+ );
378
+ break;
379
+ }
380
+ }
381
+ }
382
+ }
383
+ });
384
+ }
385
+ /**
386
+ * Auto-transform .superRefine() to .check() — callback signature is identical.
387
+ */
388
+ transformSuperRefine(sourceFile) {
389
+ const filePath = sourceFile.getFilePath();
390
+ const nodesToTransform = [];
391
+ sourceFile.forEachDescendant((node) => {
392
+ if (import_ts_morph.Node.isPropertyAccessExpression(node) && node.getName() === "superRefine") {
393
+ nodesToTransform.push({ node, lineNumber: node.getStartLineNumber() });
394
+ }
395
+ });
396
+ nodesToTransform.sort((a, b) => b.node.getStart() - a.node.getStart());
397
+ for (const { node, lineNumber } of nodesToTransform) {
398
+ const parent = node.getParent();
399
+ if (parent && import_ts_morph.Node.isCallExpression(parent)) {
400
+ const objectText = node.getExpression().getText();
401
+ const args = parent.getArguments();
402
+ let argsText = args.map((a) => a.getText()).join(", ");
403
+ if (args.length > 0) {
404
+ const callbackText = args[0]?.getText() ?? "";
405
+ const paramMatch = callbackText.match(
406
+ /^\s*\(?\s*\w+\s*,\s*(\w+)\s*\)?(?:\s*:\s*[^)]+)?\s*=>/
407
+ );
408
+ if (paramMatch?.[1]) {
409
+ const ctxName = paramMatch[1];
410
+ const addIssuePattern = new RegExp(`${ctxName}\\.addIssue\\(`, "g");
411
+ if (addIssuePattern.test(callbackText)) {
412
+ argsText = callbackText.replace(
413
+ new RegExp(`${ctxName}\\.addIssue\\(`, "g"),
414
+ `${ctxName}.issues.push(`
415
+ );
416
+ if (args.length > 1) {
417
+ const remaining = args.slice(1).map((a) => a.getText()).join(", ");
418
+ argsText = `${argsText}, ${remaining}`;
419
+ }
420
+ }
421
+ }
422
+ }
423
+ parent.replaceWithText(`${objectText}.check(${argsText})`);
424
+ this.warnings.push(
425
+ `${filePath}:${lineNumber}: .superRefine() auto-transformed to .check() (deprecated in v4).`
426
+ );
427
+ }
428
+ }
429
+ }
430
+ /**
431
+ * Auto-transform invalid_type_error/required_error params to unified `error` param.
432
+ */
433
+ transformErrorParams(sourceFile) {
434
+ const filePath = sourceFile.getFilePath();
435
+ const nodesToTransform = [];
436
+ sourceFile.forEachDescendant((node) => {
437
+ if (import_ts_morph.Node.isObjectLiteralExpression(node)) {
438
+ const properties = node.getProperties();
439
+ const propNames = properties.filter((p) => import_ts_morph.Node.isPropertyAssignment(p)).map((p) => import_ts_morph.Node.isPropertyAssignment(p) ? p.getName() : "");
440
+ const hasInvalidType = propNames.includes("invalid_type_error");
441
+ const hasRequired = propNames.includes("required_error");
442
+ if (hasInvalidType || hasRequired) {
443
+ const parent = node.getParent();
444
+ if (parent && import_ts_morph.Node.isCallExpression(parent)) {
445
+ const expr = parent.getExpression();
446
+ if (import_ts_morph.Node.isPropertyAccessExpression(expr) && expr.getExpression().getText() === "z") {
447
+ nodesToTransform.push({ node, lineNumber: node.getStartLineNumber() });
448
+ }
449
+ }
450
+ }
451
+ }
452
+ });
453
+ nodesToTransform.sort((a, b) => b.node.getStart() - a.node.getStart());
454
+ for (const { node, lineNumber } of nodesToTransform) {
455
+ const properties = node.getProperties();
456
+ let invalidTypeValue;
457
+ let requiredValue;
458
+ const otherProps = [];
459
+ for (const prop of properties) {
460
+ if (import_ts_morph.Node.isPropertyAssignment(prop)) {
461
+ const name = prop.getName();
462
+ const value = prop.getInitializer()?.getText() ?? "";
463
+ if (name === "invalid_type_error") {
464
+ invalidTypeValue = value;
465
+ } else if (name === "required_error") {
466
+ requiredValue = value;
467
+ } else {
468
+ otherProps.push(prop.getText());
469
+ }
470
+ }
471
+ }
472
+ let errorValue;
473
+ if (invalidTypeValue && requiredValue) {
474
+ errorValue = `error: (issue) => issue.input === undefined ? ${requiredValue} : ${invalidTypeValue}`;
475
+ } else if (requiredValue) {
476
+ errorValue = `error: ${requiredValue}`;
477
+ } else if (invalidTypeValue) {
478
+ errorValue = `error: ${invalidTypeValue}`;
479
+ } else {
480
+ continue;
481
+ }
482
+ const allProps = [errorValue, ...otherProps].join(", ");
483
+ node.replaceWithText(`{ ${allProps} }`);
484
+ this.warnings.push(
485
+ `${filePath}:${lineNumber}: invalid_type_error/required_error auto-transformed to unified error param.`
486
+ );
487
+ }
488
+ }
489
+ /**
490
+ * Detect instanceof Error patterns in catch blocks that reference ZodError.
491
+ * In v4, ZodError no longer extends Error.
492
+ */
493
+ checkInstanceofError(sourceFile) {
494
+ const filePath = sourceFile.getFilePath();
495
+ sourceFile.forEachDescendant((node) => {
496
+ if (import_ts_morph.Node.isCatchClause(node)) {
497
+ const block = node.getBlock();
498
+ const blockText = block.getText();
499
+ if (blockText.includes("ZodError") && blockText.includes("instanceof Error")) {
500
+ const lineNumber = node.getStartLineNumber();
501
+ this.warnings.push(
502
+ `${filePath}:${lineNumber}: ZodError no longer extends Error in v4. \`instanceof Error\` checks on ZodError will return false. Use \`instanceof ZodError\` or check for \`.issues\` property instead.`
503
+ );
504
+ }
505
+ }
506
+ });
507
+ }
508
+ /**
509
+ * Detect .refine()/.superRefine() followed by .transform() in the same chain.
510
+ * In v4, .transform() runs even when .refine() fails.
511
+ */
512
+ checkTransformRefineOrder(sourceFile) {
513
+ const filePath = sourceFile.getFilePath();
514
+ sourceFile.forEachDescendant((node) => {
515
+ if (import_ts_morph.Node.isCallExpression(node)) {
516
+ const expression = node.getExpression();
517
+ if (import_ts_morph.Node.isPropertyAccessExpression(expression) && expression.getName() === "transform") {
518
+ const chainText = node.getText();
519
+ if (chainText.includes(".refine(") || chainText.includes(".superRefine(") || chainText.includes(".check(")) {
520
+ const lineNumber = node.getStartLineNumber();
521
+ this.warnings.push(
522
+ `${filePath}:${lineNumber}: In v4, .transform() executes even if preceding .refine() fails. Consider using .pipe() to sequence validation before transforms.`
523
+ );
524
+ }
525
+ }
526
+ }
527
+ });
528
+ }
529
+ /**
530
+ * Detect .default() combined with .optional() — behavior changed silently in v4.
531
+ * In v4, .default() always provides a value, making .optional() a no-op.
532
+ */
533
+ checkDefaultOptional(sourceFile) {
534
+ const filePath = sourceFile.getFilePath();
535
+ sourceFile.forEachDescendant((node) => {
536
+ if (import_ts_morph.Node.isCallExpression(node)) {
537
+ const expression = node.getExpression();
538
+ if (import_ts_morph.Node.isPropertyAccessExpression(expression) && (expression.getName() === "optional" || expression.getName() === "default")) {
539
+ const chainText = node.getText();
540
+ const hasDefault = chainText.includes(".default(");
541
+ const hasOptional = chainText.includes(".optional(");
542
+ if (hasDefault && hasOptional) {
543
+ const lineNumber = node.getStartLineNumber();
544
+ this.warnings.push(
545
+ `${filePath}:${lineNumber}: .default() + .optional() behavior changed in v4. .default() now always provides a value, making .optional() effectively a no-op. Review whether .optional() is still needed.`
131
546
  );
132
547
  }
133
548
  }
@@ -167,20 +582,55 @@ var ZodV3ToV4Transformer = class {
167
582
  `${filePath}:${lineNumber}: z.function() parameter handling has changed. Review input/output schema definitions.`
168
583
  );
169
584
  break;
585
+ case "pipe":
586
+ this.warnings.push(
587
+ `${filePath}:${lineNumber}: .pipe() has stricter type checking in v4. If you get type errors, add explicit type annotations to .transform() return types or cast through unknown as a last resort.`
588
+ );
589
+ break;
170
590
  }
171
591
  }
172
592
  }
173
593
  }
174
594
  });
175
- const text = sourceFile.getFullText();
176
- if (text.includes(".flatten()")) {
177
- const matches = text.matchAll(/\.flatten\(\)/g);
178
- for (const match of matches) {
179
- const pos = match.index ?? 0;
180
- const lineNumber = sourceFile.getLineAndColumnAtPos(pos).line;
181
- this.warnings.push(
182
- `${filePath}:${lineNumber}: .flatten() is removed in v4. Use error.issues directly or implement custom flattening.`
183
- );
595
+ }
596
+ /**
597
+ * Detect .catch() combined with .optional() — behavior changed in v4.
598
+ * In v4, .catch() on optional properties always returns the catch value.
599
+ */
600
+ checkCatchOptional(sourceFile) {
601
+ const filePath = sourceFile.getFilePath();
602
+ sourceFile.forEachDescendant((node) => {
603
+ if (import_ts_morph.Node.isCallExpression(node)) {
604
+ const expression = node.getExpression();
605
+ if (import_ts_morph.Node.isPropertyAccessExpression(expression) && (expression.getName() === "optional" || expression.getName() === "catch")) {
606
+ const chainText = node.getText();
607
+ if (chainText.includes(".catch(") && chainText.includes(".optional(")) {
608
+ const lineNumber = node.getStartLineNumber();
609
+ this.warnings.push(
610
+ `${filePath}:${lineNumber}: .catch() + .optional() behavior changed in v4. .catch() on optional properties now always returns the catch value, even when the property is absent from input.`
611
+ );
612
+ }
613
+ }
614
+ }
615
+ });
616
+ }
617
+ /**
618
+ * Detect imports of utility types that moved to 'zod/v4/core' in v4.
619
+ */
620
+ checkUtilityTypeImports(sourceFile) {
621
+ const filePath = sourceFile.getFilePath();
622
+ for (const importDecl of sourceFile.getImportDeclarations()) {
623
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
624
+ if (moduleSpecifier !== "zod") continue;
625
+ const namedImports = importDecl.getNamedImports();
626
+ for (const namedImport of namedImports) {
627
+ const name = namedImport.getName();
628
+ if (UTILITY_TYPES.has(name)) {
629
+ const lineNumber = importDecl.getStartLineNumber();
630
+ this.warnings.push(
631
+ `${filePath}:${lineNumber}: '${name}' may need to be imported from 'zod/v4/core' in v4. Internal utility types have moved to a separate subpackage.`
632
+ );
633
+ }
184
634
  }
185
635
  }
186
636
  }