Skip to content

feat: budgeted console formatter (inspect builtin) - #1991

Open
edusperoni wants to merge 1 commit into
feat/primordialsfrom
feat/inspect
Open

feat: budgeted console formatter (inspect builtin)#1991
edusperoni wants to merge 1 commit into
feat/primordialsfrom
feat/inspect

Conversation

@edusperoni

@edusperoni edusperoni commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Description

Android mirror of NativeScript/ios#416. Stacked on #1990 (feat/primordials) — review only the last commit.

Replaces the console's logcat formatting pipeline — which could hang the app on a single console.log — with internal/inspect.js, a budgeted util.inspect-lite built on primordials. The DevTools path (inspector formats raw values itself) is untouched.

The problem

  • transformJSObject fell back to JSON.stringify(obj, replacer, 2) with no depth or size limit — logging a store/component tree serialized everything reachable.
  • The replacer's seen array made cycle tracking O(n²) and never popped on subtree exit, so shared acyclic references falsely printed [Circular].
  • The hand-rolled array printer recursed with no depth limit and only caught an array containing itself.
  • JSON.stringify invokes getters and toJSON — a log line could execute arbitrary user code.
  • console.dir had its own duplicate dump with the same issues.

The fix (inspect.js, ported verbatim from iOS — function-body builtin, primordials-clean):

  • Budgets everywhere: depth 2 (dir uses 4), 100 entries per collection, 10k chars per string, 16KB hard cap with an explicit truncation marker.
  • Correct cycles: ancestor Set (enter/exit) — true cycles say [Circular], diamonds print normally.
  • Getters never invoked ([Getter]/[Setter] tags), with two guarded exceptions: error.stack, and custom toString overrides — NativeScript core's ViewBase/Observable convention (Button(42)) keeps working, detected via descriptors and invoked guarded + capped.
  • Tamper-immune: brands via captured Object.prototype.toString, Map/Set walked through captured iterator next, sizes via captured accessor getters.
  • Exposed as the internal __inspect global.

binding.getNativeWrapperHint — the Android-specific piece. Java-backed objects render as short hints instead of having their native-backed graphs walked: [java.lang.Object], [java.util.ArrayList], [package java.lang]; Java classes print [Function: java.lang.Object] via their existing SetClassName. Detection reads private symbols and internal fields only — no JNI, no JS execution, cannot throw. Two hazards handled along the way:

  • InternalFieldCount() == 2 alone false-positives on typed arrays/ArrayBuffers (V8 gives them the same embedder-field count); the hint additionally requires the field to hold a real External.
  • Package objects expose their children as native data properties whose getters getOwnPropertyDescriptor would invokeconsole.log(java) would have materialized every class in every package. Packages now carry a private marker and short-circuit to [package …].

Deleted: smart-stringify.js, GetSmartJSONStringifyFunction, JsonStringifyObject (console was its only caller), transformJSObject, the array printer, and dir's duplicate dump. If the formatter ever fails to initialize, logging degrades to ToDetailString.

Output changes to be aware of: logs now look like Node's inspect output ({ a: 1, self: [Circular] }, Map(2) { … }, truncation markers) instead of pretty-printed JSON; console.dir keeps its dump markers but prints one budgeted rendering at depth 4; Java arrays render as a hint instead of being walked.

Related Pull Requests

Does your pull request have unit tests?

Yes — 14 new device specs (tests/testInspect.js): cycles vs diamonds, caps/truncation, getter non-invocation, native-wrapper hints on real Java objects, tampered-prototype formatting, and a bounded-time regression (console.log of a ~5000-node cyclic graph). Full suite: 627 specs, 0 failures (613 baseline + 14).

console.* now formats objects through internal/inspect.js, a
util.inspect-lite compiled once per isolate: depth 2 (console.dir 4),
100 entries per collection, 10k characters per string and a 16KB hard
cap, so no single log call can hang the app on a large or cyclic graph.
Cycle detection tracks in-progress ancestors only, so shared acyclic
references still print in full and only true cycles say [Circular].

Getters are never invoked - accessors render as [Getter]/[Setter] - with
two guarded exceptions: error.stack, and a custom toString override,
which keeps NativeScript core's Button(42) convention working. All
intrinsic access goes through primordials and brands come from a
captured Object.prototype.toString, so a tampered prototype cannot
change or break the output.

binding.getNativeWrapperHint is the Android-specific piece: objects
backed by a Java counterpart render as a short [java.lang.Object] hint
instead of having their native-backed graph walked, and package objects
render as [package java.lang] rather than materializing every class they
contain. Detection reads private symbols and internal fields only, so it
never runs JS and cannot throw.

This replaces the JSON-based path: smart-stringify.js, JsonStringifyObject
and transformJSObject are gone, along with console's hand-rolled array
printer and console.dir's duplicate dump logic. Logs now look like Node
inspect output instead of pretty-printed JSON. If the formatter fails to
initialize, logging degrades to ToDetailString rather than throwing.

Mirrors NativeScript/ios#416.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 726d3cd6-0115-45c2-83d2-9ead58098015

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant