Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
58 changes: 34 additions & 24 deletions src/js/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,12 +344,19 @@ function isURL(value) {

const SymbolToPrimitive = Symbol.toPrimitive;

const builtInObjects = new SafeSet(
ArrayPrototypeFilter(
ObjectGetOwnPropertyNames(globalThis),
e => RegExpPrototypeExec(/^[A-Z][a-zA-Z0-9]+$/, e) !== null,
),
);
// Frozen to the names Node v26 sees on globalThis at bootstrap, before host
// globals (Buffer, URL, SharedArrayBuffer, Float16Array, ...) are installed.
// prettier-ignore
Comment thread
robobun marked this conversation as resolved.
Outdated
const builtInObjects = new SafeSet([
"AggregateError", "Array", "ArrayBuffer", "Atomics", "BigInt", "BigInt64Array",
"BigUint64Array", "Boolean", "DataView", "Date", "Error", "EvalError",
"FinalizationRegistry", "Float32Array", "Float64Array", "Function", "Infinity",
"Int16Array", "Int32Array", "Int8Array", "Intl", "Iterator", "JSON", "Map",
"Math", "NaN", "Number", "Object", "Promise", "Proxy", "RangeError",
"ReferenceError", "Reflect", "RegExp", "Set", "String", "Symbol", "SyntaxError",
"TypeError", "URIError", "Uint16Array", "Uint32Array", "Uint8Array",
"Uint8ClampedArray", "WeakMap", "WeakRef", "WeakSet",
]);

// https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot
const isUndetectableObject = v => typeof v === "undefined" && v !== undefined;
Expand Down Expand Up @@ -2779,6 +2786,24 @@ function formatBigIntNoColor(bigint, options) {
return formatBigInt(stylizeNoColor, bigint, options?.numericSeparator ?? inspectDefaultOptions.numericSeparator);
}

function formatPercentS(inspectOptions, arg) {
if (typeof arg === "number") {
return formatNumberNoColor(arg, inspectOptions);
}
if (typeof arg === "bigint") {
return formatBigIntNoColor(arg, inspectOptions);
}
if (typeof arg !== "object" || arg === null || !hasBuiltInToString(arg)) {
return String(arg);
}
return inspect(arg, {
...inspectOptions,
compact: 3,
colors: false,
depth: 0,
});
}

function formatWithOptionsInternal(inspectOptions, args) {
const first = args[0];
let a = 0;
Expand All @@ -2798,25 +2823,9 @@ function formatWithOptionsInternal(inspectOptions, args) {
const nextChar = StringPrototypeCharCodeAt(first, ++i);
if (a + 1 !== args.length) {
switch (nextChar) {
case 115: {
// 's'
const tempArg = args[++a];
if (typeof tempArg === "number") {
tempStr = formatNumberNoColor(tempArg, inspectOptions);
} else if (typeof tempArg === "bigint") {
tempStr = formatBigIntNoColor(tempArg, inspectOptions);
} else if (typeof tempArg !== "object" || tempArg === null || !hasBuiltInToString(tempArg)) {
tempStr = String(tempArg);
} else {
tempStr = inspect(tempArg, {
...inspectOptions,
compact: 3,
colors: false,
depth: 0,
});
}
case 115: // 's'
tempStr = formatPercentS(inspectOptions, args[++a]);
break;
}
case 106: // 'j'
tempStr = tryStringify(args[++a]);
break;
Expand Down Expand Up @@ -2995,6 +3004,7 @@ export default {
inspect,
format,
formatWithOptions,
formatPercentS,
getStringWidth,
stripVTControlCharacters,
//! non-standard properties, should these be kept? (not currently exposed)
Expand Down
33 changes: 26 additions & 7 deletions src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2526,12 +2526,7 @@ pub mod formatter {
const MIN_BEFORE_E_NOTATION: f64 = 0.000001;
match token {
PercentTag::S => {
self.print_as::<ENABLE_ANSI_COLORS>(
Tag::String,
writer_,
next_value,
next_value.js_type(),
)?;
self.print_percent_s(writer_, next_value)?;
Comment thread
robobun marked this conversation as resolved.
writer = WrappedWriter {
ctx: writer_,
failed: false,
Expand Down Expand Up @@ -3318,6 +3313,9 @@ pub mod formatter {
max_depth: u32,
colors: bool,
) -> JSValue;

/// `formatPercentS` from `internal/util/inspect.js` (see `UtilInspect.cpp`).
safe fn Bun__callFormatPercentS(global: &JSGlobalObject, arg: JSValue) -> JSValue;
}

// ───────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -3646,7 +3644,6 @@ pub mod formatter {
value: JSValue,
js_type: jsc::JSType,
) -> JsResult<()> {
// This is called from the '%s' formatter, so it can actually be any value
use crate::StringJsc as _;
let str = OwnedString::new(BunString::from_js(value, self.global_this)?);
let mut writer = WrappedWriter {
Expand Down Expand Up @@ -3891,6 +3888,28 @@ pub mod formatter {
Ok(())
}

#[inline(never)]
fn print_percent_s(
&mut self,
writer_: &mut dyn bun_io::Write,
value: JSValue,
) -> JsResult<()> {
let result = if value.is_string_literal() {
value
} else {
crate::from_js_host_call(self.global_this, || {
Bun__callFormatPercentS(self.global_this, value)
})?
};
if writer_
.write_fmt(format_args!("{}", result.fmt_string(self.global_this)))
.is_err()
{
self.failed = true;
}
Ok(())
}

#[inline(never)]
fn print_custom_formatted_object<const C: bool>(
&mut self,
Expand Down
19 changes: 19 additions & 0 deletions src/jsc/bindings/UtilInspect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,23 @@ extern "C" JSC::EncodedJSValue JSC__JSValue__callCustomInspectFunction(
RELEASE_AND_RETURN(scope, JSValue::encode(inspectRet));
}

extern "C" JSC::EncodedJSValue Bun__callFormatPercentS(
Zig::GlobalObject* globalObject,
JSC::EncodedJSValue encodedArg)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);

JSFunction* fn = globalObject->utilInspectFormatPercentSFunction();
RETURN_IF_EXCEPTION(scope, {});

MarkedArgumentBuffer arguments;
arguments.append(jsUndefined());
arguments.append(JSValue::decode(encodedArg));

auto result = JSC::profiledCall(globalObject, ProfilingReason::API, fn, JSC::getCallData(fn), jsUndefined(), arguments);
RETURN_IF_EXCEPTION(scope, {});
RELEASE_AND_RETURN(scope, JSValue::encode(result));
}

}
12 changes: 12 additions & 0 deletions src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2291,6 +2291,18 @@ void GlobalObject::finishCreation(VM& vm)
init.set(uncheckedDowncast<JSFunction>(prop));
});

m_utilInspectFormatPercentSFunction.initLater(
[](const Initializer<JSFunction>& init) {
auto scope = DECLARE_THROW_SCOPE(init.vm);
JSValue mod = uncheckedDowncast<Zig::GlobalObject>(init.owner)->internalModuleRegistry()->requireId(init.owner, init.vm, Bun::InternalModuleRegistry::Field::InternalUtilInspect);
RETURN_IF_EXCEPTION(scope, );
RELEASE_ASSERT(mod.isObject());
auto prop = mod.getObject()->getIfPropertyExists(init.owner, Identifier::fromString(init.vm, "formatPercentS"_s));
RETURN_IF_EXCEPTION(scope, );
ASSERT(prop);
init.set(uncheckedDowncast<JSFunction>(prop));
});

m_utilInspectOptionsStructure.initLater(
[](const Initializer<Structure>& init) {
init.set(Bun::createUtilInspectOptionsStructure(init.vm, init.owner));
Expand Down
2 changes: 2 additions & 0 deletions src/jsc/bindings/ZigGlobalObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ class GlobalObject : public Bun::GlobalScope {

JSC::Structure* utilInspectOptionsStructure() const { return m_utilInspectOptionsStructure.getInitializedOnMainThread(this); }
JSC::JSFunction* utilInspectFunction() const { return m_utilInspectFunction.getInitializedOnMainThread(this); }
JSC::JSFunction* utilInspectFormatPercentSFunction() const { return m_utilInspectFormatPercentSFunction.getInitializedOnMainThread(this); }
JSC::JSFunction* utilInspectStylizeColorFunction() const { return m_utilInspectStylizeColorFunction.getInitializedOnMainThread(this); }
JSC::JSFunction* utilInspectStylizeNoColorFunction() const { return m_utilInspectStylizeNoColorFunction.getInitializedOnMainThread(this); }

Expand Down Expand Up @@ -600,6 +601,7 @@ class GlobalObject : public Bun::GlobalScope {
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_nativeMicrotaskTrampoline) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_performMicrotaskVariadicFunction) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_utilInspectFunction) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_utilInspectFormatPercentSFunction) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_utilInspectOptionsStructure) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_utilInspectStylizeColorFunction) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_utilInspectStylizeNoColorFunction) \
Expand Down
19 changes: 19 additions & 0 deletions test/js/node/util/node-inspect-tests/parallel/util-format.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,25 @@ test("no assertion failures", () => {
assert.strictEqual(util.format("%s", { __proto__: null }), "[Object: null prototype] {}");
}

// `%s` with values whose inherited `toString` comes from a host global
// (Buffer, URL, ...) must call String(value), not inspect it.
assert.strictEqual(util.format("%s", Buffer.from("ab")), "ab");
assert.strictEqual(util.format("%s", Buffer.from([0xe2, 0x82, 0xac])), "\u20ac");
assert.strictEqual(util.format("%s", new URL("http://a/b")), "http://a/b");
assert.strictEqual(util.format("%s", new URLSearchParams("a=1&b=2")), "a=1&b=2");
{
class MyBuffer extends Buffer {}
const sub = new MyBuffer(2);
sub[0] = 0x68;
sub[1] = 0x69;
assert.strictEqual(util.format("%s", sub), "hi");
}
// Uint8Array inherits toString from %TypedArray%.prototype, a language built-in.
assert.strictEqual(util.format("%s", new Uint8Array([65])), "65");
// Language built-ins with their own toString keep the inspect path.
assert.strictEqual(util.format("%s", [1, 2]), "[ 1, 2 ]");
assert.strictEqual(util.format("%s", new Map([["k", "v"]])), "Map(1) { 'k' => 'v' }");

// JSON format specifier
assert.strictEqual(util.format("%j"), "%j");
assert.strictEqual(util.format("%j", 42), "42");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
// USE OR OTHER DEALINGS IN THE SOFTWARE.

import assert from "assert";
import { isWindows } from "harness";
import { isWindows, isDebug } from "harness";
import util, { inspect } from "util";
import vm from "vm";
import { MessageChannel } from "worker_threads";
Expand Down Expand Up @@ -236,9 +236,15 @@ test("inspect from a different context", () => {
);*/
});

// The surrogate-pair loop below runs ~10k util.inspect calls, which exceeds the
// default timeout on debug+ASAN builds; prettier-ignore keeps the 3rd test() arg
// from forcing a whole-body reindent.
// prettier-ignore
test("no assertion failures 2", () => {
// Float16Array is intentionally absent: it is not in Node's bootstrap-time
// `builtInObjects`, so showHidden inspects its prototype getters and the
// output diverges from the other typed arrays in Node as well.
[
Float16Array,
Float32Array,
Float64Array,
Int16Array,
Expand Down Expand Up @@ -270,7 +276,6 @@ test("no assertion failures 2", () => {

// Now check that declaring a TypedArray in a different context works the same.
[
Float16Array,
Float32Array,
Float64Array,
Int16Array,
Expand Down Expand Up @@ -1811,7 +1816,7 @@ test("no assertion failures 2", () => {
})("a");
assert.strictEqual(util.inspect(args), "[Arguments] { '0': 'a' }");
}
});
}, isDebug ? 30_000 : undefined);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
robobun marked this conversation as resolved.
Outdated

test("util.inspect stack overflow handling", () => {
// Test that a long linked list can be inspected without throwing an error.
Expand Down
Loading
Loading