mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Lower promise methods called through an optional chain
`p?.catch(h)`, `p?.then(h)` and a guarded chain tail (`f()?.then(h)`) were refused: the promise-method lowering rejected every `questionDotToken` instead of asking whether the optional chain was currently being lowered, so the guarded body never reached it. It uses `chainBlocked` now, like every other lowering that rides the chain. The call that ENDS a chain checker-types as the chain's undefined-armed union (`Promise<T> | undefined`) while this lowering builds the promise arm, which `finishOptionalChain` wraps back into the union. The handler's result type takes that arm, so the handler is typed against the promise it actually handles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
279d42e562
commit
bf28595035
@@ -279,7 +279,11 @@ import { isThisParameter, type ParamShape } from "./call-signatures.js";
|
||||
import { dynamicOverloadChoice } from "./overload-choice.js";
|
||||
import { lowerStringIteratorValue } from "./iterator-adapters.js";
|
||||
import { checkedArmExtraction } from "./coercions/unions.js";
|
||||
import { maybePromiseReturnStmt, promiseArmedUnionOf } from "./maybe-promise.js";
|
||||
import {
|
||||
maybePromiseReturnStmt,
|
||||
promiseArmedUnionOf,
|
||||
promiseOrUnitUnionOf,
|
||||
} from "./maybe-promise.js";
|
||||
import {
|
||||
requireObjLitGenericReceiver,
|
||||
objLitGenericFnNodeOf,
|
||||
@@ -5998,9 +6002,19 @@ export function lowerPromiseMethodCall(
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (call.questionDotToken || access.questionDotToken) return null;
|
||||
if (lowerer.chainBlocked(access, call)) return null;
|
||||
const member = access.name.text;
|
||||
if (member !== "then" && member !== "catch" && member !== "finally") return null;
|
||||
// A call that ENDS an optional chain (`refreshing?.catch(h)`,
|
||||
// `client?.close().catch(h)`) checker-types as the chain's
|
||||
// undefined-armed union while the body this lowering builds is the
|
||||
// promise arm — `finishOptionalChain` wraps it back into the union.
|
||||
// Take the arm so the handler typing sees the promise it handles.
|
||||
const callResultType = (): IrType | undefined => {
|
||||
const t = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (!t || t.kind === "promise") return t ?? undefined;
|
||||
return promiseOrUnitUnionOf(lowerer, t)?.promise ?? t;
|
||||
};
|
||||
let recvT =
|
||||
lowerer.mapTypeOf(lowerer.typeOf(access.expression)) ??
|
||||
dynFallbackType(lowerer, access.expression, lowerer.typeOf(access.expression));
|
||||
@@ -6117,7 +6131,7 @@ export function lowerPromiseMethodCall(
|
||||
const rejectedNode = call.arguments.nodes[1]!;
|
||||
const fulfilledAbsent = isAbsentHandler(fulfilledNode);
|
||||
const rejectedAbsent = isAbsentHandler(rejectedNode);
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const resultT = callResultType();
|
||||
// An untyped call in JavaScript whose receiver arrived as a typed
|
||||
// promise (a function instantiated with one): Promise.prototype.then
|
||||
// over the promise as a checked-dynamic value, which shares its
|
||||
@@ -6516,7 +6530,7 @@ export function lowerPromiseMethodCall(
|
||||
`then handlers whose parameter is not the settled value's type (expected '${lowerer.fmt(inner)}', got '${lowerer.fmt(param)}')`,
|
||||
);
|
||||
}
|
||||
let resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
let resultT = callResultType();
|
||||
// JavaScript whose declared result type has no mapping (zod's
|
||||
// `Promise.all(items).then((r) => ParseStatus.mergeArray(status, r))`,
|
||||
// typed by a .d.ts union): the handler's value settles the promise as
|
||||
@@ -6864,7 +6878,7 @@ export function lowerPromiseMethodCall(
|
||||
"value — take `(e)` or `(e: unknown)` and narrow with instanceof)",
|
||||
);
|
||||
}
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const resultT = callResultType();
|
||||
if (resultT?.kind !== "promise") {
|
||||
lowerer.noLowering(
|
||||
"catch with this handler's result type",
|
||||
|
||||
@@ -23,6 +23,18 @@ export function maybePromiseUnionOf(lowerer: Lowerer, t: IrType): MaybePromiseUn
|
||||
return armed;
|
||||
}
|
||||
|
||||
/** The promise arm of a promise-or-unit union (`Promise<void> | void`,
|
||||
* `Promise<T> | undefined`): exactly one promise arm and only nullish
|
||||
* arms beside it, so no other arm carries a value the promise path would
|
||||
* have to reconcile. A guard that proves the value present (`?.`, an
|
||||
* explicit narrow) leaves this arm alone. */
|
||||
export function promiseOrUnitUnionOf(lowerer: Lowerer, t: IrType): MaybePromiseUnion | null {
|
||||
const armed = promiseArmedUnionOf(lowerer, t);
|
||||
if (!armed) return null;
|
||||
if (!armed.def.arms.every((a, i) => i === armed.promiseTag || isUnitType(a))) return null;
|
||||
return armed;
|
||||
}
|
||||
|
||||
/** Any union with exactly one promise arm: a maybe-promise union or a
|
||||
* promise-or-unit union (`Promise<void> | void`). An async `return`
|
||||
* resolves both the same way: adopt the promise arm, fulfill otherwise. */
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// `?.` reaches a promise method through a receiver that may be nullish:
|
||||
// `p?.catch(handler)` guards the method read (JS calls nothing when `p` is
|
||||
// nullish), and the chain tail `f()?.then(..).catch(..)` guards the whole
|
||||
// tail. The guarded call itself is an ordinary promise method call, so the
|
||||
// handler typing sees the promise arm, and the chain result is
|
||||
// `Promise<..> | undefined`.
|
||||
|
||||
interface Client {
|
||||
name: string;
|
||||
}
|
||||
|
||||
class Box {
|
||||
opening: Promise<Client> | undefined;
|
||||
pending: Promise<void> | undefined;
|
||||
|
||||
constructor(opening: Promise<Client> | undefined, pending: Promise<void> | undefined) {
|
||||
this.opening = opening;
|
||||
this.pending = pending;
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
this.opening?.catch(() => undefined);
|
||||
console.log("1 opening guarded");
|
||||
await this.pending?.catch(() => undefined);
|
||||
console.log("2 pending guarded");
|
||||
}
|
||||
}
|
||||
|
||||
function open(name: string): Promise<Client> {
|
||||
return Promise.resolve({ name });
|
||||
}
|
||||
|
||||
function pick(flag: boolean): Promise<string> | undefined {
|
||||
return flag ? Promise.resolve("value") : undefined;
|
||||
}
|
||||
|
||||
function tail(flag: boolean): Promise<number | undefined> {
|
||||
return pick(flag)!
|
||||
.then((value) => {
|
||||
console.log("tail then", value);
|
||||
return value.length;
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
function guardedTail(flag: boolean): void {
|
||||
pick(flag)?.then((value) => {
|
||||
console.log("guarded then", value);
|
||||
return value.length;
|
||||
});
|
||||
console.log("3 tail attached", flag);
|
||||
}
|
||||
|
||||
const present = new Box(open("digest"), Promise.resolve());
|
||||
const absent = new Box(undefined, undefined);
|
||||
present
|
||||
.run()
|
||||
.then(() => absent.run())
|
||||
.then(() => tail(true))
|
||||
.then(() => {
|
||||
guardedTail(true);
|
||||
guardedTail(false);
|
||||
})
|
||||
.then(() => console.log("done"));
|
||||
Reference in New Issue
Block a user