Skip to content

Commit 6eb6ebd

Browse files
proggeramlugRalph Küpper
andauthored
fix(util): quote top-level strings in util.format %o/%O (Node parity) (#5106)
`util.format("%o", "hello")` / `console.log("%o", str)` printed the string unquoted (`hello`) where Node renders it through `util.inspect` (`'hello'`). The `%o`/`%O` arms called `format_jsvalue(val, 0)`, which mirrors the bare `console.log(str)` behavior of leaving a *top-level* string unquoted — but `%o`/`%O` should inspect the value, and inspect quotes strings at every depth (nested strings already quoted correctly, e.g. `%o` of `{a:"x"}` → `{ a: 'x' }`). Route the `%o`/`%O` argument through a new `inspect_format_arg` helper that mirrors `js_util_inspect`'s top-level handling: a primitive string becomes `'<escaped>'`; everything else goes through `format_jsvalue`. Verified vs Node v26.3.0: `%o`/`%O` of a string now yield `'hello'`; numbers/objects/arrays/null/bool unchanged; `%s` still coerces without quotes. Existing formatting/inspect unit tests pass (12). (Out of scope: Node's `%o` also sets `showHidden`, so an array prints `[ 1, 'two', [length]: 2 ]`; matching the `[length]` internal-slot rendering is a separate showHidden-array feature and left as-is.) Co-authored-by: Ralph Küpper <ralph2@skelpo.com>
1 parent 088afba commit 6eb6ebd

1 file changed

Lines changed: 18 additions & 2 deletions

File tree

crates/perry-runtime/src/builtins/formatting/util_format.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,22 @@ unsafe fn util_format_json_object_has_cycle(ptr: *const u8, stack: &mut Vec<usiz
112112
found
113113
}
114114

115+
/// Render a `%o`/`%O` argument the way `util.inspect` does at the top
116+
/// level: a primitive string is **quoted** (`'hello'`), matching
117+
/// `js_util_inspect`. `format_jsvalue` alone leaves a top-level string
118+
/// unquoted (the `console.log(str)` bare-string behavior), so calling it
119+
/// directly for `%o`/`%O` diverged from Node, which routes the value
120+
/// through `util.inspect` (strings quoted at every depth).
121+
fn inspect_format_arg(val: f64) -> String {
122+
let jv = crate::value::JSValue::from_bits(val.to_bits());
123+
if jv.is_any_string() {
124+
let s = jsvalue_string_content(val).unwrap_or_default();
125+
format!("'{}'", escape_string(&s))
126+
} else {
127+
format_jsvalue(val, 0)
128+
}
129+
}
130+
115131
#[no_mangle]
116132
pub extern "C" fn js_util_format(arr_ptr: *const crate::array::ArrayHeader) -> f64 {
117133
use crate::value::JSValue;
@@ -324,12 +340,12 @@ pub extern "C" fn js_util_format(arr_ptr: *const crate::array::ArrayHeader) -> f
324340
let _depth_guard = InspectDepthLimitGuard::new(4);
325341
let _hidden_guard = InspectShowHiddenGuard::new(true);
326342
let _proxy_guard = InspectShowProxyGuard::new(true);
327-
out.push_str(&format_jsvalue(val, 0));
343+
out.push_str(&inspect_format_arg(val));
328344
}
329345
b'O' => {
330346
// `%O` keeps the default depth cap (2) — matching
331347
// Node's `util.inspect` default options.
332-
out.push_str(&format_jsvalue(val, 0));
348+
out.push_str(&inspect_format_arg(val));
333349
}
334350
b'c' => {
335351
// Browser/Node console style marker. Consume the CSS

0 commit comments

Comments
 (0)