mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Pass strings to an untyped 'data' listener on a child stream after setEncoding (#2602)
This commit is contained in:
@@ -824,6 +824,32 @@ function childStreamEncodedAt(
|
||||
return before;
|
||||
}
|
||||
|
||||
/** Whether the program calls `setEncoding` on the stream `receiver`
|
||||
* names: a call whose receiver has the same access path, anywhere in the
|
||||
* file. Its 'data' chunks can then be strings. */
|
||||
export function childStreamSetsEncoding(lowerer: Lowerer, receiver: ts.Expression): boolean {
|
||||
const key = accessPathOf(lowerer, receiver);
|
||||
if (key === null) return false;
|
||||
let found = false;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (found) return;
|
||||
if (
|
||||
ts.isCallExpression(node) &&
|
||||
ts.isPropertyAccessExpression(node.expression) &&
|
||||
node.expression.name.text === "setEncoding"
|
||||
) {
|
||||
const path = accessPathOf(lowerer, node.expression.expression);
|
||||
if (path !== null && path.root === key.root && path.path === key.path) {
|
||||
found = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(ts.getSourceFileOfNode(receiver));
|
||||
return found;
|
||||
}
|
||||
|
||||
/** An inline 'exit'/'close' listener may declare `(code: number, signal:
|
||||
* string)`: TypeScript accepts it through ChildProcess's catch-all `on`
|
||||
* overload, while Node passes `number | null` and `Signals | null`. Those
|
||||
@@ -850,7 +876,7 @@ function narrowedExitListenerParams(lowerer: Lowerer, listener: ts.Expression):
|
||||
|
||||
/** Lowers an inline listener with the given parameters bound as
|
||||
* checked-dynamic values typed `unknown` for the body. */
|
||||
function lowerWithCheckedParams(
|
||||
export function lowerWithCheckedParams(
|
||||
lowerer: Lowerer,
|
||||
listener: ts.Expression,
|
||||
symbols: readonly ts.Symbol[],
|
||||
|
||||
@@ -16,6 +16,10 @@ import { DYN, STRING, VOID } from "../../ir/ir.js";
|
||||
import { everyStmt } from "../../ir/traverse.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { ENTRY_NAME, type Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
childStreamSetsEncoding,
|
||||
lowerWithCheckedParams,
|
||||
} from "./builtins/child-process-methods.js";
|
||||
|
||||
/** `child.name = value` in a JavaScript file over a typed child: the write
|
||||
* lands in the child's property table through its checked-dynamic box. */
|
||||
@@ -235,12 +239,50 @@ export function lowerChildPropertyCall(
|
||||
recv,
|
||||
method: access.name.text,
|
||||
calleeName: ts.getTextOfNode(access),
|
||||
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
args: call.arguments.nodes.map((arg, i) =>
|
||||
i === 1 && dataListenerOfEncodedStream(lowerer, call, access)
|
||||
? lowerer.coerceToExpected(lowerUntypedChunkListener(lowerer, arg), DYN)
|
||||
: lowerer.lowerExprExpecting(arg, DYN),
|
||||
),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
const LISTENER_METHODS: ReadonlySet<string> = new Set([
|
||||
"on",
|
||||
"once",
|
||||
"addListener",
|
||||
"prependListener",
|
||||
"prependOnceListener",
|
||||
]);
|
||||
|
||||
/** `stream.on("data", (chunk) => ...)` with an unannotated chunk, on a
|
||||
* stream the program calls `setEncoding` on. The declarations type the
|
||||
* chunk as a Buffer, but after `setEncoding` Node passes strings. */
|
||||
function dataListenerOfEncodedStream(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): boolean {
|
||||
const [event, listener] = call.arguments.nodes;
|
||||
if (!LISTENER_METHODS.has(access.name.text) || event === undefined || listener === undefined)
|
||||
return false;
|
||||
if (!ts.isStringLiteralLike(event) || event.text !== "data") return false;
|
||||
if (!ts.isArrowFunction(listener) && !ts.isFunctionExpression(listener)) return false;
|
||||
const chunk = listener.parameters.nodes[0];
|
||||
if (chunk === undefined || chunk.type !== undefined || !ts.isIdentifier(chunk.name)) return false;
|
||||
return childStreamSetsEncoding(lowerer, access.expression);
|
||||
}
|
||||
|
||||
/** The listener with its chunk parameter bound as a checked-dynamic value,
|
||||
* which holds a string or a Buffer as Node passes it. */
|
||||
function lowerUntypedChunkListener(lowerer: Lowerer, listener: ts.Expression): IrExpr {
|
||||
const fn = listener as ts.ArrowFunction | ts.FunctionExpression;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(fn.parameters.nodes[0]!.name);
|
||||
return symbol ? lowerWithCheckedParams(lowerer, listener, [symbol]) : lowerer.lowerExpr(listener);
|
||||
}
|
||||
|
||||
/** `child.name` in JavaScript over an untyped receiver holding a child, or
|
||||
* for a name the program adds to children. */
|
||||
export function lowerChildPropertyRead(
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/child-stream-set-encoding-untyped-listener.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
// An unannotated 'data' listener on a child stream that setEncoding
|
||||
// switched to strings receives strings, as in Node's
|
||||
// test-child-process-default-options: the declarations type the chunk as
|
||||
// a Buffer. A stream without setEncoding keeps delivering Buffers, and a
|
||||
// listener added before the setEncoding call also receives strings.
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
process.env.HELLO = 'WORLD';
|
||||
const child = spawn('/usr/bin/env', [], {});
|
||||
let response = '';
|
||||
const kinds = new Set();
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stdout.on('data', function (chunk) {
|
||||
kinds.add(typeof chunk);
|
||||
response += chunk;
|
||||
});
|
||||
|
||||
const raw = spawn('printf', ['raw bytes']);
|
||||
let rawText = '';
|
||||
raw.stdout.on('data', (chunk) => {
|
||||
kinds.add(Buffer.isBuffer(chunk) ? 'buffer' : typeof chunk);
|
||||
rawText += chunk.toString('utf8', 0, chunk.length);
|
||||
});
|
||||
|
||||
const early = spawn('printf', ['early listener']);
|
||||
let earlyText = '';
|
||||
early.stdout.on('data', (chunk) => {
|
||||
earlyText += chunk;
|
||||
kinds.add(`early ${typeof chunk}`);
|
||||
});
|
||||
early.stdout.setEncoding('utf8');
|
||||
|
||||
process.on('exit', () => {
|
||||
console.log(response.includes('HELLO=WORLD'), JSON.stringify(rawText), JSON.stringify(earlyText));
|
||||
console.log([...kinds].sort().join(' '));
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 6521, "san": 6908 }
|
||||
Reference in New Issue
Block a user