mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Short-circuit optional chains on any values only on the guarded step
`a?.b.c` on an `any` value answered undefined when `a` was present and `a.b` undefined: every later step of the chain read optionally. Node short-circuits only on the guarded receiver and throws reading `.c`. An `any` receiver now makes the tail an optional chain like a dyn one, and inside the chain's guarded body the consumed `?.` no longer marks the later dynamic reads optional. (Kept from #1561, whose checked-cast fallback is reverted.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c76892dc74
commit
22b9db26b5
@@ -235,7 +235,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
const receiver = tryLowerExpression(lowerer, expr.expression);
|
||||
if (receiver?.type.kind === "dyn") {
|
||||
const key = lowerer.lowerExprExpecting(expr.argumentExpression, DYN);
|
||||
const optional = hasOptionalChainGuard(expr.expression);
|
||||
const optional = hasOptionalChainGuard(expr.expression, lowerer);
|
||||
return lowerer.maybeNarrow(
|
||||
{
|
||||
kind: "dynKeyGet",
|
||||
@@ -378,7 +378,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
lowerer.lowerExpr(expr.argumentExpression),
|
||||
expr.argumentExpression,
|
||||
);
|
||||
const optional = hasOptionalChainGuard(expr.expression);
|
||||
const optional = hasOptionalChainGuard(expr.expression, lowerer);
|
||||
return lowerer.maybeNarrow(
|
||||
{
|
||||
kind: "dynKeyGet",
|
||||
@@ -488,7 +488,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
? lowerer.coerceToExpected(rawKey, DYN)
|
||||
: lowerRecordPropertyKey(lowerer, rawKey, expr.argumentExpression);
|
||||
if (key.type.kind === "string" || key.type.kind === "dyn") {
|
||||
const opt = hasOptionalChainGuard(expr.expression);
|
||||
const opt = hasOptionalChainGuard(expr.expression, lowerer);
|
||||
return lowerer.maybeNarrow(
|
||||
{
|
||||
kind: "dynKeyGet",
|
||||
|
||||
@@ -98,7 +98,11 @@ export function isRequireMainFilename(lowerer: Lowerer, expr: ts.Expression): bo
|
||||
export function isOptionalChainTail(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
const tail = chainTailDot(lowerer, expr);
|
||||
if (!tail) return false;
|
||||
const recvT = lowerer.mapTypeOf(lowerer.typeOf(tail.expression));
|
||||
const recvTs = lowerer.typeOf(tail.expression);
|
||||
// An `any` receiver short-circuits the whole tail too: `a?.b.c` reads
|
||||
// `.c` normally once `a` is present.
|
||||
if (recvTs.flags & ts.TypeFlags.Any) return true;
|
||||
const recvT = lowerer.mapTypeOf(recvTs);
|
||||
if (recvT?.kind === "dyn") return true;
|
||||
if (recvT?.kind === "union") {
|
||||
const def = lowerer.unions.get(recvT.unionId);
|
||||
@@ -119,7 +123,7 @@ export function isOptionalChainTail(lowerer: Lowerer, expr: ts.Expression): bool
|
||||
* throwing. Walks the receiver spine only; the argument of the access
|
||||
* itself is not part of the guard. Parentheses end the chain
|
||||
* (`(a?.b).c` reads `.c` from undefined); `!` does not. */
|
||||
export function hasOptionalChainGuard(expr: ts.Expression): boolean {
|
||||
export function hasOptionalChainGuard(expr: ts.Expression, lowerer?: Lowerer): boolean {
|
||||
let cur: ts.Expression = expr;
|
||||
for (;;) {
|
||||
if (ts.isNonNullExpression(cur)) {
|
||||
@@ -131,7 +135,11 @@ export function hasOptionalChainGuard(expr: ts.Expression): boolean {
|
||||
ts.isElementAccessExpression(cur) ||
|
||||
ts.isCallExpression(cur)
|
||||
) {
|
||||
if (cur.questionDotToken) return true;
|
||||
// Inside an optional chain's guarded body (dynamic reads pass the
|
||||
// lowerer), the enclosing optChain already short-circuits on the
|
||||
// consumed token, so the steps above it read normally: `a?.b.c`
|
||||
// throws when `a.b` is undefined.
|
||||
if (cur.questionDotToken) return lowerer === undefined || !lowerer.chainHandled.has(cur);
|
||||
cur = cur.expression;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -874,7 +874,7 @@ export function lowerPropertyAccessExpr(
|
||||
type: STRING,
|
||||
loc: locOf(expr.name),
|
||||
};
|
||||
const opt = hasOptionalChainGuard(expr.expression);
|
||||
const opt = hasOptionalChainGuard(expr.expression, lowerer);
|
||||
return lowerer.maybeNarrow(
|
||||
{
|
||||
kind: "dynKeyGet",
|
||||
@@ -983,7 +983,7 @@ export function lowerPropertyAccessExpr(
|
||||
type: STRING,
|
||||
loc: locOf(expr.name),
|
||||
};
|
||||
const opt = hasOptionalChainGuard(expr.expression);
|
||||
const opt = hasOptionalChainGuard(expr.expression, lowerer);
|
||||
return lowerer.maybeNarrow(
|
||||
{
|
||||
kind: "dynKeyGet",
|
||||
@@ -1115,7 +1115,7 @@ export function lowerPropertyAccessExpr(
|
||||
type: STRING,
|
||||
loc: locOf(expr.name),
|
||||
};
|
||||
const opt = hasOptionalChainGuard(expr.expression);
|
||||
const opt = hasOptionalChainGuard(expr.expression, lowerer);
|
||||
const read: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
key,
|
||||
@@ -1210,7 +1210,7 @@ export function lowerPropertyAccessExpr(
|
||||
type: STRING,
|
||||
loc: locOf(expr.name),
|
||||
};
|
||||
const opt = hasOptionalChainGuard(expr.expression);
|
||||
const opt = hasOptionalChainGuard(expr.expression, lowerer);
|
||||
return lowerer.maybeNarrow(
|
||||
{
|
||||
kind: "dynKeyGet",
|
||||
@@ -2829,7 +2829,7 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi
|
||||
kind: "dynKeyGet",
|
||||
value,
|
||||
key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) },
|
||||
...(expr.questionDotToken || hasOptionalChainGuard(expr.expression)
|
||||
...(expr.questionDotToken || hasOptionalChainGuard(expr.expression, lowerer)
|
||||
? { optional: true as const }
|
||||
: {}),
|
||||
type: DYN,
|
||||
@@ -3185,7 +3185,7 @@ export function lowerUnionProperty(
|
||||
kind: "dynKeyGet",
|
||||
value: lowerer.coerceToExpected(value, DYN),
|
||||
key,
|
||||
...(hasOptionalChainGuard(expr.expression) ? { optional: true as const } : {}),
|
||||
...(hasOptionalChainGuard(expr.expression, lowerer) ? { optional: true as const } : {}),
|
||||
type: DYN,
|
||||
loc: locOf(expr),
|
||||
};
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/optional-chain-any-tail.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// An optional chain on an `any` value short-circuits only on the guarded
|
||||
// step: `a?.b.c` is undefined when `a` is nullish and throws when `a` is
|
||||
// present but `a.b` is undefined, as in Node.
|
||||
function read(label: string, f: () => unknown): void {
|
||||
try {
|
||||
console.log(label, f());
|
||||
} catch (e) {
|
||||
console.log(label, "threw", (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
const present: any = JSON.parse('{"b":{"c":{"d":1}},"k":{"method":null}}');
|
||||
const empty: any = JSON.parse('{"b":{}}');
|
||||
const none: any = null;
|
||||
|
||||
read("present", () => present?.b.c.d);
|
||||
read("missing tail", () => empty?.b.c.d);
|
||||
read("nullish root", () => none?.b.c.d);
|
||||
read("second guard", () => empty?.b.c?.d);
|
||||
read("element", () => present?.["k"].method);
|
||||
read("call on nullish", () => none?.["k"].method?.());
|
||||
read("nested guard", () => empty.x?.b.c);
|
||||
Reference in New Issue
Block a user