Fix safeStringify treating shared references as circular - #335589
Open
Leo Camus (Dev-next-gen) wants to merge 2 commits into
Open
Fix safeStringify treating shared references as circular#335589Leo Camus (Dev-next-gen) wants to merge 2 commits into
Leo Camus (Dev-next-gen) wants to merge 2 commits into
Conversation
safeStringify kept every object it had seen in a Set and never removed
anything, so a value that appears in two sibling branches (a DAG, not a
cycle) was replaced with "[Circular]" on every occurrence after the
first. For example safeStringify([shared, { x: shared }]) returned
[{"a":1},{"x":"[Circular]"}].
Track only the current ancestor path instead, using the replacer's
`this` (the holder) to drop subtrees that are finished. True cycles are
still reported as "[Circular]". This is the same change that was made
to stableStringify in microsoft#327398.
The existing safeStringify test encoded the old behaviour: obj2 is both
obj1.friend and a sibling in `c`, so its second occurrence is shared,
not circular. Updated that expectation and added a test for shared
references in object and array positions.
Copilot started reviewing on behalf of
Leo Camus (Dev-next-gen)
September 10, 2026 23:01
View session
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The implementation correctly handles shared and circular references; only minor comment-style cleanup remains.
Pull request overview
Updates safeStringify to distinguish shared references from genuine circular references.
Changes:
- Tracks only the active ancestor path during serialization.
- Updates circular-reference expectations and adds shared-reference coverage.
File summaries
| File | Description |
|---|---|
src/vs/base/common/objects.ts |
Implements ancestor-path cycle detection. |
src/vs/base/test/common/objects.test.ts |
Updates and expands serialization tests. |
Review details
Suppressed comments (1)
src/vs/base/common/objects.ts:176
- This comment only narrates the
whileloop immediately below it, which the repository's comment guidance explicitly asks us to avoid. The holder comparison is already clear from the code, so please remove the comment.
// `this` is the object holding `key`, pop the subtrees that are already done
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
safeStringifyinsrc/vs/base/common/objects.tsputs every object it visits into aSetand never takes anything out, so it can't tell a cycle apart from an object that is simply referenced twice. Every occurrence of a shared value after the first comes out as"[Circular]":#327398 fixed the same bug in
stableStringifyin July. Here I track only the current ancestor path. The replacer is now afunction, so it receives the holder object asthis, and it pops finished subtrees off the stack until the holder is back on top. Real cycles are still replaced with"[Circular]".The existing
safeStringifytest asserted the old behaviour. In that fixtureobj2isobj1.friendand also the second element ofc. That second occurrence is not inside itself, so it is now serialized as{ friend: { friend: '[Circular]' } }, and the cycle back throughobj1is still caught one level down. I updated that expectation and added a test for shared references in both object and array positions.How I tested it: I ran
objects.test.tsagainst the originalobjects.ts, where the new test fails with the output shown above, and then with the fix, where all 10 tests in the file pass (I ran mocha directly on the transpiled file because the machine I used has Node 22). I also compared the patched function withJSON.stringifyon 20,000 random acyclic graphs containing shared nodes. The output matched every time, while the original differed on 7,751 of them. On 20,000 random cyclic graphs it matched a recursive ancestor-set reference every time.saveCachedSessionsgoes through this function and #314404 is about its cost, so I also timed it on 2,000 session-like records without sharing. The output is identical and the new version is slightly faster (about 6.0 ms per call against 6.7 ms), since it no longer builds a set of every visited object.One trade-off: an object reachable along several paths is now written out once per path, as
JSON.stringifywould do, so the output can be larger than before for heavily shared graphs. That is what the fix is for, and #327398 accepted the same trade-off.I didn't find an existing issue for this. I came across it while reading the code.
AI tools used