Fix Buffer.from on narrowed callback unions (#422)

- Extract the checker-proven source arm before constructing a Buffer from a callback value.
- Cover narrowed byte and number array callbacks with Node differential fixtures.
This commit is contained in:
Chris Tate
2026-09-25 07:14:19 -05:00
committed by GitHub
parent f7bad5b3f8
commit b5c0d78d0d
3 changed files with 53 additions and 14 deletions
@@ -4621,7 +4621,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// Typed-array/Buffer receivers and the Buffer statics — before the
// island path (bytes never cross the boundary).
lowerer.lowerBytesMethodCall(expr, expr.expression) ??
lowerBufferStaticCallWithOptionalArg(lowerer, expr, expr.expression) ??
lowerBufferStaticCallWithNarrowedArg(lowerer, expr, expr.expression) ??
// URL.revokeObjectURL's zero-argument contract (the one-argument
// form keeps the fence — createObjectURL does too).
lowerUrlStaticCall(lowerer, expr, expr.expression) ??
@@ -4914,25 +4914,34 @@ function optionalCallValue(lowerer: Lowerer, node: ts.Expression): IrExpr | null
return lowerer.runtimeOptionalIdentifierValue(node)?.value ?? lowerAbsenceProbe(lowerer, node);
}
function lowerBufferStaticCallWithOptionalArg(
function lowerBufferStaticCallWithNarrowedArg(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
const lowered = lowerer.lowerBufferStaticCall(call, access);
if (lowered?.kind !== "bytesNew" || !lowered.source || lowered.source.type.kind !== "union") return lowered;
const present = lowerer.stripUndefinedArm(lowered.source.type);
const validSource =
(present.kind === "array" && present.elem.kind === "f64") ||
(present.kind === "bytes" && present.elem === "u8");
if (!validSource) return lowered;
const helper = lowerer.narrowedArmHelper(lowered.source.type.unionId, present, lowered.source.loc);
return helper
? {
...lowered,
source: { kind: "call", callee: helper, args: [lowered.source], type: present, loc: lowered.source.loc },
}
: lowered;
// A callback can store a wider union than the checker sees at this use
// (including an added undefined arm). Extract the exact arm proven by
// control-flow narrowing, not merely the union with undefined removed.
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(call.arguments[0]!));
if (
!narrowed ||
!(narrowed.kind === "f64" ||
(narrowed.kind === "array" && narrowed.elem.kind === "f64") ||
(narrowed.kind === "bytes" && narrowed.elem === "u8")) ||
lowerer.armTag(lowered.source.type.unionId, narrowed) < 0
) {
lowerer.unsupported("SC1090", call.arguments[0]!, "a Buffer constructor argument whose narrowed type is not a stored source arm");
}
const helper = lowerer.narrowedArmHelper(lowered.source.type.unionId, narrowed, lowered.source.loc);
if (!helper) {
lowerer.unsupported("SC1090", call.arguments[0]!, "a Buffer constructor argument whose narrowed type is not a stored source arm");
}
return {
...lowered,
source: { kind: "call", callee: helper, args: [lowered.source], type: narrowed, loc: lowered.source.loc },
};
}
function lowerOptionalNumberDefault(
@@ -6895,6 +6895,12 @@
],
"diags": []
},
"<repo>/tests/corpus/3021-buffer-union-narrow-callback.ts": {
"order": [
"<repo>/tests/corpus/3021-buffer-union-narrow-callback.ts"
],
"diags": []
},
"<repo>/tests/corpus/303-break-continue.ts": {
"order": [
"<repo>/tests/corpus/303-break-continue.ts"
@@ -0,0 +1,24 @@
type SqlParam = string | number | boolean | null | Uint8Array;
function encodeParam(param: SqlParam): string | number | boolean | null | { $hex: string } {
if (param !== null && typeof param === "object") {
return { $hex: Buffer.from(param).toString("hex") };
}
return param;
}
function encodeParams(params: SqlParam[]): string {
return JSON.stringify(params.map(encodeParam));
}
console.log(encodeParams(["a", 1, true, null, new Uint8Array([0xde, 0xad])]));
type ListParam = string | number[] | null;
function encodeList(param: ListParam): string {
if (Array.isArray(param)) return Buffer.from(param).toString("hex");
return param === null ? "null" : param;
}
const listParams: ListParam[] = [null, [1, 254], "text"];
console.log(listParams.map(encodeList).join("|"));