mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Store AbortController and AbortSignal handles in class fields
A class field typed AbortController or AbortSignal (plain, optional, nullable, or in an array) maps to checked-dynamic storage, and the class-field rule refused it (SC1090) although the same handles already live in locals, Map values, and closures. They now take the checked-dynamic slot, like the fetch Headers, Request, and Response handles. controller?.abort() on a nullable controller was refused as a method call (also for locals): the static abort lowering ran only without an optional chain. It now runs again once the chain has narrowed the receiver. Found by pi's TUI Editor (autocompleteAbort: AbortController | undefined). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
022428ead6
commit
684a0abd59
@@ -1630,16 +1630,22 @@ export function lowerStaticAbortControllerCall(
|
||||
call: ts.CallExpression,
|
||||
): IrExpr | null {
|
||||
const access = call.expression;
|
||||
// An optional chain (`controller?.abort()`) lowers the call again once it
|
||||
// has narrowed the receiver (Lowerer.chainHandled).
|
||||
if (
|
||||
call.questionDotToken ||
|
||||
(call.questionDotToken && !lowerer.chainHandled.has(call)) ||
|
||||
call.arguments.nodes.some((argument) => ts.isSpreadElement(argument)) ||
|
||||
(!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) ||
|
||||
access.questionDotToken ||
|
||||
(access.questionDotToken && !lowerer.chainHandled.has(access)) ||
|
||||
staticResponseMemberName(lowerer, access) !== "abort"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression));
|
||||
const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(
|
||||
access.questionDotToken
|
||||
? lowerer.checker.getNonNullableType(lowerer.typeOf(access.expression))
|
||||
: lowerer.typeOf(access.expression),
|
||||
);
|
||||
const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol();
|
||||
if (
|
||||
symbol === undefined ||
|
||||
|
||||
@@ -3910,7 +3910,8 @@ export class Lowerer {
|
||||
/** Declared class fields may store a DYN value only when its static type
|
||||
* is `unknown` or `object` (also with `null` or `undefined`, or an array
|
||||
* of those), names checked protocol identity, is an owned runtime
|
||||
* handle (a Worker or MessagePort, a fetch Headers/Request/Response, or
|
||||
* handle (a Worker or MessagePort, a fetch Headers/Request/Response or
|
||||
* AbortController/AbortSignal, or
|
||||
* a node:crypto ECDH, DiffieHellman or DiffieHellmanGroup), or is a
|
||||
* WeakMap/WeakSet (native weak collections live in checked-dynamic
|
||||
* storage, as locals of those types do); `any` fields stay refused. */
|
||||
|
||||
@@ -1747,9 +1747,17 @@ export function mapAsyncGeneratorAnnotation(type: ts.Type, ctx: TypeMapperCtx):
|
||||
return { kind: "generator", async: true, ...channels };
|
||||
}
|
||||
|
||||
/** The native fetch handles (Headers, Request, Response), optionally in
|
||||
* arrays or with null/undefined: checked-dynamic handles wherever they are
|
||||
* stored, like the worker handles above. */
|
||||
const FETCH_HANDLE_NAMES: ReadonlySet<string> = new Set([
|
||||
"Headers",
|
||||
"Request",
|
||||
"Response",
|
||||
"AbortController",
|
||||
"AbortSignal",
|
||||
]);
|
||||
|
||||
/** The native fetch handles (Headers, Request, Response, AbortController,
|
||||
* AbortSignal), optionally in arrays or with null/undefined: checked-dynamic
|
||||
* handles wherever they are stored, like the worker handles above. */
|
||||
export function isFetchHandleType(
|
||||
type: ts.Type,
|
||||
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
|
||||
@@ -1765,9 +1773,7 @@ export function isFetchHandleType(
|
||||
const symbol = member.getSymbol();
|
||||
if (
|
||||
!symbol ||
|
||||
(ts.symbolName(symbol) !== "Headers" &&
|
||||
ts.symbolName(symbol) !== "Request" &&
|
||||
ts.symbolName(symbol) !== "Response") ||
|
||||
!FETCH_HANDLE_NAMES.has(ts.symbolName(symbol)) ||
|
||||
!symbol.declarations.some(
|
||||
(d) =>
|
||||
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-abort-controller-fields.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Class fields typed AbortController and AbortSignal (plain, optional,
|
||||
// `| undefined`, `| null`, in arrays) hold the live handles: aborting
|
||||
// through the field is seen through every other reference, and optional
|
||||
// calls on a nullable field skip an empty slot.
|
||||
class Editor {
|
||||
private abort: AbortController | undefined;
|
||||
private signal: AbortSignal | null = null;
|
||||
private readonly lifetime = new AbortController();
|
||||
history: AbortController[] = [];
|
||||
last?: AbortSignal;
|
||||
start(): AbortSignal {
|
||||
this.abort?.abort();
|
||||
this.abort = new AbortController();
|
||||
this.history.push(this.abort);
|
||||
this.signal = this.abort.signal;
|
||||
this.last = this.signal;
|
||||
return this.signal;
|
||||
}
|
||||
cancel(reason?: string): boolean {
|
||||
this.abort?.abort(reason);
|
||||
return this.signal?.aborted ?? false;
|
||||
}
|
||||
reason(): unknown {
|
||||
return this.signal?.reason;
|
||||
}
|
||||
dispose(): boolean {
|
||||
this.lifetime.abort();
|
||||
return this.lifetime.signal.aborted;
|
||||
}
|
||||
}
|
||||
|
||||
const idle = new Editor();
|
||||
console.log(idle.cancel(), idle.reason());
|
||||
|
||||
const e = new Editor();
|
||||
const first = e.start();
|
||||
let events = 0;
|
||||
first.addEventListener("abort", () => {
|
||||
events++;
|
||||
});
|
||||
const second = e.start();
|
||||
console.log(first.aborted, second.aborted, events, e.history.length);
|
||||
console.log(e.cancel("stop"), e.reason(), second.aborted, second.reason);
|
||||
console.log(e.history[0]!.signal === first, e.last === second, e.dispose());
|
||||
|
||||
// A nullable local takes the same optional call.
|
||||
let maybe: AbortController | undefined = e.history.length > 5 ? new AbortController() : undefined;
|
||||
maybe?.abort();
|
||||
console.log(maybe?.signal.aborted);
|
||||
maybe = new AbortController();
|
||||
maybe?.abort("late");
|
||||
console.log(maybe?.signal.aborted, maybe?.signal.reason);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4302 }
|
||||
Reference in New Issue
Block a user