mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Throw Node's TypeError for member reads of array holes in wide unions (#1806)
This commit is contained in:
@@ -985,11 +985,41 @@ export function runtimeOptionalReceiverRead(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// An array hole in a slot with more than two arms fails the member
|
||||
// access with Node's TypeError before the checked extraction runs.
|
||||
const holeGuarded = (extracted: IrExpr): IrExpr => {
|
||||
if (local.type.kind !== "union") return extracted;
|
||||
const holeTag = lowerer.armTag(local.type.unionId, UNDEFINED_T);
|
||||
if (holeTag < 0) return extracted;
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId: local.type.unionId,
|
||||
tag: holeTag,
|
||||
negated: false,
|
||||
value: varRef(local.id, local.type, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: nodeThrowExpr(
|
||||
1,
|
||||
"",
|
||||
runtimeOptionalReceiverMessage(expr, property),
|
||||
extracted.type,
|
||||
loc,
|
||||
),
|
||||
else_: extracted,
|
||||
type: extracted.type,
|
||||
loc,
|
||||
};
|
||||
};
|
||||
if (narrowed.kind === "object") {
|
||||
const view = checkedClassAssertion(lowerer, varRef(local.id, local.type, loc), narrowed, loc);
|
||||
// Preserve the member-specific missing-value error on the two-arm
|
||||
// path below; mixed storage also needs to validate its concrete class.
|
||||
if (view && (lowerer.unions.get(local.type.unionId)?.arms.length ?? 0) > 2) return view;
|
||||
if (view && (lowerer.unions.get(local.type.unionId)?.arms.length ?? 0) > 2)
|
||||
return holeGuarded(view);
|
||||
}
|
||||
if (narrowed.kind === "record" && lowerer.armTag(local.type.unionId, narrowed) < 0) {
|
||||
const hosted = hostedStorageView(lowerer, varRef(local.id, local.type, loc), narrowed, loc);
|
||||
@@ -1014,7 +1044,9 @@ export function runtimeOptionalReceiverRead(
|
||||
"a direct read from a runtime-optional capture with multiple value arms",
|
||||
);
|
||||
}
|
||||
return lowerer.checkedNarrowCall(helper, varRef(local.id, local.type, loc), narrowed, loc);
|
||||
return holeGuarded(
|
||||
lowerer.checkedNarrowCall(helper, varRef(local.id, local.type, loc), narrowed, loc),
|
||||
);
|
||||
}
|
||||
// A predicate can strengthen a record's fields or narrow a base class
|
||||
// to a subclass without changing the slot's stored value arm. Extract
|
||||
@@ -1029,10 +1061,7 @@ export function runtimeOptionalReceiverRead(
|
||||
"a runtime-optional receiver without a representable value arm",
|
||||
);
|
||||
}
|
||||
const message =
|
||||
property === null
|
||||
? `${expr.text} is not a function`
|
||||
: `Cannot read properties of undefined (reading '${property}')`;
|
||||
const message = runtimeOptionalReceiverMessage(expr, property);
|
||||
return lowerer.maybeNarrow(
|
||||
{
|
||||
kind: "ternary",
|
||||
@@ -1061,6 +1090,14 @@ export function runtimeOptionalReceiverRead(
|
||||
);
|
||||
}
|
||||
|
||||
/** Node's TypeError message for a member read (`property`) or a call
|
||||
* (`property` null) through an undefined runtime-optional receiver. */
|
||||
function runtimeOptionalReceiverMessage(expr: ts.Identifier, property: string | null): string {
|
||||
return property === null
|
||||
? `${expr.text} is not a function`
|
||||
: `Cannot read properties of undefined (reading '${property}')`;
|
||||
}
|
||||
|
||||
export function runtimeOptionalTrueIds(lowerer: Lowerer, node: ts.Expression): IrLocal[] {
|
||||
return runtimeOptionalGuardIds(lowerer, node, true);
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7280-named-reads-hole-receivers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Named reads and method calls through a parameter narrowed to one arm of a
|
||||
// wide union, when an array hole reaches the narrowed branch.
|
||||
type Bag = number | string | number[] | { a?: number; f?: () => string };
|
||||
function isBag(value: Bag): value is { a?: number; f?: () => string } {
|
||||
return !Array.isArray(value) && typeof value !== "string" && typeof value !== "number";
|
||||
}
|
||||
function peek(value: Bag): string {
|
||||
if (isBag(value)) return typeof value.a;
|
||||
return "-";
|
||||
}
|
||||
function call(value: Bag): string {
|
||||
if (isBag(value)) return value.f?.() ?? "none";
|
||||
return "-";
|
||||
}
|
||||
class Shape {
|
||||
side: number;
|
||||
constructor(side: number) {
|
||||
this.side = side;
|
||||
}
|
||||
area(): number {
|
||||
return this.side * this.side;
|
||||
}
|
||||
}
|
||||
type Item = Shape | string | number;
|
||||
function area(item: Item): number {
|
||||
if (item instanceof Shape || (typeof item !== "string" && typeof item !== "number")) return item.area();
|
||||
return 0;
|
||||
}
|
||||
const bags: Bag[] = [{ a: 1, f: () => "f" }, "s", [1]];
|
||||
bags.length = 4;
|
||||
const items: Item[] = [new Shape(2), "x"];
|
||||
items.length = 3;
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < bags.length; i++) {
|
||||
for (const which of [0, 1]) {
|
||||
try {
|
||||
out.push(which === 0 ? peek(bags[i]) : call(bags[i]));
|
||||
} catch (e) {
|
||||
out.push(e instanceof TypeError ? e.message : "other");
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
out.push(String(area(items[i])));
|
||||
} catch (e) {
|
||||
out.push(e instanceof TypeError ? e.message : "other");
|
||||
}
|
||||
}
|
||||
console.log(out.join("\n"));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4700, "san": 2400 }
|
||||
Reference in New Issue
Block a user