mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Map JavaScript [] literal fields dynamically and keep union-added fields unit-only
Two JavaScript object literal mappings from #1799 broke packages. A property initialized to `[]` has the checker type `never[]`, and never's element maps to f64. A literal with such a property builds as a checked-dynamic object when it lowers on its own. A literal nested in one that maps to a record (`{ common: { issues: [] }, path: params?.path || [] }`, zod 3's parse context) lowers against its record type, so the property was a `number[]`. `ctx.common.issues.push(issue)` then threw "TypeError: expected number at $, got object", where Node pushes the record. Such a property now maps to the checked-dynamic array, as a property initialized to `undefined` or `null` maps to the checked-dynamic value. That `undefined`/`null` rule also matched the properties the checker adds when it normalizes a union of literals: `{ _tag, value } | { _tag, error }` gains an optional `error?: undefined` and `value?: undefined`. Mapping those to the checked-dynamic value let `{ _tag, value }` fit both arms, and Effect 4's CLI parser (`consumeFlagValue`) failed with SC2003 "union conversion has no unambiguous layout mapping". The rule now skips optional properties, so they keep their unit-only type and each literal converts to its own arm.
This commit is contained in:
@@ -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
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7014-js-never-array-literal-fields/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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());
|
||||
+5
@@ -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;
|
||||
+31
@@ -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);
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "mini", "version": "1.0.0", "type": "module", "main": "./index.js", "types": "./index.d.ts" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3004, "san": 3359 }
|
||||
Reference in New Issue
Block a user