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.
This commit is contained in:
Chris Tate
2026-10-29 04:41:05 -05:00
parent 22357e8bed
commit 64f9481fb8
4 changed files with 58 additions and 11 deletions
@@ -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;
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/web-streams-typed-element-tee.ts"
],
"diags": []
}
@@ -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<Uint8Array>): Promise<string> {
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<void> {
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();
@@ -0,0 +1 @@
{ "plain": 3000, "san": 6000 }