Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/zod-deprecated-sites.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"uptide": patch
---

The zod pack reports `.refine(check, { message })`, the type names zod 4 marks deprecated (`ZodTypeAny`, `ZodIssue`, `ZodSchema`, `ZodErrorMap` and the others, including `import('zod').ZodTypeAny`) and `errorMap` options with `ctx.defaultError`, as deprecated findings.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ See the [pack compatibility matrix](docs/compatibility.md) and the [migration pa
| `stripe` | 14+ → latest, API version pinning | 5 | 100% | 92% | 21 | unkeyed/unkey, nextjs/saas-starter, starl3xx/wallet-to-social, AtlasDevHQ/atlas | verified |
| `typescript` | 5 → 6 | 12 | 100% | 99% | 1399 | m-lyon/recipe, robrechtme/recipes, JosephMaynard/dependency-radar, CroffZ/github-release-notifier, SignalK/signalk-derived-data, shogo82148/typescript-action, Borewit/music-metadata | verified |
| `vitest` | 4 → 5 | 6 | 100% | 97% | 32 | votingworks/vxsuite, vitorvasc/opentelemetry-ecosystem-explorer | verified |
| `zod` | 3 → 4 | 8 | 100% | 65% | 219 | ugurkocde/AwesomeIntune, howardyang2009/PATH, band-ai/band-sdk-typescript, vzdc-artcc/website, CANShift/canshift-core | verified |
| `zod` | 3 → 4 | 11 | 100% | 76% | 219 | ugurkocde/AwesomeIntune, howardyang2009/PATH, band-ai/band-sdk-typescript, vzdc-artcc/website, CANShift/canshift-core | verified |

4 more in validation: see the [compatibility matrix](docs/compatibility.md).
<!-- packs:end -->
Expand Down
2 changes: 1 addition & 1 deletion docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ already run as assisted ([verification gate](packs.md)).
| `stripe` | 14+ → latest, API version pinning | 5 | 100% | 92% | 21 | unkeyed/unkey, nextjs/saas-starter, starl3xx/wallet-to-social, AtlasDevHQ/atlas | verified |
| `typescript` | 5 → 6 | 12 | 100% | 99% | 1399 | m-lyon/recipe, robrechtme/recipes, JosephMaynard/dependency-radar, CroffZ/github-release-notifier, SignalK/signalk-derived-data, shogo82148/typescript-action, Borewit/music-metadata | verified |
| `vitest` | 4 → 5 | 6 | 100% | 97% | 32 | votingworks/vxsuite, vitorvasc/opentelemetry-ecosystem-explorer | verified |
| `zod` | 3 → 4 | 8 | 100% | 65% | 219 | ugurkocde/AwesomeIntune, howardyang2009/PATH, band-ai/band-sdk-typescript, vzdc-artcc/website, CANShift/canshift-core | verified |
| `zod` | 3 → 4 | 11 | 100% | 76% | 219 | ugurkocde/AwesomeIntune, howardyang2009/PATH, band-ai/band-sdk-typescript, vzdc-artcc/website, CANShift/canshift-core | verified |

| Package | Rewrite rule | Precision | Real true positives | False positives | Note |
| --- | --- | ---: | ---: | ---: | --- |
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/packs/zod/RESEARCH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# zod pack: what the rules do not reach (UPT-92)

Misses left in the ground truth after the `refine-message`, `deprecated-names` and `error-map`
rules (breaking recall 65% → 76%, 17 misses):

- **Compiler errors with no zod API at the line (10).** `post-runs.ts:55,82` and
`resume-run.ts:52,127` (PATH) pass a value to a function of the repository's own whose
parameter type is inferred from a schema defined in another file; `mcp/sdk.ts:359`,
`server.ts:311`, `sse.ts:239`, `stdio.ts:93` (band-sdk) pass a shape to the MCP SDK's
`tool()`. The line names no zod symbol; the cause is a schema type elsewhere, and the
diagnostic (TS2345, TS2322) is the only evidence. Attributing it needs the checker's type of
the argument traced back to a zod type, which is the type diff's job, not a syntax rule's.
`open-workflow.ts:149` is the same: `z.unknown()` is fine, the error is the declared type
of the map it is assigned into, two lines up. Left as they are.
- **`name === "ZodArray"` (wire-step-plugins.ts).** Four lines the upgrade changed compare
zod 3's `_def.typeName` strings. A rule for it found those and one more at line 90, a
comparison the upgrade left alone, so it had a breaking false positive on this truth and
was dropped rather than shipped as `deprecated`.
- `LessonForm.tsx:22` (`error.errors` on a binding the scan cannot type) and
`validate-dashboard.ts:21` (a line the upgrade touched with no zod symbol) are `error-issues`
and `generic` misses outside these rules.
- `prompt/index.ts:41` is a single-argument `z.record` (`record-key`); not investigated here.
14 changes: 14 additions & 0 deletions packages/core/src/packs/zod/fixtures/deprecated-names/after.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z, type ZodRawShape, type ZodTypeAny } from 'zod'; // @uptide deprecated-names at:ZodTypeAny

// Shape of packages/sdk/src/mcp/zod.ts in band-ai/band-sdk-typescript.
export function toShape(): Record<string, import('zod').ZodTypeAny> { // @uptide deprecated-names at:import('zod')
const shape: Record<string, ZodTypeAny> = {}; // @uptide deprecated-names at:ZodTypeAny
return shape;
}
export const issue = (i: z.ZodIssue) => i.message; // @uptide deprecated-names at:z.ZodIssue
export type Raw = ZodRawShape; // @uptide deprecated-names keep at:ZodRawShape

// Left alone: a local type with the name, and the replacement.
type ZodSchema = { local: true };
export const local: ZodSchema = { local: true }; // @uptide deprecated-names keep at:ZodSchema
export const ok = (t: z.ZodType) => t; // @uptide deprecated-names keep at:z.ZodType
14 changes: 14 additions & 0 deletions packages/core/src/packs/zod/fixtures/deprecated-names/before.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z, type ZodRawShape, type ZodTypeAny } from 'zod'; // @uptide deprecated-names at:ZodTypeAny

// Shape of packages/sdk/src/mcp/zod.ts in band-ai/band-sdk-typescript.
export function toShape(): Record<string, import('zod').ZodTypeAny> { // @uptide deprecated-names at:import('zod')
const shape: Record<string, ZodTypeAny> = {}; // @uptide deprecated-names at:ZodTypeAny
return shape;
}
export const issue = (i: z.ZodIssue) => i.message; // @uptide deprecated-names at:z.ZodIssue
export type Raw = ZodRawShape; // @uptide deprecated-names keep at:ZodRawShape

// Left alone: a local type with the name, and the replacement.
type ZodSchema = { local: true };
export const local: ZodSchema = { local: true }; // @uptide deprecated-names keep at:ZodSchema
export const ok = (t: z.ZodType) => t; // @uptide deprecated-names keep at:z.ZodType
12 changes: 12 additions & 0 deletions packages/core/src/packs/zod/fixtures/error-map/after.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { z } from 'zod';

// Shape of packages/schema/src/nodes.ts in howardyang2009/PATH.
const unknownStep = (_issue: unknown, ctx: { defaultError: string }) => ({ message: ctx.defaultError }); // @uptide error-map at:defaultError
export const node = z.union(
[z.string(), z.number()],
{ errorMap: unknownStep }, // @uptide error-map at:errorMap
);

// Left alone: the zod 4 option, and an errorMap property that is no call's option.
export const next = z.string({ error: () => 'no' });
export const registry = { errorMap: unknownStep }; // @uptide error-map keep at:errorMap
12 changes: 12 additions & 0 deletions packages/core/src/packs/zod/fixtures/error-map/before.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { z } from 'zod';

// Shape of packages/schema/src/nodes.ts in howardyang2009/PATH.
const unknownStep = (_issue: unknown, ctx: { defaultError: string }) => ({ message: ctx.defaultError }); // @uptide error-map at:defaultError
export const node = z.union(
[z.string(), z.number()],
{ errorMap: unknownStep }, // @uptide error-map at:errorMap
);

// Left alone: the zod 4 option, and an errorMap property that is no call's option.
export const next = z.string({ error: () => 'no' });
export const registry = { errorMap: unknownStep }; // @uptide error-map keep at:errorMap
5 changes: 4 additions & 1 deletion packages/core/src/packs/zod/fixtures/error-params/after.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const label = (field: string) => `${field} is required`;
export const city = z.string((<T extends { required_error?: string; invalid_type_error?: string }>(options: T) => { const { required_error, invalid_type_error, ...rest } = options; return { ...rest, error: (iss: { input: unknown }) => iss.input === undefined ? required_error : invalid_type_error }; })({ required_error: label('City') })); // @uptide error-params at:z.string

// Left for a person: an existing errorMap must be merged by hand, a spread may hide the keys.
export const merged = z.string({ required_error: 'x', errorMap: () => ({ message: 'y' }) }); // @uptide error-params keep at:z.string
export const merged = z.string({ // @uptide error-params keep at:z.string
required_error: 'x',
errorMap: () => ({ message: 'y' }), // @uptide error-map at:errorMap
});
const base = { description: 'shared' };
export const spread = z.string({ ...base, required_error: 'x' }); // @uptide error-params keep at:z.string
5 changes: 4 additions & 1 deletion packages/core/src/packs/zod/fixtures/error-params/before.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const label = (field: string) => `${field} is required`;
export const city = z.string({ required_error: label('City') }); // @uptide error-params at:z.string

// Left for a person: an existing errorMap must be merged by hand, a spread may hide the keys.
export const merged = z.string({ required_error: 'x', errorMap: () => ({ message: 'y' }) }); // @uptide error-params keep at:z.string
export const merged = z.string({ // @uptide error-params keep at:z.string
required_error: 'x',
errorMap: () => ({ message: 'y' }), // @uptide error-map at:errorMap
});
const base = { description: 'shared' };
export const spread = z.string({ ...base, required_error: 'x' }); // @uptide error-params keep at:z.string
15 changes: 15 additions & 0 deletions packages/core/src/packs/zod/fixtures/refine-message/after.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { z } from 'zod';

// Shape of actions/event.ts in vzdc-artcc/website: a refinement with a message option.
export const event = z
.object({ start: z.date(), end: z.date() })
.refine((e) => e.end > e.start, {
message: 'End must follow start.', // @uptide refine-message at:message
path: ['end'],
});
export const loa = z.object({ n: z.number() }).refine((v) => v.n > 0, { message: 'Positive.' }); // @uptide refine-message at:message

// Left alone: a string, the new option, and a refine that is not zod's.
export const a = z.string().refine((v) => v.length > 0, 'Required'); // @uptide refine-message keep at:refine
export const b = z.string().refine((v) => v.length > 0, { error: 'Required' }); // @uptide refine-message keep at:refine
export const c = z.string().refine((v) => v.length > 0); // @uptide refine-message keep at:refine
15 changes: 15 additions & 0 deletions packages/core/src/packs/zod/fixtures/refine-message/before.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { z } from 'zod';

// Shape of actions/event.ts in vzdc-artcc/website: a refinement with a message option.
export const event = z
.object({ start: z.date(), end: z.date() })
.refine((e) => e.end > e.start, {
message: 'End must follow start.', // @uptide refine-message at:message
path: ['end'],
});
export const loa = z.object({ n: z.number() }).refine((v) => v.n > 0, { message: 'Positive.' }); // @uptide refine-message at:message

// Left alone: a string, the new option, and a refine that is not zod's.
export const a = z.string().refine((v) => v.length > 0, 'Required'); // @uptide refine-message keep at:refine
export const b = z.string().refine((v) => v.length > 0, { error: 'Required' }); // @uptide refine-message keep at:refine
export const c = z.string().refine((v) => v.length > 0); // @uptide refine-message keep at:refine
4 changes: 2 additions & 2 deletions packages/core/src/packs/zod/fixtures/removed-exports/after.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { z, type SafeParseReturnType, type ZodTypeDef } from 'zod'; // @uptide removed-exports at:SafeParseReturnType
import { ZodTypeAny } from 'zod';
import { ZodType } from 'zod';

// Shape of actions/event.ts in vzdc-artcc/website: a result typed with a zod 3 root export.
export const parse = (input: unknown): SafeParseReturnType<unknown, string> => // @uptide removed-exports at:SafeParseReturnType
z.string().safeParse(input) as SafeParseReturnType<unknown, string>; // @uptide removed-exports at:SafeParseReturnType

export type Def = z.ZodTypeDef; // @uptide removed-exports at:z.ZodTypeDef
export type Shape = Record<string, import('zod').ZodEffects<z.ZodString>>; // @uptide removed-exports at:import('zod')
export const schema: ZodTypeAny = z.string();
export const schema: ZodType = z.string();

// Left alone: a local type that shares the name, and a member that is not the export.
type SafeParseSuccess = { success: true };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { z, type SafeParseReturnType, type ZodTypeDef } from 'zod'; // @uptide removed-exports at:SafeParseReturnType
import { ZodTypeAny } from 'zod';
import { ZodType } from 'zod';

// Shape of actions/event.ts in vzdc-artcc/website: a result typed with a zod 3 root export.
export const parse = (input: unknown): SafeParseReturnType<unknown, string> => // @uptide removed-exports at:SafeParseReturnType
z.string().safeParse(input) as SafeParseReturnType<unknown, string>; // @uptide removed-exports at:SafeParseReturnType

export type Def = z.ZodTypeDef; // @uptide removed-exports at:z.ZodTypeDef
export type Shape = Record<string, import('zod').ZodEffects<z.ZodString>>; // @uptide removed-exports at:import('zod')
export const schema: ZodTypeAny = z.string();
export const schema: ZodType = z.string();

// Left alone: a local type that shares the name, and a member that is not the export.
type SafeParseSuccess = { success: true };
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/packs/zod/guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export const zodGuide = {
'ZodError.formErrors and ZodError.errors were removed in Zod 4 and read undefined. formErrors was identical to flatten(): use z.flattenError(error), which returns the same { formErrors, fieldErrors } object (flatten() is deprecated). errors was an alias of issues: use error.issues. Keep the returned shape and the control flow; do not cast to hide the change.',
removedExports:
"Zod 4's root no longer exports the names zod 3 kept for its internals: SafeParseReturnType, SafeParseSuccess and SafeParseError (use ZodSafeParseResult, which takes only the output type), ZodTypeDef and the Zod*Def interfaces (gone), ZodEffects (a refinement or transform is now the schema itself, a ZodPipe for transform), ZodNativeEnum (use ZodEnum), and the parse and issue helpers. They are still exported from 'zod/v3', which keeps zod 3's runtime and types, not the schemas you build with 'zod'. Replace the name with its zod 4 equivalent; do not import it from 'zod/v3' while the schemas come from 'zod'.",
refineMessage:
'refine(check, { message }) still works in Zod 4 but message is deprecated: pass { error: message } (or a string as the second argument). path and params are unchanged.',
deprecatedNames:
'ZodTypeAny, ZodSchema, ZodIssue, ZodIssueCode, ZodErrorMap, ZodFlattenedError and ZodFormattedError are deprecated aliases. Use z.ZodType, z.core.$ZodIssue, the raw issue code strings, z.core.$ZodErrorMap, z.core.$ZodFlattenedError and z.core.$ZodFormattedError.',
errorMap:
'errorMap was removed from schema params and refinements: pass error, a function that receives the issue and returns a message or undefined to use the default. There is no ctx: ctx.defaultError has no equivalent, return undefined to fall through to the default message.',
generics:
'ZodType now takes Output and Input, defaulting to unknown. The definition type parameter is gone. Remove reliance on ZodTypeDef without weakening the input/output contract. SafeParse success contains data; failure contains error. Narrow on success before accessing either. Preserve validation and type safety; do not cast to any to hide diagnostics.',
};
38 changes: 37 additions & 1 deletion packages/core/src/packs/zod/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ import {
} from './error-members.js';
import { zodGuide } from './guide.js';
import { defaultMessageSites } from './messages.js';
import { removedExportFindings, removedExportSites } from './removed-exports.js';
import { removedExportFindings, removedExportSites, siteFindings } from './removed-exports.js';
import {
deprecatedNameSites,
errorMapSites,
refineMessageSites,
siteRules,
} from './source-rules.js';

const params: Record<string, number> = {
string: 0,
Expand Down Expand Up @@ -399,6 +405,33 @@ export const zodPack: Pack = {
guide: zodGuide.removedExports,
detect: (text, file) => removedExportSites(text, file),
},
{
id: 'refine-message',
summary: 'refine(check, { message }): message is deprecated, use error',
severity: 'deprecated',
kinds: [],
symbols: /$^/,
guide: zodGuide.refineMessage,
detect: (text, file) => refineMessageSites(text, file),
},
{
id: 'deprecated-names',
summary: 'ZodTypeAny, ZodSchema, ZodIssue and the other aliases zod 4 marks deprecated',
severity: 'deprecated',
kinds: [],
symbols: /$^/,
guide: zodGuide.deprecatedNames,
detect: (text, file) => deprecatedNameSites(text, file),
},
{
id: 'error-map',
summary: 'errorMap options and ctx.defaultError: use error',
severity: 'deprecated',
kinds: [],
symbols: /$^/,
guide: zodGuide.errorMap,
detect: (text, file) => errorMapSites(text, file),
},
{
id: 'types',
summary: 'ZodType generics and ZodTypeDef changed; type-level code is migrated by the agent',
Expand Down Expand Up @@ -439,6 +472,9 @@ export const zodPack: Pack = {
rewrite: rewriteErrorMembers,
}),
...removedExportFindings({ root, workspace, from, to, usages, read }),
...siteRules.flatMap((spec) =>
siteFindings({ root, workspace, from, to, usages, read }, spec),
),
];
const key = (f: Finding) => `${f.rule}:${f.usage.file}:${f.usage.line}`;
const seen = new Set(own.map(key));
Expand Down
Loading
Loading