Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,11 @@ ignorePatterns = [
# `bun lint` lints src/js.
config = "oxlint.json"
ignorePatterns = ["/*", "!/src", "/src/*", "!/src/js"]

[lint.rules]
# A program can delete the `__proto__` accessor of Object.prototype. Use $getPrototypeOf(value).
"no-proto" = "error"
"no-restricted-syntax" = [
"error",
{ selector = "AssignmentExpression[left.type='MemberExpression']:matches([left.property.name='prototype'], [left.property.value='prototype'])", message = "A function in a built-in module has no `prototype` of its own, and an assignment creates an enumerable, configurable one with no `constructor`. Use $toClass(fn, name, base)." },
]
8 changes: 6 additions & 2 deletions src/js/builtins.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,14 +589,15 @@ declare function $ERR_HTTP2_PING_CANCEL(): Error;
*
* This does:
* - Sets the name of the function to the given name
* - Sets .prototype to Object.create(base?.prototype, { constructor: { value: fn } })
* - Defines .prototype, not enumerable and not configurable: `prototype`, or Object.create(base?.prototype, { constructor: { value: fn } })
* - Calls Object.setPrototypeOf(fn, base ?? Function.prototype)
*
* @param fn - The function to convert to a class
* @param name - The name of the class
* @param base - The base class to inherit from
* @param prototype - An existing object to use as fn.prototype. It is not written to.
*/
declare function $toClass(fn: Function, name: string, base?: Function | undefined | null);
declare function $toClass(fn: Function, name: string, base?: Function | undefined | null, prototype?: object);

declare function $min(a: number, b: number): number;

Expand All @@ -617,6 +618,9 @@ interface ObjectConstructor {
/** gets a property on an object */
declare function $getByIdDirect<T, K extends keyof T>(obj: T, key: K): T[K];

/** `Object.getPrototypeOf(value)`, as the `op_get_prototype_of` bytecode */
declare function $getPrototypeOf(value: unknown): object | null;

/**
* Gets a private property on an object.
* Translates to the `op_get_by_id_direct` bytecode.
Expand Down
3 changes: 2 additions & 1 deletion src/js/builtins/ConsoleObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,8 @@ export function createConsoleConstructor(console: typeof globalThis.console) {
const kColorInspectOptions = { colors: true };
const kNoColorInspectOptions = {};

Object.defineProperties((Console.prototype = {}), {
$toClass(Console, "Console");
Object.defineProperties(Console.prototype, {
[kBindStreamsEager]: {
...consolePropAttributes,
// Eager version for the Console constructor
Expand Down
3 changes: 1 addition & 2 deletions src/js/builtins/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,7 @@ export function createBunShellTemplateFunction(createShellInterpreter_, createPa
return Shell;
}

Shell.prototype = ShellPrototype.prototype;
Object.setPrototypeOf(Shell, ShellPrototype);
$toClass(Shell, "Shell", ShellPrototype, ShellPrototype.prototype);
Object.setPrototypeOf(BunShell, ShellPrototype.prototype);

BunShell[cwdSymbol] = defaultCwd;
Expand Down
13 changes: 13 additions & 0 deletions src/js/internal-for-testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1003,3 +1003,16 @@ export const internalModuleBytecode: {
(index: number): { name: string; bytecode: Uint8Array; strings: Uint8Array } | null;
(source: string, name: string): { name: string; bytecode: Uint8Array; strings: Uint8Array };
} = $newCppFunction("InternalModuleRegistry.cpp", "jsInternalModuleBytecode", 2);

// What $toClass installs on a new function of a builtin module. `readNameFirst` reifies the lazy `name` before the call.
export function toClassForTesting(
name: string,
base?: unknown,
prototype?: unknown,
readNameFirst?: boolean,
): Function {
function Target() {}
if (readNameFirst && Target.name !== "Target") throw new Error("unreachable");
$toClass(Target, name, base as Function, prototype as object);
return Target;
}
2 changes: 1 addition & 1 deletion src/js/internal/streams/readable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ function ReadableState(options, stream, isDuplex) {
this.encoding = options.encoding;
}
}
ReadableState.prototype = {};
$toClass(ReadableState, "ReadableState");
ObjectDefineProperties(ReadableState.prototype, {
objectMode: makeBitMapDescriptor(kObjectMode),
ended: makeBitMapDescriptor(kEnded),
Expand Down
2 changes: 1 addition & 1 deletion src/js/internal/streams/writable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function makeBitMapDescriptor(bit) {
},
};
}
WritableState.prototype = {};
$toClass(WritableState, "WritableState");
ObjectDefineProperties(WritableState.prototype, {
// Object stream flag to indicate whether or not this stream
// contains buffers or objects.
Expand Down
2 changes: 1 addition & 1 deletion src/js/internal/test_runner/mock_timers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ class MockTimers {
},
});

MockDate.prototype = NativeDateConstructor.prototype;
$toClass(MockDate, "Date", undefined, NativeDateConstructor.prototype);
MockDate.parse = NativeDateConstructor.parse;
MockDate.UTC = NativeDateConstructor.UTC;
MockDate.prototype.toUTCString = NativeDateConstructor.prototype.toUTCString;
Expand Down
11 changes: 1 addition & 10 deletions src/js/node/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,7 @@ function Assert(options) {
}
}

// Functions compiled as builtins have no automatic `.prototype`; assign one
// explicitly (same pattern as EventEmitter in node/events.ts).
Assert.prototype = {};
ObjectDefineProperty(Assert.prototype, "constructor", {
__proto__: null,
value: Assert,
writable: true,
enumerable: false,
configurable: true,
});
$toClass(Assert, "Assert");

// All of the following functions must throw an AssertionError
// when a corresponding condition is not met, with a message that
Expand Down
2 changes: 1 addition & 1 deletion src/js/node/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ function Certificate(): void {
this.exportPublicKey = exportPublicKey;
this.exportChallenge = exportChallenge;
}
Certificate.prototype = {};
$toClass(Certificate, "Certificate");
Certificate.verifySpkac = verifySpkac;
Certificate.exportPublicKey = exportPublicKey;
Certificate.exportChallenge = exportChallenge;
Expand Down
8 changes: 3 additions & 5 deletions src/js/node/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ declare class EventEmitter {

// EventEmitter must be a standard function because some old code will do weird tricks like `EventEmitter.$apply(this)`.
function EventEmitter(opts) {
if (this._events === undefined || this._events === this.__proto__._events) {
if (this._events === undefined || this._events === ($getPrototypeOf(this) as EventEmitter)._events) {
this._events = Object.create(null);
this._eventsCount = 0;
this[kShapeMode] = false;
Expand All @@ -127,8 +127,8 @@ function EventEmitter(opts) {
}
}
}
Object.defineProperty(EventEmitter, "name", { value: "EventEmitter", configurable: true });
const EventEmitterPrototype = (EventEmitter.prototype = {} as EventEmitter);
$toClass(EventEmitter, "EventEmitter");
const EventEmitterPrototype = EventEmitter.prototype;

EventEmitterPrototype.setMaxListeners = function setMaxListeners(n) {
validateNumber(n, "setMaxListeners", 0);
Expand All @@ -137,8 +137,6 @@ EventEmitterPrototype.setMaxListeners = function setMaxListeners(n) {
};
Object.defineProperty(EventEmitterPrototype.setMaxListeners, "name", { value: "setMaxListeners" });

EventEmitterPrototype.constructor = EventEmitter;

EventEmitterPrototype.getMaxListeners = function getMaxListeners() {
return _getMaxListeners(this);
};
Expand Down
9 changes: 1 addition & 8 deletions src/js/node/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -975,14 +975,7 @@ function validateStringOrSymbol(value: unknown, name: string) {
// default original is a plain `function () {}`, so give it one explicitly.
function createDefaultOriginal(): Function {
const original = function () {};
Object.defineProperty(original, "prototype", {
// @ts-ignore
__proto__: null,
value: {},
writable: true,
enumerable: false,
configurable: false,
});
$toClass(original, "original");
return original;
}

Expand Down
2 changes: 1 addition & 1 deletion src/js/node/tls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ var InternalSecureContext = class SecureContext {
function SecureContext(options): void {
return createSecureContext(options) as never;
}
SecureContext.prototype = InternalSecureContext.prototype;
$toClass(SecureContext, "SecureContext", undefined, InternalSecureContext.prototype);
InternalSecureContext.prototype.constructor = SecureContext;

function createSecureContext(options) {
Expand Down
139 changes: 63 additions & 76 deletions src/js/node/tty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,87 +28,68 @@ function ReadStream(fd): void {
this.isTTY = isatty(fd);
}
$toClass(ReadStream, "ReadStream", fs.ReadStream);

Object.defineProperty(ReadStream, "prototype", {
get() {
const Prototype = Object.create(fs.ReadStream.prototype);

// Add ref/unref methods to make tty.ReadStream behave like Node.js
// where TTY streams have socket-like behavior
Prototype.ref = function () {
// Get the underlying native stream source if available
const source = this.$bunNativePtr;
if (source?.updateRef) {
source.updateRef(true);
}
return this;
};

Prototype.unref = function () {
// Get the underlying native stream source if available
const source = this.$bunNativePtr;
if (source?.updateRef) {
source.updateRef(false);
const ReadStreamPrototype = ReadStream.prototype;

// In Node.js a TTY stream has ref() and unref(), like a socket.
ReadStreamPrototype.ref = function () {
// Get the underlying native stream source if available
const source = this.$bunNativePtr;
if (source?.updateRef) {
source.updateRef(true);
}
return this;
};

ReadStreamPrototype.unref = function () {
// Get the underlying native stream source if available
const source = this.$bunNativePtr;
if (source?.updateRef) {
source.updateRef(false);
}
return this;
};

ReadStreamPrototype.setRawMode = function (flag) {
flag = !!flag;

// Windows calls uv_tty_set_mode on the stream's uv_tty_t. POSIX uses termios on the fd: the handle was not reliable there.
if (process.platform === "win32") {
// stdin has a shared uv_tty handle, and its stream is constructed differently.
if (this.fd === 0) {
const err = ttySetMode(flag);
if (err) {
this.emit("error", new Error("setRawMode failed with errno: " + err));
return this;
}
return this;
};

Prototype.setRawMode = function (flag) {
flag = !!flag;

// On windows, this goes through the stream handle itself, as it must call
// uv_tty_set_mode on the uv_tty_t.
//
// On POSIX, I tried to use the same approach, but it didn't work reliably,
// so we just use the file descriptor and use termios APIs directly.
if (process.platform === "win32") {
// Special case for stdin, as it has a shared uv_tty handle
// and it's stream is constructed differently
if (this.fd === 0) {
const err = ttySetMode(flag);
if (err) {
this.emit("error", new Error("setRawMode failed with errno: " + err));
return this;
}
} else {
const handle = this.$bunNativePtr;
if (!handle) {
this.emit("error", new Error("setRawMode failed because it was called on something that is not a TTY"));
return this;
}

// If you call setRawMode before you call on('data'), the stream will
// not be constructed, leading to EBADF
// This corresponds to the `ensureConstructed` function in `native-readable.ts`
this.$start();

const err = handle.setRawMode(flag);
if (err) {
this.emit("error", err);
return this;
}
}
} else {
const state = (this[kRawModeState] ??= new Uint8Array(rawModeStateSize));
const err = ttySetMode(this.fd, flag, state);
if (err) {
this.emit("error", new Error("setRawMode failed with errno: " + err));
return this;
}
} else {
const handle = this.$bunNativePtr;
if (!handle) {
this.emit("error", new Error("setRawMode failed because it was called on something that is not a TTY"));
return this;
}

this.isRaw = flag;
// setRawMode before on('data') finds the stream not constructed (EBADF). See ensureConstructed in native-readable.ts.
this.$start();

const err = handle.setRawMode(flag);
if (err) {
this.emit("error", err);
return this;
}
}
} else {
const state = (this[kRawModeState] ??= new Uint8Array(rawModeStateSize));
const err = ttySetMode(this.fd, flag, state);
if (err) {
this.emit("error", new Error("setRawMode failed with errno: " + err));
return this;
};
}
}

Object.defineProperty(ReadStream, "prototype", { value: Prototype });
this.isRaw = flag;

return Prototype;
},
enumerable: true,
configurable: true,
});
return this;
};

function WriteStream(fd): void {
if (!(this instanceof WriteStream)) return new WriteStream(fd);
Expand All @@ -132,7 +113,13 @@ function WriteStream(fd): void {
Object.defineProperty(WriteStream, "prototype", {
get() {
const Real = fs.WriteStream.prototype;
Object.defineProperty(WriteStream, "prototype", { value: Real });
// $toClass takes a function that has no `prototype` yet, and this one has the accessor.
Object.defineProperty(WriteStream, "prototype", {
value: Real,
writable: true,
enumerable: false,
configurable: false,
});

WriteStream.prototype._refreshSize = function () {
const oldCols = this.columns;
Expand Down Expand Up @@ -197,7 +184,7 @@ Object.defineProperty(WriteStream, "prototype", {

return Real;
},
enumerable: true,
enumerable: false,
configurable: true,
});

Expand Down
5 changes: 4 additions & 1 deletion src/js/node/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ function Url() {
this.path = null;
this.href = null;
}
Url.prototype = {} as Url;
declare namespace Url {
export let prototype: Url;
}
$toClass(Url, "Url");

// Reference: RFC 3986, RFC 1808, RFC 2396

Expand Down
Loading
Loading