Answer typeof for builtin module functions and classes (#1279)

This commit is contained in:
Chris Tate
2026-10-18 07:16:53 -05:00
committed by GitHub
4 changed files with 58 additions and 0 deletions
@@ -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))
);
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5974-typeof-module-exports.cjs"
],
"diags": []
}
@@ -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);
+2
View File
@@ -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,