Repository navigation
Conversation
…y matrix The matrix gets 43 rows for boxed strings: String subclass instances, own properties, Symbol.toStringTag, toString() overrides, String.prototype, and the same values in a Map or a Set. A row that Bun answers differently from Node v26.3.0 has a looseBug or strictBug marker and runs as test.failing. One test pins that Bun.deepEquals rejects a boxed string against a String subclass instance with the same text, as Jest's toEqual() does. The comment on the String arm of specialObjectsDequalSlow said that a String subclass instance is DerivedStringObjectType. Only String.prototype has that type.
|
Status
|
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Boxed-string comparison behavior src/jsc/bindings/bindings.cpp, test/js/node/assert/deep-equal.test.ts, test/js/bun/bun-object/deep-equals.test.ts |
The comment identifies String.prototype as having DerivedStringObjectType. Tests cover boxed-string properties and strict and loose comparisons with String subclasses across direct and nested values, Maps, and Sets. The Bun-specific test expects a boxed string and a subclass instance with matching text to compare as unequal in both directions. |
Suggested reviewers: jarred-sumner
Merge Risk: 
Minimal · up to d26d1
-
No actionable merge-blocking risk is established. The change documents existing boxed-string differences rather than changing comparison behavior; merge after normal CI validation.
Pre-merge checks: 
4
-
Full details: Title check
-
Explanation
The title names the main change: adding Node's expected results for boxed strings to the deep-equality test matrix in node:assert. The changeset adds 44 boxed-string rows to deep-equal.test.ts, adds a deepEquals test, and corrects a comment in bindings.cpp. The title does not mention the comment fix, which is a minor secondary change.
Full details: Description check
-
Explanation
The description follows the template with both required sections, 'What does this PR do?' and 'How did you verify your code works?'. The first section is covered by the Problem, Fix, and Background parts. The second section lists the verification steps: deep-equal.test.ts (610 pass), deep-equals.test.ts (68 pass), and 582 matrix expectations checked under Node v26.3.0. It also states that the bun-object test did not run locally and defers to CI. The description is long and includes extra notes, but it covers the required content.
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
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.
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Additional findings (outside the current diff — GitHub can't attach inline comments there):
-
🟣
src/jsc/bindings/bindings.cpp— Users of assert.deepStrictEqual and util.isDeepStrictEqual get wrong answers or a thrown TypeError for boxed strings whose toString is overridden or whose prototype is null. At src/jsc/bindings/bindings.cpp:2049-2051 the String arm reads each operand with toStringInline, which runs user toString/Symbol.toPrimitive and throws on a null-prototype wrapper, instead of reading the [[StringData]] slot like Node's StringPrototypeValueOf. Fix: in the node modes (checkPrototypes or isStrict) read the wrapper's internal value via jsCast<StringObject*>(c)->internalValue() for both operands, keeping toStringInline only for the Jest-compatible Bun.deepEquals path; un-fail the three rows this PR marks. [also at: test/js/node/assert/deep-equal.test.ts:487 - Callers of util.isDeepStrictEqual get a thrown TypeError, not a boolean, for two null-prototype boxed strings, and this PR pins that as an accepted test.failing row instead of a defect.]Why this was flagged
Trigger: assert.deepStrictEqual(new StringWithConstantToString("xyz"), new StringWithConstantToString("xyw")) or util.isDeepStrictEqual(Object.setPrototypeOf(new String("a"), null), Object.setPrototypeOf(new String("a"), null)). Both reach specialObjectsDequalSlow's StringObjectType arm at src/jsc/bindings/bindings.cpp:2031. Line 2049 and 2051 call toStringInline on each wrapper: for the first pair the overridden toString returns "abc" twice so line 2054 reports equal and deepStrictEqual passes where Node throws ERR_ASSERTION; for the null-prototype pair OrdinaryToPrimitive finds no toString/valueOf and throws TypeError, so util.isDeepStrictEqual throws instead of returning true. The PR records both as strictBug test.failing rows at test/js/node/assert/deep-equal.test.ts:452 and :486 but leaves the defect. Node reads the internal slot (isEqualBoxedPrimitive -> StringPrototypeValueOf), never user code. Remedy: read StringObject::internalValue() in the checkPrototypes/isStrict modes.
Verification: Pre-existing; acknowledged in diff: the new matrix rows pin these outcomes as test.failing in test/js/node/assert/deep-equal.test.ts. At src/jsc/bindings/bindings.cpp:2049-2052 the StringObjectType arm calls toStringInline on the wrapper objects, which looks up user toString/valueOf and throws a TypeError when neither is callable. The base commit has the identical calls, so the base fails by the same route.
…als test The matrix gets the sibling of the toString() row: two instances of one String subclass with different text and the same Symbol.toPrimitive. The test that Bun.deepEquals rejects a boxed string against a String subclass instance moves to test/js/bun/bun-object/deep-equals.test.ts, beside the other tests of Bun.deepEquals.
The comment now states only the fact that the code cannot show: String.prototype is the one object with DerivedStringObjectType.
|
On the finding about The finding is correct, and main has the same behaviour. Node reads the internal slot of a boxed string. Bun reads the text through This PR does not change that read. It declares no behaviour change, and a read of the slot moves results of |
There was a problem hiding this comment.
LGTM — both points from the earlier pass are addressed (the same-class Symbol.toPrimitive row is in the matrix, and the Bun.deepEquals pin now lives in test/js/bun/bun-object/deep-equals.test.ts), and the remaining change is a comment correction plus test rows with no comparator change.
What was reviewed:
- The rewritten comment at
src/jsc/bindings/bindings.cpp:2033: consistent with how theDerivedStringObjectTypefallthrough and thecalculatedClassNamecheck below it behave, and with the existingskipPrototypeclass S extends Stringtest passing via the class-name skip rather than the type-tag branch. - Spot-checked the new
strict/looseexpectations (own-key counts for fields andundefinedprops,Symbol.toStringTagtags, boxed-value comparison fortoStringoverrides, consumed Set/Map member pairing) against Node'sinnerDeepEqual/keyCheckalgorithm; everylooseBug/strictBugmarker matches a path the C++ takes today. - The two
String.prototypestrict rows that were questioned aretest.failingon Bun either way, so they cannot turn CI green or red incorrectly on this PR.
Extended reasoning...
The diff touches three files: a one-line comment rewrite in the StringObjectType arm of specialObjectsDequalSlow in src/jsc/bindings/bindings.cpp (logic unchanged), one new Bun.deepEquals test, and 43 new table-driven rows in the node:assert deep-equality matrix where divergent rows run as test.failing. It touches no security-sensitive surface. Both inline comments from the prior run were addressed by the follow-up commits, no CODEOWNERS entry covers the changed paths, and the bug hunt ran dry with no findings. The approval rests on there being no behaviour change: the only risk is a mis-recorded Node expectation, and the ones that were questioned are already marked as Bun-failing so they cannot affect CI outcome.
Behaviour change: none
Problem
assert.deepEqual(new String("abc"), new S("abc"))(class S extends String {}) throwsERR_ASSERTION. Node v26.3.0 passes it. The matrix intest/js/node/assert/deep-equal.test.tshas no row for it or for the pairs near it.specialObjectsDequalSlow(src/jsc/bindings/bindings.cpp:2033) says that a String subclass instance isDerivedStringObjectType. OnlyString.prototypehas that type.Fix
Symbol.toStringTag,toString()overrides,String.prototype, and Map and Set members.looseBugorstrictBugmarker and runs astest.failing. There are 20 loose and 5 strict. No comparison changes.deep-equal.test.ts(610 pass) anddeep-equals.test.ts(68 pass) on a debug build. All 582 matrix expectations pass under Node v26.3.0.Background
new String("abc")makes. Its JavaScriptCore type isStringObjectType, and a subclass instance has the same type.assert.deepEqualruns the comparison ofBun.deepEquals(a, b)andexpect().toEqual(). Jest'stoEqual()rejects the pair above (expect29.5.0 and 30.5.2), so that comparison keeps its answer. One new test indeep-equals.test.tspins it forBun.deepEquals.errorsof an Error like Node #35495 gives legacyassert.deepEquala native entry of its own. The fix for these rows belongs behind that entry. I built it as a separate comparator mode and did not open it. It duplicates that entry. It also needs each Set member and Map key paired once (deepEquals: compare a same-typed pair once, and pair Set and Map entries as node does #45457).Notes
Repro. Node v26.3.0 prints
passfor each line. Bun printspass,pass,throw.Why
toEqual()andBun.deepEqualskeep their answer.expect(new String("abc")).toEqual(new S("abc"))fails in Jest, and the Number and Boolean pairs pass.toEqual()runsiterableEqualityfirst, and that function rejects two iterables whose constructors differ. A boxed string is iterable. A boxed number is not.test/js/bun/test/expect.test.js:1070has pinned the same answer since #3367.Where the rows come from. Every expectation is the answer of Node v26.3.0. I ran the test file under Node with a stand-in for
bun:test. It runs each matrix test and ignores thetest.failingmarker. 582 of 582 pass (194 rows, three suites). Main has 150 rows.The stand-in and how to run it
Rows with a marker.
looseBug, reports not equal: a boxed string and a subclass instance with the same text (both orders), two subclasses, the pair in an object, in an array, as a Map value, as a Map key and in a Set, two Maps keyed by both kinds with the values swapped,String.prototypeagainst an empty boxed string (both orders) and against an empty subclass instance.looseBug, reports equal: different values of an extra own property, an ownundefinedproperty, different out-of-range indexed properties, two instances of one subclass with different text and the sametoString()or the sameSymbol.toPrimitive,String.prototypeagainst{}(both orders).looseBugandstrictBug, throws a TypeError: two null-prototype boxed strings.strictBug: thetoString()andSymbol.toPrimitiverows (reports equal) and the twoString.prototyperows against an empty boxed string (reports not equal).Cause of the
toString(),Symbol.toPrimitiveand null-prototype rows. The String arm reads the text of each operand withtoStringInline(src/jsc/bindings/bindings.cpp:2047). That call runs user code, and it throws when the object has notoString. Node reads the internal slot. This PR does not change that read, because the change moves results ofassert.deepStrictEqualandutil.isDeepStrictEqual.Rows without a marker that guard a later fix. They agree with Node on main. A fix that only stops the class check breaks them.
undefinedproperty, with aSymbol.toStringTaggetter, or with atoString()orSymbol.toPrimitivethat returns the text of the other operand.1against'1', an own symbol property, and an extra own property that holds the pair. Node accepts these three. Main accepts them only because the loose comparison does not read the own properties of a boxed string.The fix that is not in this PR. I built a node-only comparator mode. In it, Node's loose rule compares two boxed strings: the
Object.prototype.toStringtag, the text in the internal slot, and the own enumerable string keys. I compared 2,500 ordered pairs of 50 boxed-string-like values.assert.deepEqualandnotDeepEqualagreed with Node on 1,993 pairs before and on 2,484 pairs after. The 12 other entry points kept all 30,000 results. It is not opened for these reasons:errorsof an Error like Node #35495 already adds the samejsFunctionIsDeepEqualentry with a run-time flag. The two changes conflict in 6 files.Set([new String("a"), new S("a")])againstSet([new String("a"), new String("b")])then passes, and a Map keyed by both kinds no longer equals its copy. deepEquals: compare a same-typed pair once, and pair Set and Map entries as node does #45457 is open for that pairing.Related. #35495 (native entry for legacy
deepEqual), #45457 (pairing of Set and Map entries), #32872 (own properties of a boxed string inBun.deepEquals), #35391 (Node's loose relation, draft), #23877 (==in legacydeepEqual).no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/bun-object/deep-equals.test.ts