From 0ee069f094d4aa7c880e0b1282250659c4abefc1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 06:53:41 -0500 Subject: [PATCH] Answer typeof for builtin module functions and classes `typeof zlib.Inflate` and `typeof zlib.createGzip` stopped at SC1090 or SC2020, because the operand lowered as a library function or class value. A typeof of a function or class exported by a Node builtin module, read through a plain binding, now folds to "function" as in Node: the read has no side effects to preserve. --- .../src/frontend/lowering/lower-exprs.ts | 41 +++++++++++++++++++ .../5974-typeof-module-exports.cjs.json | 6 +++ tests/corpus/5974-typeof-module-exports.cjs | 9 ++++ tests/harness/corpus-costs.json | 2 + 4 files changed, 58 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5974-typeof-module-exports.cjs.json create mode 100644 tests/corpus/5974-typeof-module-exports.cjs diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 6774de117..f0538bb71 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -648,6 +648,30 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // have non-stdlib symbols and keep the ordinary path. let tested = expr.expression; while (ts.isParenthesizedExpression(tested)) tested = tested.expression; + // `typeof zlib.Inflate` / `typeof zlib.createGzip`: an export of a + // Node builtin module read through a plain binding (an import or + // require result). The read has no side effects, and Node's answer for + // every exported function and class is "function". + if ( + ts.isPropertyAccessExpression(tested) && + ts.isIdentifier(tested.expression) && + lowerer.isStdlibMember(tested) + ) { + const sym = lowerer.checker.getSymbolAtLocation(tested.name); + const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + if ( + decls.some(isBuiltinModuleValueDecl) && + decls.every((decl) => builtinModuleExportParent(decl) !== undefined) + ) { + const t = lowerer.typeOf(tested); + if ( + lowerer.checker.getCallSignatures(t).length > 0 || + lowerer.checker.getConstructSignatures(t).length > 0 + ) { + return { kind: "strLit", value: "function", type: STRING, loc }; + } + } + } if (ts.isIdentifier(tested)) { const sym = lowerer.checker.getSymbolAtLocation(tested); if (!sym) { @@ -15825,3 +15849,20 @@ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, loc: SrcLoc): IrE } return { kind: "call", callee: name, args: [source], type: DYN, loc }; } + +/** The `declare module "..."` body (or nested namespace body) a module + * export's declaration sits directly in, or undefined. */ +function builtinModuleExportParent(decl: ts.Node): ts.ModuleBlock | undefined { + const parent = ts.isVariableDeclaration(decl) ? decl.parent?.parent?.parent : decl.parent; + return parent !== undefined && ts.isModuleBlock(parent) ? parent : undefined; +} + +/** A function, class or variable exported by a builtin module's body. */ +function isBuiltinModuleValueDecl(decl: ts.Node): boolean { + return ( + builtinModuleExportParent(decl) !== undefined && + (ts.isFunctionDeclaration(decl) || + ts.isClassDeclaration(decl) || + ts.isVariableDeclaration(decl)) + ); +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5974-typeof-module-exports.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5974-typeof-module-exports.cjs.json new file mode 100644 index 000000000..469ee3272 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5974-typeof-module-exports.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5974-typeof-module-exports.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/5974-typeof-module-exports.cjs b/tests/corpus/5974-typeof-module-exports.cjs new file mode 100644 index 000000000..7e209f147 --- /dev/null +++ b/tests/corpus/5974-typeof-module-exports.cjs @@ -0,0 +1,9 @@ +// typeof on functions and classes exported by Node builtin modules, read +// through require bindings: Node answers "function" for each. +const zlib = require("node:zlib"); +const path = require("node:path"); +const fs = require("node:fs"); +console.log(typeof zlib.createGzip, typeof zlib.createBrotliDecompress, typeof zlib.gunzipSync); +console.log(typeof zlib.Inflate, typeof zlib.Gzip, typeof zlib.DeflateRaw); +console.log(typeof path.join, typeof path.posix.join, typeof fs.readFileSync); +console.log(typeof path.sep); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 2c2eeab31..25f00b1f7 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1908,6 +1908,7 @@ "5970-net-connect-host-name.ts": 2400, "5971-buffer-chunks-to-string.ts": 2700, "5972-net-set-encoding-listeners.ts": 2550, + "5974-typeof-module-exports.cjs": 2000, "5990-worker-uncloneable.mjs": 3283, "5991-worker-async-dispose.mjs": 5699, "5992-worker-thread-name-lifetime.mjs": 2687, @@ -4603,6 +4604,7 @@ "5970-net-connect-host-name.ts": 2800, "5971-buffer-chunks-to-string.ts": 3100, "5972-net-set-encoding-listeners.ts": 3000, + "5974-typeof-module-exports.cjs": 2500, "5990-worker-uncloneable.mjs": 22432, "5991-worker-async-dispose.mjs": 25768, "5992-worker-thread-name-lifetime.mjs": 28932,