Call untyped write completion callbacks with the error argument (#2585)

This commit is contained in:
Chris Tate
2026-10-29 19:39:05 -05:00
committed by GitHub
5 changed files with 54 additions and 3 deletions
@@ -6966,11 +6966,15 @@ export function lowerStreamMethodCall(
const lowerWriteCb = (cbArg: ts.Expression): IrExpr => {
// The stream ignores what the callback returns (`() => count++`, a
// stored function returning a value): the value is discarded.
// An untyped callback (a function a JavaScript module returned) is
// called unbound with the error argument, as Node calls it.
const raw = lowerer.lowerExpr(cbArg);
const cb =
raw.type.kind === "func" && raw.type.ret.kind !== "void" && raw.type.params.length <= 1
? lowerer.coerceToExpected(raw, funcOf(raw.type.params, VOID))
: raw;
raw.type.kind === "dyn"
? lowerer.coerceToExpected(raw, funcOf([DYN], VOID))
: raw.type.kind === "func" && raw.type.ret.kind !== "void" && raw.type.params.length <= 1
? lowerer.coerceToExpected(raw, funcOf(raw.type.params, VOID))
: raw;
if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 1) {
lowerer.noLowering(
"write completion callbacks of this shape",
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/stream-dynamic-write-callback/report.js",
"<repo>/tests/corpus/stream-dynamic-write-callback/main.ts"
],
"diags": []
}
@@ -0,0 +1,28 @@
// Write completion callbacks from a JavaScript module run unbound with
// Node's arguments: null after a successful write, the _write error after a
// failed one, and the write-after-end error. Their return value is ignored.
import { Writable } from "node:stream";
import { makeReporter } from "./report.js";
const lines: string[] = [];
const sink = new Writable({
write(chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
callback(chunk.toString() === "bad" ? new Error("rejected") : null);
},
});
sink.on("error", (error) => lines.push(`error event ${error.message}`));
sink.write("good", makeReporter(lines, "good"));
sink.write("bad", "utf8", makeReporter(lines, "bad"));
const ended = new Writable({
write(_chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
callback();
},
});
ended.on("error", () => lines.push("ended error event"));
ended.end();
ended.write("late", makeReporter(lines, "late"));
setTimeout(() => {
for (const line of lines) console.log(line);
}, 5);
@@ -0,0 +1,11 @@
// A write completion callback built in JavaScript: it records how it was
// called. Module code is strict, so an unbound call sees `this` undefined.
export function makeReporter(lines, label) {
return function (...args) {
const error = args[0];
lines.push(
`${label} args=${args.length} error=${error === null ? "null" : error === undefined ? "undefined" : error.message} this=${typeof this}`,
);
return "ignored";
};
}
@@ -0,0 +1 @@
{ "plain": 1833, "san": 2753 }