Let constructor functions inherit from EventEmitter and the stream classes (#1512)

This commit is contained in:
Chris Tate
2026-10-20 11:32:22 -05:00
committed by GitHub
44 changed files with 2097 additions and 8 deletions
@@ -16,7 +16,11 @@ Other immutable aliases of table-backed Node builtins support direct calls, but
Native calls accept standard process streams; custom native streams and mutations to `util.inspect.colors` remain unsupported.
`util.inherits` links the prototypes of constructor functions, and `util.deprecate`, `util.callbackify`, and `util.debuglog` return native function values that forward receivers and arguments.
Class constructors passed to `util.inherits` or `util.deprecate` are refused.
In JavaScript, a constructor function can inherit from `EventEmitter`, the legacy `Stream` constructor, or a stream class through `util.inherits` or `Object.setPrototypeOf`.
`EventEmitter.call(this)` and `Stream.call(this)` initialize the object as Node does, and its listener methods keep their state in the object's own `_events`, `_eventsCount`, and `_maxListeners` properties.
The `captureRejections` option and `events.errorMonitor` listeners are not supported on such objects.
Calling `Readable`, `Writable`, `Duplex`, `Transform`, or `PassThrough` with a constructor function's receiver throws `SC2020` where the call runs.
Other class constructors passed to `util.inherits` or `util.deprecate` are refused.
Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported.
A stored `util.deprecate` reports a `length` of 4, where Node reports 3.
In TypeScript, a `util.callbackify` result must reach a callback whose error parameter accepts `null`; the @types/node overloads that type it as non-null are refused.
@@ -32,6 +32,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
bigint: features.bigint,
searchParams: features.searchParams,
qs: features.qs,
dynBuiltinClass: features.dynBuiltinClass,
parseArgs: features.parseArgs,
stream: features.stream,
net: features.net,
@@ -42,6 +42,7 @@ export async function compilePackedLibrary(
bigint: options.bigint ?? false,
searchParams: options.searchParams ?? false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -1437,6 +1437,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.classPrototype": "scr_dyn_class_prototype",
"dyn.classBasePrototype": "scr_dyn_class_base_prototype",
"dyn.classInherit": "scr_dyn_class_inherit",
"dyn.builtinClass": "scr_dyn_builtin_class",
"dyn.builtinInstanceOf": "scr_dyn_builtin_instanceof",
"dyn.functionInstanceOf": "scr_dyn_function_instanceof",
"dyn.classSuper": "scr_dyn_class_super",
"dyn.assignPrototype": "scr_dyn_assign_prototype",
"dyn.definePrototypeProps": "scr_dyn_define_prototype_props",
@@ -49,6 +49,7 @@ export interface NativeLinkFeatures {
bigint: boolean;
searchParams: boolean;
qs: boolean;
dynBuiltinClass: boolean;
parseArgs: boolean;
stream: boolean;
net: boolean;
@@ -152,6 +152,11 @@ export interface CcOptions {
* line (escape-only programs ride the always-linked component encoder
* and never flip this). */
qs?: boolean;
/** The program uses EventEmitter or a stream class as an untyped
* constructor value (a constructor function's base): compiles
* scr_dyn_builtin_class.c, which also references the inspection,
* dyn-invoke and symbol units. */
dynBuiltinClass?: boolean;
/** The program uses native util parsing, comparison, or styling. Optional
* utility units also require their symbol, inspection, and warning features. */
parseArgs?: boolean;
@@ -236,6 +236,15 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
(u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync && u.opts.dynInvoke && u.regex,
"scr_util_callable.c",
),
unit(
(u) =>
u.opts.dynBuiltinClass &&
u.opts.inspect &&
u.opts.dynInvoke &&
u.opts.symbol &&
(u.opts.emitter || u.opts.workers),
"scr_dyn_builtin_class.c",
),
unit((u) => u.opts.stream, "scr_stream.c", "scr_fs_stream.c"),
// The zlib stream classes reference both the zlib engine and the
// stream unit.
@@ -107,6 +107,7 @@ const features = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -44,6 +44,7 @@ const BASE: NativeLinkFeatures = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -50,6 +50,7 @@ export async function warmNativeCaches(
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -45,6 +45,7 @@ const native: EarlyExecutableNativeFeatures = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -72,6 +72,7 @@ export interface EarlyExecutableNativeFeatures {
bigint: boolean;
searchParams: boolean;
qs: boolean;
dynBuiltinClass: boolean;
parseArgs: boolean;
stream: boolean;
net: boolean;
@@ -190,6 +191,7 @@ const BOOLEAN_NATIVE_KEYS = [
"bigint",
"searchParams",
"qs",
"dynBuiltinClass",
"parseArgs",
"stream",
"net",
@@ -0,0 +1,149 @@
import { numLit } from "../../../ir/build.js";
import { BOOL, DYN, RUNTIME_EMITTER_CLASS, type IrExpr, type SrcLoc } from "../../../ir/ir.js";
import * as ts from "../../ts7/adapter.js";
import { locOf } from "../../program.js";
import type { Lowerer } from "../lowerer.js";
/* EventEmitter and the stream classes as untyped constructor values, the
* bases of JavaScript constructor functions:
*
* function Bus() { EventEmitter.call(this); }
* util.inherits(Bus, EventEmitter);
*
* A compiled `class Bus extends EventEmitter` gets a native emitter prefix,
* but `new Bus()` on a constructor function builds an untyped object, so
* the base is the runtime's dyn constructor (scr_dyn_builtin_class.c): a
* function whose prototype carries Node's EventEmitter methods over the
* object's own `_events` table. The stream classes have prototype chains
* for util.inherits and instanceof; constructing over them refuses where
* it runs. These values appear only where a constructor function uses its
* base: util.inherits, `Base.call(this, …)` and `Base.apply(this, …)`,
* `Base.prototype` and `instanceof Base` on an untyped value. */
/** scr_dyn_builtin_class's kind for a runtime class, or null. */
export function builtinClassKind(className: string): number | null {
switch (className) {
case RUNTIME_EMITTER_CLASS:
return 0;
case "%Readable":
return 2;
case "%Writable":
return 3;
case "%Duplex":
return 4;
case "%Transform":
return 5;
case "%PassThrough":
return 6;
default:
return null;
}
}
/** The classes by builtin module member. JavaScript sources bind
* `require("events").EventEmitter` as a module member rather than a class
* value, and `require("stream")` is the legacy Stream constructor. */
const MODULE_CLASS_KINDS: Readonly<Record<string, Readonly<Record<string, number>>>> = {
events: { EventEmitter: 0, default: 0 },
stream: { Stream: 1, Readable: 2, Writable: 3, Duplex: 4, Transform: 5, PassThrough: 6 },
};
/** The kind of a builtin module member that is one of those classes. */
export function builtinMemberClassKind(module: string, member: string): number | null {
return MODULE_CLASS_KINDS[module]?.[member] ?? null;
}
function unparenthesized(node: ts.Expression): ts.Expression {
let current = node;
while (ts.isParenthesizedExpression(current)) current = current.expression;
return current;
}
/** The kind of a node that names one of those classes: a class value, a
* builtin module member, or the events and stream modules themselves. */
export function builtinClassKindOf(lowerer: Lowerer, node: ts.Expression): number | null {
const expr = unparenthesized(node);
const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (mapped?.kind === "classval") return builtinClassKind(mapped.className);
const member = ts.isIdentifier(expr)
? lowerer.builtinImportOf(expr)
: ts.isPropertyAccessExpression(expr)
? lowerer.builtinMemberOf(expr)
: null;
if (member) return MODULE_CLASS_KINDS[member.module]?.[member.member] ?? null;
const module = lowerer.builtinNamespaceModuleOf(expr);
return module === "events" ? 0 : module === "stream" ? 1 : null;
}
/** Whether evaluating the node can be skipped: a binding or a member of
* one, or of a builtin require. */
function referenceOnly(node: ts.Expression): boolean {
const expr = unparenthesized(node);
if (ts.isIdentifier(expr)) return true;
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
const base = unparenthesized(expr.expression);
return (
ts.isIdentifier(base) ||
(ts.isCallExpression(base) &&
ts.isIdentifier(base.expression) &&
base.expression.text === "require" &&
base.arguments.nodes.length === 1 &&
ts.isStringLiteral(base.arguments.nodes[0]!))
);
}
return false;
}
/** The untyped constructor named by a node, or null when it names none of
* those classes. */
export function builtinClassDynValueOf(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
const kind = builtinClassKindOf(lowerer, node);
if (kind === null) return null;
const loc = locOf(node);
const value = builtinClassValue(kind, loc);
if (referenceOnly(node)) return value;
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(node), loc }],
result: value,
type: DYN,
loc,
};
}
export function builtinClassValue(kind: number, loc: SrcLoc): IrExpr {
return { kind: "libCall", fn: "dyn.builtinClass", args: [numLit(kind, loc)], type: DYN, loc };
}
/** `Base.call(this, …)` / `Base.apply(this, args)` on one of those class
* values: Function.prototype.call/apply over the untyped constructor. */
export function lowerBuiltinClassCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
const callee = call.expression;
if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken) return null;
const method = callee.name.text;
if (method !== "call" && method !== "apply") return null;
if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
const receiver = builtinClassDynValueOf(lowerer, callee.expression);
if (receiver === null) return null;
const loc = locOf(call);
return {
kind: "dynInvoke",
recv: receiver,
method,
calleeName: callee.getText(),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
};
}
/** `value instanceof Base` for an untyped value. */
export function builtinClassInstanceOf(value: IrExpr, kind: number, loc: SrcLoc): IrExpr {
return {
kind: "libCall",
fn: "dyn.builtinInstanceOf",
args: [value, numLit(kind, loc)],
type: BOOL,
loc,
};
}
@@ -1,4 +1,5 @@
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
import { builtinClassKind, builtinClassValue } from "./builtins/builtin-class-dyn.js";
import * as ts from "../ts7/adapter.js";
import {
BOOL,
@@ -450,6 +451,19 @@ export function lowerClassPrototypeData(
exactClassOfReceiver(lowerer, expr.expression) ??
(stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined);
if (!info) return null;
// EventEmitter.prototype and the stream prototypes: the untyped
// constructor's prototype object (builtin-class-dyn.ts).
const builtinKind = builtinClassKind(info.def.name);
if (builtinKind !== null) {
const loc = locOf(expr);
return {
kind: "dynKeyGet",
value: builtinClassValue(builtinKind, loc),
key: { kind: "strLit", value: "prototype", type: STRING, loc },
type: DYN,
loc,
};
}
const parent = expr.parent;
const descriptorKey =
ts.isCallExpression(parent) &&
@@ -1,5 +1,6 @@
/* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */
import { cjsMemberAliasValueOf } from "../cjs-exports-object.js";
import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js";
import * as ts from "../../ts7/adapter.js";
import * as posix from "node:path/posix";
import { type Lowerer, PoisonError } from "../lowerer.js";
@@ -539,6 +540,10 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// the same identity-token story as stdlib globals above (the
// harness adds worker_threads.Worker to its identity Set).
if (isJsSourceFile(expr.getSourceFile())) {
// EventEmitter and the stream classes are the runtime's untyped
// constructors, so constructor functions can inherit from them.
const classKind = builtinMemberClassKind(bi.module, bi.member);
if (classKind !== null) return builtinClassValue(classKind, loc);
// A function member is a function value that fences when called.
if (builtinModuleFnOf(lowerer, bi.module, bi.member))
return builtinFunctionFenceValue(lowerer, bi, expr, loc);
@@ -1,5 +1,6 @@
/* instanceof and in, including private-name and runtime-key in. */
import * as ts from "../../ts7/adapter.js";
import { builtinClassInstanceOf, builtinClassKind } from "../builtins/builtin-class-dyn.js";
import {
checkedPrivateIn,
localPrivateBrandClass,
@@ -473,6 +474,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return checkedClassInstanceOf(lowerer, value, constructor, loc);
}
// `this instanceof Glob` in a JavaScript constructor function: the
// function's prototype against the value's prototype chain.
if (
constructor?.type.kind === "func" &&
isJsSourceFile(expr.getSourceFile()) &&
lowerer.dynConvertible(constructor.type)
) {
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return {
kind: "libCall",
fn: "dyn.functionInstanceOf",
args: [value, { kind: "dynFrom", value: constructor, type: DYN, loc }],
type: BOOL,
loc,
};
}
lowerer.unsupported(
"SC1090",
expr.right,
@@ -581,6 +598,14 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
loc,
};
}
// `u instanceof EventEmitter` (or a stream class): an untyped object
// built by a constructor function inherits the untyped constructor's
// prototype (builtin-class-dyn.ts). Compiled emitters and streams have
// no untyped form, so the prototype chain answers for every value.
const builtinKind = builtinClassKind(target.def.name);
if (left.type.kind === "dyn" && builtinKind !== null) {
return builtinClassInstanceOf(left, builtinKind, loc);
}
if (left.type.kind === "dyn") {
lowerer.unsupported(
"SC1090",
@@ -1,4 +1,5 @@
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js";
import { lowerBuiltinClassCall } from "./builtins/builtin-class-dyn.js";
import { refuseModuleMemberThis } from "./module-member-this.js";
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
import { lowerHashStreamCall } from "./builtins/hash-stream.js";
@@ -266,6 +267,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
if (object) return object;
}
// `EventEmitter.call(this)` in a constructor function: the untyped
// EventEmitter constructor initializes the receiver.
const builtinClassCall = lowerBuiltinClassCall(lowerer, expr);
if (builtinClassCall) return builtinClassCall;
// require() of a JSON document: the shared parsed object.
const json = lowerJsonRequireCall(lowerer, expr);
if (json) return json;
@@ -1,4 +1,5 @@
import { dynUndefinedExpr, boolLit, helperLoc, numLit, strLit, varRef } from "../../ir/build.js";
import { builtinClassDynValueOf, builtinClassKindOf } from "./builtins/builtin-class-dyn.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
@@ -2609,8 +2610,15 @@ function lowerUtilCallableCall(
const arity = member === "deprecate" ? 4 : member === "callbackify" ? 1 : 2;
// Program classes keep static prototypes: there is no function object
// whose prototype chain the runtime could relink or wrap.
// EventEmitter and the stream classes are the exception as superCtor:
// the runtime has untyped constructors for them (builtin-class-dyn.ts).
const classArgs = member === "inherits" ? 2 : member === "deprecate" ? 1 : 0;
for (const arg of expr.arguments.nodes.slice(0, classArgs)) {
const builtinBase =
member === "inherits" && expr.arguments.nodes[1] !== undefined
? builtinClassKindOf(lowerer, expr.arguments.nodes[1])
: null;
for (const [index, arg] of expr.arguments.nodes.slice(0, classArgs).entries()) {
if (index === 1 && builtinBase !== null) continue;
if (lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "classval")
lowerer.noLowering(
`util.${member} over a class constructor`,
@@ -2629,9 +2637,12 @@ function lowerUtilCallableCall(
const stmts: IrStmt[] = [];
const args: IrExpr[] = [];
for (let i = 0; i < arity; i++) {
const input: IrExpr = expr.arguments.nodes[i]
? lowerer.lowerExprExpecting(expr.arguments.nodes[i]!, DYN)
: dynUndefinedExpr(loc);
const node = expr.arguments.nodes[i];
const input: IrExpr = !node
? dynUndefinedExpr(loc)
: i === 1 && builtinBase !== null
? (builtinClassDynValueOf(lowerer, node) ?? lowerer.lowerExprExpecting(node, DYN))
: lowerer.lowerExprExpecting(node, DYN);
const saved = lowerer.declareHiddenLocal("%utilInput", DYN);
stmts.push({ kind: "varDecl", localId: saved.id, init: input, loc });
args.push(varRef(saved.id, DYN, loc));
@@ -1,6 +1,7 @@
import { needsGeneralNodeStreams, stdioIntoGeneralStream } from "./general-streams.js";
import { extendAssertedRecordMembers } from "./assertion-members.js";
import { jsDynamicDeclarations } from "./js-dynamic-objects.js";
import { builtinClassValue, builtinMemberClassKind } from "./builtins/builtin-class-dyn.js";
import {
lifetimeRegionType,
lowerLifetimeRegionCall,
@@ -12686,6 +12687,12 @@ export class Lowerer {
}
// A bare `fs.constants` read (not one of the baked bits above).
if (bi.member === "constants" && bi.module === "fs") return fsConstantsObject(loc);
// `stream.Readable` as a JavaScript value: the runtime's untyped
// constructor, the base a constructor function inherits.
if (isJsSourceFile(expr.getSourceFile())) {
const classKind = builtinMemberClassKind(bi.module, bi.member);
if (classKind !== null) return builtinClassValue(classKind, loc);
}
if (builtinModuleFnOf(this, bi.module, bi.member)) {
this.unsupported(
"SC1090",
+1
View File
@@ -551,6 +551,7 @@ async function compileExecutableNative(
bigint: features.bigint,
searchParams: features.searchParams,
qs: features.qs,
dynBuiltinClass: features.dynBuiltinClass,
parseArgs: features.parseArgs,
stream: features.stream,
net: features.net,
@@ -576,6 +576,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.objCreateWithProperties",
"dyn.getPrototype",
"dyn.setPrototype",
// a proxy in the prototype chain can throw
"dyn.builtinInstanceOf",
"dyn.functionInstanceOf",
"dyn.getOwnPropertyNames",
"dyn.ownKeys",
"dyn.getOwnPropertySymbols",
@@ -3536,6 +3536,16 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"dyn.classPrototype": { argTypes: [DYN, DYN], result: DYN },
"dyn.classBasePrototype": { argTypes: [DYN], result: DYN },
"dyn.classInherit": { argTypes: [DYN, DYN], result: DYN },
/** EventEmitter (0), Stream (1), Readable (2), Writable (3), Duplex (4),
* Transform (5) and PassThrough (6) as untyped constructor values, one
* per thread (scr_dyn_builtin_class.c). */
"dyn.builtinClass": { argTypes: [F64], result: DYN },
/** `value instanceof C` for those constructors, over the value's
* prototype chain. */
"dyn.builtinInstanceOf": { argTypes: [DYN, F64], result: BOOL },
/** `value instanceof f` for an untyped constructor function: the
* prototype chain against f.prototype. */
"dyn.functionInstanceOf": { argTypes: [DYN, DYN], result: BOOL },
"dyn.classSuper": { argTypes: [DYN, DYN, DYN], result: VOID },
"dyn.assignPrototype": { argTypes: [DYN, DYN, DYN], result: DYN },
"dyn.definePrototypeProps": { argTypes: [DYN, DYN, DYN], result: DYN },
+30 -3
View File
@@ -241,8 +241,21 @@ const CHILD_PROCESS_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
/** libCall name prefixes that link the child-process unit. */
const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRes.", "writer."];
/** The entry points scr_dyn_builtin_class.c defines. The unit calls
* listeners through the dyn-invoke unit, renders unhandled 'error'
* payloads through the inspect unit, creates EventEmitter's private
* symbols through the symbol unit and shares defaultMaxListeners with the
* emitter unit, so each of them links all five. */
const BUILTIN_CLASS_LIB_FNS: readonly IrLibFn[] = [
"dyn.builtinClass",
"dyn.builtinInstanceOf",
"dyn.functionInstanceOf",
];
/** Entry points that link the symbol unit, besides the sym. and disposable. families. */
const SYMBOL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
// EventEmitter's kShapeMode and kCapture properties
...BUILTIN_CLASS_LIB_FNS,
"dyn.usingDispose",
"dyn.usingMethod",
"util.isDeepStrictEqual",
@@ -519,9 +532,16 @@ const RUNTIME_FEATURE_RULES = {
(node.name === "promise.allSettled" || node.name === "promise.any")) ||
(node.kind === "dynFrom" && boxesPromise(node.value.type, tables(), new Set())),
}),
/** EventEmitter and the stream classes as untyped constructor values
* (scr_dyn_builtin_class.c), which call listeners through the dyn-invoke
* unit, render unhandled 'error' payloads through the inspect unit and
* create EventEmitter's private symbols through the symbol unit. */
dynBuiltinClass: rule({
fns: [...BUILTIN_CLASS_LIB_FNS],
}),
dynInvoke: rule({
initial: (mod) => mod.workers === true,
fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS],
fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS],
// Constant-key reads of Object.prototype member names answer the
// inherited member through scr_dyn_invoke.c.
expr: (node, _tables) =>
@@ -538,7 +558,7 @@ const RUNTIME_FEATURE_RULES = {
emitter: rule({
initial: (mod) =>
mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS),
fns: [...STREAM_LIB_FNS],
fns: [...STREAM_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS],
prefixes: ["emitter.", ...STREAM_LIB_PREFIXES],
}),
/** Subclasses of the runtime EventTarget/Event bases tear down and box
@@ -572,7 +592,13 @@ const RUNTIME_FEATURE_RULES = {
* terminal unit prints completion errors through it when there is a
* completer. */
inspect: rule({
fns: ["global.native", "util.styleText", "crypto.cipherNew", ...UTIL_CALLABLE_LIB_FNS],
fns: [
"global.native",
"util.styleText",
"crypto.cipherNew",
...BUILTIN_CLASS_LIB_FNS,
...UTIL_CALLABLE_LIB_FNS,
],
prefixes: ["insp.", "console.", "test."],
libCall: (node) => node.fn === "rl.terminal" && !isDynUndefined(node.args[3]!),
}),
@@ -678,6 +704,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
dgram: false,
dnsPromises: false,
dynAsync: false,
dynBuiltinClass: false,
dynInvoke: false,
dynTier: false,
emitter: false,
+1
View File
@@ -89,6 +89,7 @@ async function main(): Promise<number> {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
+1
View File
@@ -143,6 +143,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
"bigint",
"searchParams",
"qs",
"dynBuiltinClass",
"parseArgs",
"stream",
"net",
@@ -96,6 +96,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5817-constructor-function-instanceof.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5818-constructor-function-emitter-defaults.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6453-constructor-function-emitter.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6454-constructor-function-stream-bases.cjs"
],
"diags": []
}
@@ -88,6 +88,10 @@ export const OPTIONAL_RUNTIME_SOURCES = [
["scr_util_compare.c", all("parseArgs", "symbol")],
["scr_util_style.c", all("parseArgs", "inspect", "dynAsync")],
["scr_util_callable.c", all("parseArgs", "inspect", "dynAsync", "dynInvoke", "regex")],
// Untyped EventEmitter and stream constructor values call listeners
// through the dyn-invoke unit, inspect unhandled 'error' payloads and
// create EventEmitter's private symbols.
["scr_dyn_builtin_class.c", all("dynBuiltinClass", "inspect", "dynInvoke", "symbol", "emitter")],
["scr_stream.c", "stream"],
// The zlib stream classes reference both the zlib engine and the stream unit.
["scr_zlib_stream.c", all("zlib", "stream")],
+6
View File
@@ -1153,6 +1153,12 @@ ScrDyn *scr_dyn_define_prototype_props(ScrDyn *target, ScrDyn *descs, ScrDyn *pr
* must be static literals (the box never frees them; name may be NULL). */
ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, const char *sig, const char *name);
ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key);
/* EventEmitter and the stream constructors as untyped constructor values,
* and instanceof over untyped constructor functions
* (scr_dyn_builtin_class.c, linked with the dynBuiltinClass feature). */
ScrDyn *scr_dyn_builtin_class(double kind);
bool scr_dyn_builtin_instanceof(ScrDyn *value, double kind);
bool scr_dyn_function_instanceof(ScrDyn *value, ScrDyn *callee);
bool scr_dyn_class_is_key(const ScrDyn *value, const ScrStr *type_key);
bool scr_dyn_class_is(const ScrDyn *value, const char *type_key);
/* Calls a dyn value: a non-function kind throws the catchable TypeError
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,15 @@
// `this instanceof F` in a constructor function, in a program that uses no
// EventEmitter or stream constructor value: the call-without-new idiom
// returns a new instance, and instanceof follows the function's prototype.
"use strict";
function Glob(pattern) {
if (!(this instanceof Glob)) return new Glob(pattern);
this.pattern = pattern;
}
Glob.prototype.describe = function () {
return `glob ${this.pattern}`;
};
const called = Glob("*.ts");
const constructed = new Glob("*.js");
console.log(called.describe(), constructed.describe());
console.log(called instanceof Glob, {} instanceof Glob, Object.create(Glob.prototype) instanceof Glob);
@@ -0,0 +1,53 @@
// EventEmitter.defaultMaxListeners read and written through the untyped
// EventEmitter constructor (Node's accessor and its validation), and a
// legacy pipe whose destination emits 'close' synchronously from end().
"use strict";
const util = require("util");
const EventEmitter = require("events");
const Stream = require("stream");
function Bus() {
EventEmitter.call(this);
}
util.inherits(Bus, EventEmitter);
console.log(EventEmitter.defaultMaxListeners);
console.log(new Bus().getMaxListeners());
EventEmitter.defaultMaxListeners = 3;
console.log(EventEmitter.defaultMaxListeners, new Bus().getMaxListeners());
for (const value of [-1, "4", Number.NaN]) {
try {
EventEmitter.defaultMaxListeners = value;
} catch (error) {
console.log(error.name, error.code, error.message);
}
}
console.log(EventEmitter.defaultMaxListeners);
EventEmitter.defaultMaxListeners = 10;
function Source() {
Stream.call(this);
this.readable = true;
}
util.inherits(Source, Stream);
function Sink() {
Stream.call(this);
this.writable = true;
}
util.inherits(Sink, Stream);
Sink.prototype.write = function (chunk) {
console.log("write", chunk);
return true;
};
Sink.prototype.end = function () {
console.log("sink end");
this.emit("close");
};
const source = new Source();
const sink = new Sink();
source.pipe(sink);
source.emit("data", "a");
console.log("emit end", source.emit("end"));
console.log("listeners", source.listenerCount("end"), source.listenerCount("data"), sink.listenerCount("close"));
@@ -0,0 +1,114 @@
// Constructor functions that inherit from EventEmitter the pre-class way:
// util.inherits and EventEmitter.call(this), the call-without-new guard,
// prototype methods that emit, and the listener API over the instance.
"use strict";
const util = require("util");
const { EventEmitter } = require("events");
util.inherits(Bus, EventEmitter);
function Bus(name) {
if (!(this instanceof Bus)) return new Bus(name);
EventEmitter.call(this);
this.name = name;
this.entries = [];
}
Bus.prototype.add = function (entry) {
this.entries.push(entry);
this.emit("added", entry, this.entries.length);
return this;
};
// A second level: Sub inherits Bus, and Bus's constructor runs through call.
function Sub(name) {
Bus.call(this, name);
this.sub = true;
}
util.inherits(Sub, Bus);
Sub.prototype.add = function (entry) {
return Bus.prototype.add.call(this, entry.toUpperCase());
};
// Object.setPrototypeOf instead of util.inherits.
function Legacy() {
EventEmitter.call(this);
}
Object.setPrototypeOf(Legacy.prototype, EventEmitter.prototype);
function main() {
const bus = Bus("main");
console.log(bus instanceof Bus, bus instanceof EventEmitter, Bus.super_ === EventEmitter);
console.log(Object.keys(bus).join(","), bus._eventsCount, bus._maxListeners);
bus.on("added", function (entry, count) {
console.log("added", entry, count, this === bus);
});
bus.once("added", (entry) => console.log("first only", entry));
bus.prependListener("added", () => console.log("prepended"));
bus.prependOnceListener("added", () => console.log("prepended once"));
bus.add("a").add("b");
console.log(bus.listenerCount("added"), bus.eventNames(), bus.getMaxListeners());
const handler = () => console.log("never");
bus.on("x", handler);
bus.on("x", () => console.log("kept"));
bus.off("x", handler);
console.log(bus.emit("x"), bus.emit("nothing"), bus.listeners("x").length, bus.rawListeners("added").length);
try {
bus.emit("error", new Error("boom"));
} catch (err) {
console.log("caught", err.message);
}
try {
bus.emit("error", "plain");
} catch (err) {
console.log("caught", err.code, err.message);
}
bus.on("error", (err) => console.log("handled", err.message));
bus.emit("error", new Error("quiet"));
bus.on("newListener", (event, listener) => console.log("newListener", event, typeof listener));
bus.on("removeListener", (event) => console.log("removeListener", event));
bus.on("later", () => {});
bus.removeAllListeners("added");
console.log(bus.listenerCount("added"), bus.listenerCount("later"));
bus.removeAllListeners();
console.log(bus.eventNames().length);
const sub = new Sub("child");
sub.on("added", (entry) => console.log("sub added", entry));
sub.add("c");
console.log(sub instanceof Sub, sub instanceof Bus, sub instanceof EventEmitter, sub.entries);
const legacy = new Legacy();
console.log("legacy", legacy instanceof EventEmitter, legacy.getMaxListeners());
try {
legacy.setMaxListeners(-1);
} catch (err) {
console.log(err.name, err.code);
}
try {
legacy.on("bad", "not a function");
} catch (err) {
console.log(err.name, err.code);
}
console.log(new Legacy());
// The leak warning goes to stderr with the process id: run it in a child
// and print its output with the id normalized.
const { spawnSync } = require("child_process");
const child = spawnSync(process.execPath, [process.argv[1], "leak"], { encoding: "utf8" });
console.log(child.stdout + child.stderr.replace(/\(node:\d+\)/g, "(node:<pid>)"));
}
function leak() {
const legacy = new Legacy();
legacy.setMaxListeners(1);
legacy.on("many", () => {});
legacy.on("many", () => {});
legacy.on("many", () => {});
console.log("leak", legacy.listenerCount("many"));
setTimeout(() => console.log("leak done"), 1);
}
if (process.argv[2] === "leak") leak();
else main();
@@ -0,0 +1,65 @@
// The legacy Stream constructor as a constructor function's base: its
// EventEmitter state and the legacy pipe between two such streams. The
// stream classes take util.inherits, and instanceof follows the chain.
"use strict";
const util = require("util");
const Stream = require("stream");
const { Readable, Writable, Duplex, Transform, PassThrough } = require("stream");
function Source(items) {
Stream.call(this);
this.items = items;
this.readable = true;
}
util.inherits(Source, Stream);
Source.prototype.start = function () {
for (const item of this.items) this.emit("data", item);
this.emit("end");
};
function Sink() {
Stream.call(this);
this.writable = true;
this.chunks = [];
}
util.inherits(Sink, Stream);
Sink.prototype.write = function (chunk) {
this.chunks.push(chunk);
return true;
};
Sink.prototype.end = function () {
console.log("sink end", this.chunks.join("+"));
};
const source = new Source(["a", "b", "c"]);
const sink = new Sink();
sink.on("pipe", (from) => console.log("piped", from === source));
console.log(source.pipe(sink) === sink, sink.listenerCount("pipe"));
source.start();
console.log("after end", source.listenerCount("data"), sink.listenerCount("drain"));
const kept = new Sink();
const second = new Source(["x"]);
second.pipe(kept, { end: false });
second.start();
console.log("kept open", kept.chunks);
function Reader() {}
util.inherits(Reader, Readable);
function Both() {}
util.inherits(Both, Duplex);
function Shift() {}
util.inherits(Shift, Transform);
function Through() {}
util.inherits(Through, PassThrough);
function Out() {}
util.inherits(Out, Writable);
console.log(Reader.super_ === Readable, Through.super_ === PassThrough, Out.super_ === Writable);
const reader = Object.create(Reader.prototype);
const through = Object.create(Through.prototype);
const out = Object.create(Out.prototype);
const both = Object.create(Both.prototype);
console.log(reader instanceof Readable, reader instanceof Writable);
console.log(through instanceof Transform, through instanceof Duplex, through instanceof Readable);
console.log(out instanceof Writable, out instanceof Readable, both instanceof Readable);
console.log(typeof Shift.prototype.constructor, Shift.prototype instanceof Duplex);
@@ -0,0 +1 @@
{ "plain": 2655, "san": 7944 }
@@ -0,0 +1 @@
{ "plain": 2746, "san": 51663 }
@@ -0,0 +1 @@
{ "plain": 4300, "san": 51474 }
@@ -0,0 +1 @@
{ "plain": 3371, "san": 3844 }
@@ -30,6 +30,7 @@ const features: NativeLinkFeatures = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
@@ -50,6 +50,7 @@ const featureCalls: readonly [string, IrLibFn][] = [
["bigint", "bigint.parse"],
["searchParams", "sp.new"],
["qs", "qs.parse"],
["dynBuiltinClass", "dyn.builtinClass"],
["parseArgs", "util.parseArgs"],
["parseArgs", "util.isDeepStrictEqual"],
["parseArgs", "util.styleText"],
@@ -31,6 +31,7 @@ const base: NativeLinkFeatures = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,