From 64f9481fb8a15bec461f2b5f3b94af2b398006d8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 04:41:05 -0500 Subject: [PATCH] Link the web stream classes for an element-access tee, pipeTo or pipeThrough The scan that decides whether a program uses a web stream operation the native streams lack only matched property accesses, so a TypeScript program whose one such operation was `body["tee"]()` kept the native streams and threw a TypeError at the call. Element accesses with a string literal key now count the same way, in the TypeScript scan and in the JavaScript one. Adds the corpus program web-streams-typed-element-tee.ts, with its corpus cost and order baseline. --- .../lowering/builtins/web-streams-dyn.ts | 39 +++++++++++++------ .../web-streams-typed-element-tee.ts.json | 6 +++ tests/corpus/web-streams-typed-element-tee.ts | 23 +++++++++++ .../web-streams-typed-element-tee.ts.json | 1 + 4 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-streams-typed-element-tee.ts.json create mode 100644 tests/corpus/web-streams-typed-element-tee.ts create mode 100644 tests/harness/corpus-costs/web-streams-typed-element-tee.ts.json diff --git a/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts b/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts index 67caec84a..809d5bb03 100644 --- a/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts +++ b/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts @@ -119,11 +119,8 @@ export function programUsesTypedWebStreams(lowerer: Lowerer): boolean { for (const file of lowerer.program.getSourceFiles()) { if (file.isDeclarationFile || isJsSourceFile(file)) continue; ts.walkPreorder(file, (node) => { - if ( - ts.isPropertyAccessExpression(node) && - WEB_VIEW_CALLS.has(node.name.text) && - isReadableStreamMember(lowerer, node) - ) + const member = staticMemberName(node); + if (member !== null && WEB_VIEW_CALLS.has(member) && isReadableStreamMember(lowerer, node)) found = true; else if (ts.isNewExpression(node)) { const name = stdlibGlobalNameOf(lowerer, unparenthesized(node.expression)); @@ -316,10 +313,29 @@ function isReadableStreamType(lowerer: Lowerer, type: ts.Type): boolean { ); } -/** Whether a member access resolves to a member of the stdlib - * ReadableStream interface. */ -function isReadableStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { - const member = lowerer.checker.getSymbolAtLocation(access.name); +type MemberAccess = ts.PropertyAccessExpression | ts.ElementAccessExpression; + +/** The member a property access names, or an element access with a string + * literal key (`body.tee` and `body["tee"]`); null for any other node. */ +function staticMemberName(node: ts.Node): string | null { + if (ts.isPropertyAccessExpression(node)) return node.name.text; + if (ts.isElementAccessExpression(node)) { + const key = node.argumentExpression; + if (ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) return key.text; + } + return null; +} + +/** Whether a member access (staticMemberName) resolves to a member of the + * stdlib ReadableStream interface. */ +function isReadableStreamMember(lowerer: Lowerer, access: ts.Node): boolean { + const key = ts.isPropertyAccessExpression(access) + ? access.name + : ts.isElementAccessExpression(access) + ? access.argumentExpression + : null; + if (key === null) return false; + const member = lowerer.checker.getSymbolAtLocation(key); if (member === undefined || !lowerer.isStdlibSymbol(member)) return false; let owner: ts.Node | undefined = member.declarations[0]?.parent; while (owner !== undefined && !ts.isInterfaceDeclaration(owner)) owner = owner.parent; @@ -366,9 +382,10 @@ function programUsesWebStreamOnlyMembers(lowerer: Lowerer, functions: IrFunction let found = false; ts.walkPreorder(file, (node) => { let receiver: ts.Type | null = null; - if (ts.isPropertyAccessExpression(node) && members.has(node.name.text)) { + const member = staticMemberName(node); + if (member !== null && members.has(member)) { if (isReadableStreamMember(lowerer, node)) found = true; - else receiver = lowerer.typeOf(node.expression); + else receiver = lowerer.typeOf((node as MemberAccess).expression); } else if (iterates && ts.isForOfStatement(node) && node.awaitModifier !== undefined) { receiver = lowerer.typeOf(node.expression); if (isReadableStreamType(lowerer, receiver)) found = true; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-streams-typed-element-tee.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-streams-typed-element-tee.ts.json new file mode 100644 index 000000000..61b037166 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-streams-typed-element-tee.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/web-streams-typed-element-tee.ts" + ], + "diags": [] +} diff --git a/tests/corpus/web-streams-typed-element-tee.ts b/tests/corpus/web-streams-typed-element-tee.ts new file mode 100644 index 000000000..744ff6bb3 --- /dev/null +++ b/tests/corpus/web-streams-typed-element-tee.ts @@ -0,0 +1,23 @@ +// ReadableStream tee() reached through an element access with a string key +// is the program's only web stream operation: the program still links the +// web stream classes, so a Response body splits into two branches. + +async function text(stream: ReadableStream): Promise { + const reader = stream.getReader(); + const decoder = new TextDecoder(); + let out = ""; + for (;;) { + const result = await reader.read(); + if (result.done) break; + out += decoder.decode(result.value, { stream: true }); + } + return out + decoder.decode(); +} + +async function main(): Promise { + const body = new Response("split me").body!; + const [left, right] = body["tee"](); + console.log("element tee", await text(left), "|", await text(right), body.locked); +} + +void main(); diff --git a/tests/harness/corpus-costs/web-streams-typed-element-tee.ts.json b/tests/harness/corpus-costs/web-streams-typed-element-tee.ts.json new file mode 100644 index 000000000..5cce6d4a6 --- /dev/null +++ b/tests/harness/corpus-costs/web-streams-typed-element-tee.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 6000 }