mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Throw Node's TypeError when a nullable destructuring source is null (#2537)
This commit is contained in:
@@ -2549,13 +2549,26 @@ export function lowerDestructuringDecl(
|
||||
}
|
||||
}
|
||||
const out: IrStmt[] = [];
|
||||
// V8's destructuring TypeError spells STATIC sources: an identifier
|
||||
// source reads "<name> is not iterable", an identifier-callee call
|
||||
// "<name> is not a function or its return value is not iterable";
|
||||
// everything else (property reads, method calls, parameters) gets the
|
||||
// runtime kind wording — exactly V8's CallPrinter behavior. The DYN
|
||||
// pattern arm and the unit arms of a narrowed union consume this.
|
||||
const src = decl.initializer!;
|
||||
const dynSpell = ts.isIdentifier(src)
|
||||
? `${src.text} is not iterable`
|
||||
: ts.isCallExpression(src) && ts.isIdentifier(src.expression)
|
||||
? `${src.expression.text} is not a function or its return value is not iterable`
|
||||
: undefined;
|
||||
// JS: a union source with exactly ONE destructurable arm beside unit
|
||||
// arms (`nameAndArgs.match(re)` — `RegExpMatchArray | null`, the
|
||||
// always-matches idiom) destructures through a VALIDATED narrow: the
|
||||
// unit arms throw at runtime exactly where Node's TypeError would
|
||||
// (the message is the fence's — a unit value here is the failure
|
||||
// path either way). TypeScript keeps the fence: narrowing first is
|
||||
// the annotated fix.
|
||||
// unit arms throw Node's TypeError, through the checks a dyn source
|
||||
// takes (RequireObjectCoercible for an object pattern, GetIterator for
|
||||
// an array pattern). An object pattern whose source has no V8
|
||||
// spelling keeps the fence. TypeScript keeps the fence: narrowing
|
||||
// first is the annotated fix.
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && init.type.kind === "union") {
|
||||
const def = lowerer.unions.get(init.type.unionId);
|
||||
const dataArms = (def?.arms ?? []).filter((a) => !isUnitType(a));
|
||||
@@ -2568,9 +2581,61 @@ export function lowerDestructuringDecl(
|
||||
(arm.kind === "array" || arm.kind === "record")
|
||||
) {
|
||||
const tag = def.arms.indexOf(arm);
|
||||
const unionId = init.type.unionId;
|
||||
const utmp = lowerer.declareHiddenLocal("%destrU", init.type);
|
||||
out.push({ kind: "varDecl", localId: utmp.id, init, loc });
|
||||
const uref: IrExpr = { kind: "varRef", localId: utmp.id, type: init.type, loc };
|
||||
// The failure branch holds one of the unit arms: the check reads it
|
||||
// as the dyn null or undefined it is, so the data arm needs no dyn
|
||||
// form (a record holding a generator has none).
|
||||
const unitValue = (): IrExpr => {
|
||||
const unitOf = (t: IrType): IrExpr =>
|
||||
lowerer.coerceToExpected(
|
||||
{ kind: "unitLit", unit: t.kind === "nullT" ? "null" : "undefined", type: t, loc },
|
||||
DYN,
|
||||
);
|
||||
const units = def.arms.filter(isUnitType);
|
||||
const last = units[units.length - 1]!;
|
||||
return units.slice(0, -1).reduceRight<IrExpr>(
|
||||
(rest, t) => ({
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId,
|
||||
tag: def.arms.indexOf(t),
|
||||
negated: false,
|
||||
value: uref,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: unitOf(t),
|
||||
else_: rest,
|
||||
type: DYN,
|
||||
loc,
|
||||
}),
|
||||
unitOf(last),
|
||||
);
|
||||
};
|
||||
const spelling = destrSpellingOf(src);
|
||||
const check: IrExpr | null = ts.isArrayBindingPattern(decl.name)
|
||||
? {
|
||||
kind: "dynIterN",
|
||||
value: unitValue(),
|
||||
count: decl.name.elements.nodes.length,
|
||||
...(dynSpell ? { notIterableMessage: dynSpell } : {}),
|
||||
type: DYN,
|
||||
loc,
|
||||
}
|
||||
: spelling !== null
|
||||
? {
|
||||
kind: "dynDestrCheck",
|
||||
value: unitValue(),
|
||||
spelling,
|
||||
...firstPatternProp(lowerer, decl.name as ts.ObjectBindingPattern),
|
||||
type: DYN,
|
||||
loc,
|
||||
}
|
||||
: null;
|
||||
out.push({
|
||||
kind: "if",
|
||||
cond: {
|
||||
@@ -2583,6 +2648,7 @@ export function lowerDestructuringDecl(
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
...(check !== null ? [{ kind: "exprStmt" as const, expr: check, loc }] : []),
|
||||
{
|
||||
kind: "runtimeFence",
|
||||
code: "SC1031",
|
||||
@@ -2635,42 +2701,19 @@ export function lowerDestructuringDecl(
|
||||
"destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call)",
|
||||
);
|
||||
}
|
||||
const first = (decl.name as ts.ObjectBindingPattern).elements.nodes[0];
|
||||
const firstKey =
|
||||
first === undefined || first.dotDotDotToken !== undefined
|
||||
? undefined
|
||||
: (first.propertyName ?? first.name);
|
||||
// V8 names the first property when its key is an identifier, a string
|
||||
// or a number; a computed first key takes the bare (source-only) form.
|
||||
const firstProp =
|
||||
firstKey === undefined || ts.isComputedPropertyName(firstKey)
|
||||
? undefined
|
||||
: (patternKeyNameOf(lowerer, firstKey as ts.PropertyName) ?? undefined);
|
||||
out.push({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "dynDestrCheck",
|
||||
value: { kind: "varRef", localId: tmp.id, type: init.type, loc },
|
||||
spelling,
|
||||
...(firstProp !== undefined ? { firstProp } : {}),
|
||||
...firstPatternProp(lowerer, decl.name as ts.ObjectBindingPattern),
|
||||
type: init.type,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
}
|
||||
// V8's destructuring TypeError spells STATIC sources: an identifier
|
||||
// source reads "<name> is not iterable", an identifier-callee call
|
||||
// "<name> is not a function or its return value is not iterable";
|
||||
// everything else (property reads, method calls, parameters) gets the
|
||||
// runtime kind wording — exactly V8's CallPrinter behavior. Only the
|
||||
// DYN pattern arm consumes this.
|
||||
const src = decl.initializer!;
|
||||
const dynSpell = ts.isIdentifier(src)
|
||||
? `${src.text} is not iterable`
|
||||
: ts.isCallExpression(src) && ts.isIdentifier(src.expression)
|
||||
? `${src.expression.text} is not a function or its return value is not iterable`
|
||||
: undefined;
|
||||
lowerer.lowerBindingPattern(
|
||||
decl.name as ts.ArrayBindingPattern | ts.ObjectBindingPattern,
|
||||
() => ({ kind: "varRef", localId: tmp.id, type: init.type, loc }),
|
||||
@@ -9589,6 +9632,26 @@ export function lowerDestructuringAssign(
|
||||
return { kind: "block", body: parts.stmts, loc };
|
||||
}
|
||||
|
||||
/** The first property V8 names in an object pattern's destructuring
|
||||
* TypeError ("Cannot destructure property 'a' of ..."): an identifier,
|
||||
* string or numeric first key. A computed first key, a rest element or
|
||||
* an empty pattern takes the bare (source-only) form. */
|
||||
function firstPatternProp(
|
||||
lowerer: Lowerer,
|
||||
pattern: ts.ObjectBindingPattern,
|
||||
): { firstProp?: string } {
|
||||
const first = pattern.elements.nodes[0];
|
||||
const firstKey =
|
||||
first === undefined || first.dotDotDotToken !== undefined
|
||||
? undefined
|
||||
: (first.propertyName ?? first.name);
|
||||
const firstProp =
|
||||
firstKey === undefined || ts.isComputedPropertyName(firstKey)
|
||||
? undefined
|
||||
: (patternKeyNameOf(lowerer, firstKey as ts.PropertyName) ?? undefined);
|
||||
return firstProp !== undefined ? { firstProp } : {};
|
||||
}
|
||||
|
||||
/** V8's spelling of a destructuring RHS in its TypeError message
|
||||
* ("Cannot destructure 'a' as it is undefined.") — plain identifiers
|
||||
* spell their name (parens unwrap), `this`, property chains and
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/destructure-nullable-union-source.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// A destructuring declaration whose source is one object or array type or
|
||||
// null/undefined (`s.match(re)`, `items.find(...)`) throws Node's TypeError
|
||||
// when the source is null or undefined, with V8's message for the source
|
||||
// spelling, and binds normally otherwise.
|
||||
|
||||
function show(label, fn) {
|
||||
try {
|
||||
fn();
|
||||
} catch (error) {
|
||||
console.log(label, error instanceof TypeError, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const text = "abc";
|
||||
show("object-call", () => {
|
||||
const { index } = text.match(/z/);
|
||||
console.log("never", index);
|
||||
});
|
||||
show("object-empty", () => {
|
||||
const {} = text.match(/z/);
|
||||
console.log("never");
|
||||
});
|
||||
show("object-identifier", () => {
|
||||
const found = "abc".match(/z/);
|
||||
const { index, input } = found;
|
||||
console.log("never", index, input);
|
||||
});
|
||||
show("object-string-key", () => {
|
||||
const found = "abc".match(/z/);
|
||||
const { "index": at } = found;
|
||||
console.log("never", at);
|
||||
});
|
||||
show("array-method-call", () => {
|
||||
const [whole] = "abc".match(/z/);
|
||||
console.log("never", whole);
|
||||
});
|
||||
show("array-identifier", () => {
|
||||
const found = "abc".match(/z/);
|
||||
const [whole, group] = found;
|
||||
console.log("never", whole, group);
|
||||
});
|
||||
|
||||
const items = [
|
||||
{ name: "a", size: 1 },
|
||||
{ name: "b", size: 2 },
|
||||
];
|
||||
show("object-undefined", () => {
|
||||
const { name } = items.find((item) => item.size > 5);
|
||||
console.log("never", name);
|
||||
});
|
||||
function firstMatch(text) {
|
||||
return text.match(/q/);
|
||||
}
|
||||
show("array-function-call", () => {
|
||||
const [first] = firstMatch("abc");
|
||||
console.log("never", first);
|
||||
});
|
||||
|
||||
// A source that may be null or undefined, and data arms holding a
|
||||
// generator, which has no untyped form.
|
||||
function* count() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
}
|
||||
function pick(kind) {
|
||||
if (kind === 0) return null;
|
||||
if (kind === 1) return undefined;
|
||||
return { counter: count(), label: "picked" };
|
||||
}
|
||||
function counters(present) {
|
||||
return present ? [count(), count()] : null;
|
||||
}
|
||||
show("object-null-arm", () => {
|
||||
const { counter } = pick(0);
|
||||
console.log("never", counter);
|
||||
});
|
||||
show("object-undefined-arm", () => {
|
||||
const { label } = pick(1);
|
||||
console.log("never", label);
|
||||
});
|
||||
show("array-generator-elements", () => {
|
||||
const [first] = counters(false);
|
||||
console.log("never", first);
|
||||
});
|
||||
const { counter, label } = pick(2);
|
||||
const [left, right] = counters(true);
|
||||
console.log(label, [...counter].join(","), [...left, ...right].join(","));
|
||||
|
||||
// Sources that hold a value bind as before.
|
||||
const { index } = "abc".match(/b/);
|
||||
const [whole] = "abc".match(/c/);
|
||||
const { name, size } = items.find((item) => item.size > 1);
|
||||
console.log("bound", index, whole, name, size);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2635, "san": 3894 }
|
||||
Reference in New Issue
Block a user