Skip to content

console: print nested String wrapper objects as [String: ...] - #38171

Open
robobun wants to merge 1 commit into
mainfrom
farm/e4d1168f/inspect-nested-string-wrapper
Open

robobun wants to merge 1 commit into
mainfrom
farm/e4d1168f/inspect-nested-string-wrapper

Conversation

@robobun

@robobun robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • A new String(...) wrapper nested inside an object, array, Map or Set is printed by console.log / Bun.inspect as a plain quoted string, while the Number and Boolean wrappers next to it print as wrappers:
    console.log({ n: new Number(3), b: new Boolean(true), s: new String("s") });
    // bun:   s: "s"                 node:   s: [String: 's']
    console.log([new String("arr")]);
    // bun:   [ "arr" ]              node:   [ [String: 'arr'] ]
    Bun.inspect(new String("top"));
    // bun:   "top"                  node (util.inspect):   [String: 'top']
  • console.log(new String("top")) alone was already [String: "top"], so the wrapper rendering exists; it was just unreachable for nested values.
  • Cause: in print_string (src/jsc/ConsoleObject.rs), the if self.quote_strings branch, which every value nested in a container takes (and which Bun.inspect also enables at the top level), JSON-quotes the string and returns before the js_type == StringObject branch below it is reached. Reproduces on released bun 1.4.0 and on main; the Rust port kept the branch order of the original Zig code.

Fix

  • The quoting branch now skips StringObject the same way it already skipped RegExpObject, so a String wrapper falls through to its [String: "..."] branch at every depth. The wrapper branch also adds the width of its decoration to the line-length estimate, like print_double does, so arrays of wrappers wrap at the right point.
  • Correct because the wrapper branch is the rendering Bun already uses for this type at the top level, and the other boxed primitives are already rendered the same way regardless of depth; this only removes the one path that lost the wrapper. Output matches Node apart from Bun's usual double quotes. Primitive strings and RegExps take exactly the branches they took before.
  • class X extends String instances are plain StringObject cells in JSC (DerivedStringObjectType is only used for String.prototype itself, see StringPrototypeInlines.h), so subclass instances are covered. String.prototype is intentionally left as is; console: print built-in prototypes as {} instead of boxed value / method dump #36660 handles it at the tagging layer.
  • Verified:
    • test/js/bun/util/inspect.test.js ("String wrapper objects print as [String: ...] at every depth"): top level, object, array, Map key and value, Set, subclass instance, empty wrapper, Latin-1 and UTF-16 contents with escaping, colors, plus primitive string / RegExp unchanged. Fails on the released binary (Received: ""s""), passes with this change.
    • test/js/web/console/console-log.js + .expected.txt: the console.log form for an object and an array of wrappers next to the existing top-level wrapper lines. Fails on the released binary with s: "Hello", passes with this change.
    • bun bd test test/js/bun/util/inspect.test.js, test/js/bun/console, test/js/web/console, test/js/node/util/{bun-inspect,util}, test/js/bun/test/{expect,jest-extended,expect-label,jest-each} all pass.
  • Related open PRs: console: format boxed primitives and RegExp from their internal slot #36404 changes where print_string reads the wrapper's text from and its test currently asserts the old Bun.inspect(new String("hello")) === '"hello"' output, so whichever of the two lands second needs that assertion updated to [String: "hello"]. console: substitute format specifiers in the first argument only #33445 / console: format boxed primitives and RegExp from their internal slot #36404 also cover the separate issue of a String wrapper first argument being used as the format string; unchanged here.

Background

  • Formatter in src/jsc/ConsoleObject.rs is the native pretty printer shared by console.log, Bun.inspect, bun test failure messages and the error printer. Each value is classified into a Tag; StringObject, RegExpObject and primitive strings all map to Tag::String and are printed by print_string, which then distinguishes them by the value's JSType.
  • quote_strings is a formatter flag: console.log("x") prints a bare x, but once the printer recurses into a container (or when the caller is Bun.inspect) the flag is set so strings print quoted. It is meant to affect primitive strings only; boxed Number / Boolean values are printed by print_double / print_boolean and never consult it.
  • A boxed primitive (new String("s"), new Number(3)) is an object wrapping a primitive; Node's util.inspect renders it as [String: 's'] / [Number: 3] so it cannot be confused with the primitive itself.

The quote_strings branch of print_string ran before the StringObject
branch, so a String wrapper nested in an object, array, Map or Set (or
passed to Bun.inspect at the top level) printed as a plain quoted
string while Number and Boolean wrappers in the same position printed
as [Number: 1] / [Boolean: true].
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e0d91f74-f6f3-4f0c-bd68-16b8a53a7119

📥 Commits

Reviewing files that changed from the base of the PR and between b7a0431 and 7002aec.

📒 Files selected for processing (4)
  • src/jsc/ConsoleObject.rs
  • test/js/bun/util/inspect.test.js
  • test/js/web/console/console-log.expected.txt
  • test/js/web/console/console-log.js

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Status: fix pushed, waiting on CI.

Reproduced on released bun 1.4.0 and on main (b7a0431) with console.log({ s: new String("s") }) printing s: "s" and Bun.inspect([new String("x")]) returning [ "x" ]; Node prints [String: 's'] / [ [String: 'x'] ] for the same input. The new assertions in test/js/bun/util/inspect.test.js and the added lines in test/js/web/console/console-log.js fail on the released binary and pass with this branch.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — minimal, well-scoped fix that lets StringObject fall through the quote_strings guard to its existing [String: "..."] branch, matching how RegExpObject was already excluded and how print_double/print_boolean handle the other boxed primitives.

What was reviewed:

  • Confirmed print_string control flow: primitive strings and RegExps take exactly the same branches as before; only StringObject is redirected.
  • add_for_new_line("[String: \"\"]".len()) mirrors print_double's decoration-width accounting (content length already added at the top of the function).
  • DerivedStringObject handling is unchanged (only String.prototype, per the PR description; subclass instances are StringObject and covered by the MyString test).
  • Tests cover top-level and every container, subclass, empty wrapper, Latin-1/UTF-16 escaping, and the colored instantiation.
Extended reasoning...

Overview

This PR fixes console.log / Bun.inspect output for new String(...) wrapper objects when they appear nested inside a container (object, array, Map, Set) or at the top level of Bun.inspect. Previously they printed as plain quoted strings ("s") instead of [String: "s"], unlike new Number/new Boolean which already printed as wrappers at every depth. The fix is a two-line change to print_string in src/jsc/ConsoleObject.rs: the quote_strings early-return branch now excludes StringObject (alongside the existing RegExpObject exclusion) so it falls through to the pre-existing [String: ...] rendering branch below it, and that branch now contributes its decoration width to the line-length estimate. Tests are added in inspect.test.js and console-log.js/.expected.txt.

Security risks

None. This is purely output formatting in the console/inspect pretty-printer. No parsing of untrusted input, no allocation-size arithmetic, no new FFI or JS re-entry. The value's string content is obtained the same way as before (BunString::from_js at the top of the function) and printed through the same write_json_string / Tag::JSON paths the wrapper branch already used.

Level of scrutiny

Low. The native change is a guard-condition tweak plus one add_for_new_line call, both following the exact pattern already used for RegExpObject and print_double respectively. I traced the surrounding print_string body: primitive strings (quote_strings && !StringObject && !RegExpObject) and RegExps continue on identical paths; only StringObject values, which previously short-circuited into the quoted-primitive rendering, now reach their dedicated branch. DerivedStringObject is not added to the exclusion — consistent with the existing js_type == StringObject check below and with the PR's explanation that subclass instances are StringObject cells (verified by the class MyString extends String test assertion) while DerivedStringObject is reserved for String.prototype (deferred to #36660).

Other factors

Test coverage is thorough: top-level and nested in every container type, subclass instance, empty wrapper, Latin-1 and UTF-16 contents with escape characters, the colored code path, and negative assertions that primitive strings and RegExps keep their previous rendering. The console-log fixture exercises the console.log (non-quote_strings at top level) path with both nested-object and nested-array forms alongside a bare primitive string in the same array. The PR description documents that the new tests fail on the released binary and pass with the change, and that the broader inspect/console/util/expect suites pass. No prior reviewer comments to address.

@robobun

robobun commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:34 AM PT - Aug 13th, 2026

✅ @robobun, your commit 7002aec1e6f47a606f2036a0e640a51a6f1680f4 passed in Build #94646! 🎉


🧪   To try this PR locally:

bunx bun-pr 38171

That installs a local version of the PR into your bun-38171 executable, so you can run:

bun-38171 --bun

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant