diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index d64237846..4e8812437 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -5893,6 +5893,18 @@ function emptyNeverArrayProp(p: ts.Symbol, fieldTs: ts.Type, checker: ts.TypeChe ); } +/** A JavaScript object literal's property typed `never[]` (`{ issues: [] }`). + * Nothing checks a JavaScript file's later writes, which can push any value + * onto it (`ctx.common.issues.push(issue)`), so it holds the checked-dynamic + * array rather than never's f64 element array. */ +function jsNeverArrayProp(p: ts.Symbol, fieldTs: ts.Type, checker: ts.TypeChecker): boolean { + return ( + jsObjectLiteralProperty(p) && + checker.isArrayType(fieldTs) && + (checker.getTypeArguments(fieldTs as ts.TypeReference)[0]!.flags & ts.TypeFlags.Never) !== 0 + ); +} + /** A JavaScript object literal's property (`{ lastComponent: undefined }`, * `{ user: null }`). Nothing checks a JavaScript file's later writes, which * can store any value in it (`path.lastComponent = { ... }`), so a @@ -5904,6 +5916,12 @@ function isUnitOnlyIr(t: IrType, ctx: TypeMapperCtx): boolean { return def !== undefined && def.arms.every((arm) => isUnitType(arm)); } +/** A property a JavaScript object literal declares. The checker also adds + * each missing property, optional and `undefined`, to every literal of a + * union of literals (`{ _tag, value } | { _tag, error }` gains + * `error?: undefined` and `value?: undefined`); those keep their unit-only + * type, so a literal still converts to the one arm whose properties it + * has. */ function jsObjectLiteralProperty(p: ts.Symbol): boolean { const d = p.valueDeclaration; // `__proto__: null` sets the literal's prototype; it declares no property. @@ -5911,6 +5929,7 @@ function jsObjectLiteralProperty(p: ts.Symbol): boolean { d !== undefined && (ts.isPropertyAssignment(d) || ts.isShorthandPropertyAssignment(d)) && ts.symbolName(p) !== "__proto__" && + (p.flags & ts.SymbolFlags.Optional) === 0 && isJsSourceFile(ts.getSourceFileOfNode(d)) ); } @@ -7983,6 +8002,7 @@ function mapRecordTypeInner( if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)) pt = unitOnlyUnion(ctx.unions); if (pt !== null && isUnitOnlyIr(pt, ctx) && jsObjectLiteralProperty(p)) pt = DYN; + if (pt?.kind === "array" && jsNeverArrayProp(p, fieldTs, checker)) pt = DYN; // dyn FIELDS map now (`{ v: unknown }`, `[string, unknown]` tuples): // the slot carries a dyn value exactly like an `unknown`-valued // overflow entry — same RC adapters, same dynFrom conversion on the diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7014-js-never-array-literal-fields/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7014-js-never-array-literal-fields/main.ts.json new file mode 100644 index 000000000..a67c77476 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7014-js-never-array-literal-fields/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7014-js-never-array-literal-fields/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7014-js-never-array-literal-fields/main.ts b/tests/corpus/7014-js-never-array-literal-fields/main.ts new file mode 100644 index 000000000..cf6d235c0 --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/main.ts @@ -0,0 +1,10 @@ +// A JavaScript package whose object literals start an array property as +// `[]` inside a literal that maps to a record, as zod 3's parse context does +// (`{ common: { issues: [] }, path: params?.path || [] }`), and later push +// records onto it. Node runs the pushes; each array holds them. +import { Schema, rows } from "mini"; + +const s = new Schema(); +console.log(JSON.stringify(s.safeParse(1)), JSON.stringify(s.safeParse("a"))); +console.log(JSON.stringify(s.safeParse(2, { path: ["root"] }))); +console.log(rows()); diff --git a/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.d.ts b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.d.ts new file mode 100644 index 000000000..5925684c0 --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.d.ts @@ -0,0 +1,5 @@ +export interface Issue { code: string; path: (string | number)[]; message: string } +export declare class Schema { + safeParse(data: unknown, params?: { async?: boolean; path?: (string | number)[] }): { success: true; data: string } | { success: false; issues: Issue[] }; +} +export declare function rows(): string; diff --git a/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.js b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.js new file mode 100644 index 000000000..47d24c8ad --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/index.js @@ -0,0 +1,31 @@ +// A parse context as zod 3 keeps it: the issue list starts as `[]` in a +// nested literal, and the path is the caller's array or `[]`. +export function makeIssue(params) { + const { path, issueData } = params; + const fullPath = [...path, ...(issueData.path || [])]; + return { ...issueData, path: fullPath, message: "m" }; +} +export function addIssue(ctx, issueData) { + const issue = makeIssue({ issueData, path: ctx.path }); + ctx.common.issues.push(issue); +} +export class Schema { + safeParse(data, params) { + const ctx = { + common: { issues: [], async: params?.async ?? false }, + path: params?.path || [], + data, + }; + if (typeof data !== "string") addIssue(ctx, { code: "invalid_type" }); + return ctx.common.issues.length === 0 ? { success: true, data } : { success: false, issues: ctx.common.issues }; + } +} +// Array properties of records in an array literal and in a deeper literal, +// filled after construction. +export function rows() { + const table = { rows: [{ cells: [] }, { cells: [] }], meta: { tags: { list: [] } }, size: 2 }; + table.rows[0].cells.push({ v: 1 }); + table.rows[1].cells.push("text", 3); + table.meta.tags.list.push({ name: "t" }, null); + return JSON.stringify(table); +} diff --git a/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/package.json b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/package.json new file mode 100644 index 000000000..be8bc49a2 --- /dev/null +++ b/tests/corpus/7014-js-never-array-literal-fields/node_modules/mini/package.json @@ -0,0 +1 @@ +{ "name": "mini", "version": "1.0.0", "type": "module", "main": "./index.js", "types": "./index.d.ts" } diff --git a/tests/harness/corpus-costs/7014-js-never-array-literal-fields/main.ts.json b/tests/harness/corpus-costs/7014-js-never-array-literal-fields/main.ts.json new file mode 100644 index 000000000..c5bf12c38 --- /dev/null +++ b/tests/harness/corpus-costs/7014-js-never-array-literal-fields/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3004, "san": 3359 }