From d467363f157602ee713f2140d18e90a3fa26bfba Mon Sep 17 00:00:00 2001 From: Dmitry Savy Date: Fri, 24 Jul 2026 16:28:50 -0500 Subject: [PATCH 1/2] Fix stableStringify treating shared references as circular _stableStringify uses a WeakSet to guard against circular references, but it never removed a value after serializing its subtree. As a result a value that is shared across sibling branches (a DAG, not a cycle) was reported as "[Circular]" on every occurrence after the first. For example stableStringify({ x: shared, y: shared }) returned {"x":{"a":1},"y":"[Circular]"} instead of {"x":{"a":1},"y":{"a":1}}. Track only the current ancestor path by removing the value from the set once its subtree is done. True cycles are still detected. Added tests for shared references in both object and array positions. --- src/vs/base/common/objects.ts | 28 ++++++++++++++++--------- src/vs/base/test/common/objects.test.ts | 12 +++++++++++ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/vs/base/common/objects.ts b/src/vs/base/common/objects.ts index 234816f4ade535..9642bf59817e10 100644 --- a/src/vs/base/common/objects.ts +++ b/src/vs/base/common/objects.ts @@ -215,19 +215,27 @@ function _stableStringify(value: unknown, seen: WeakSet): string { return '"[Circular]"'; } seen.add(value as object); + + let result: string; if (Array.isArray(value)) { - return '[' + value.map(v => _stableStringify(v, seen)).join(',') + ']'; - } - const keys = Object.keys(value as object).sort(); - const parts: string[] = []; - for (const k of keys) { - const v = (value as Record)[k]; - if (v === undefined) { - continue; + result = '[' + value.map(v => _stableStringify(v, seen)).join(',') + ']'; + } else { + const keys = Object.keys(value as object).sort(); + const parts: string[] = []; + for (const k of keys) { + const v = (value as Record)[k]; + if (v === undefined) { + continue; + } + parts.push(JSON.stringify(k) + ':' + _stableStringify(v, seen)); } - parts.push(JSON.stringify(k) + ':' + _stableStringify(v, seen)); + result = '{' + parts.join(',') + '}'; } - return '{' + parts.join(',') + '}'; + + // Only track the current ancestor path, so a value that is shared across + // sibling branches is serialized in full rather than reported as circular. + seen.delete(value as object); + return result; } type obj = { [key: string]: any }; diff --git a/src/vs/base/test/common/objects.test.ts b/src/vs/base/test/common/objects.test.ts index 8f33cb81980905..941449d4f729e0 100644 --- a/src/vs/base/test/common/objects.test.ts +++ b/src/vs/base/test/common/objects.test.ts @@ -181,6 +181,18 @@ suite('Objects', () => { objects.stableStringify(circular), '{"a":1,"self":"[Circular]"}' ); + + // A shared (non-circular) reference used in sibling branches is + // serialized in full and not treated as circular + const shared = { a: 1 }; + assert.strictEqual( + objects.stableStringify({ x: shared, y: shared }), + '{"x":{"a":1},"y":{"a":1}}' + ); + assert.strictEqual( + objects.stableStringify([shared, shared]), + '[{"a":1},{"a":1}]' + ); }); test('distinct', () => { From 0a4331a0873703cffeaacf48cb3fbdef9ff3d881 Mon Sep 17 00:00:00 2001 From: Dmitry Savy Date: Fri, 24 Jul 2026 16:39:42 -0500 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/vs/base/common/objects.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/base/common/objects.ts b/src/vs/base/common/objects.ts index 9642bf59817e10..ee357967364036 100644 --- a/src/vs/base/common/objects.ts +++ b/src/vs/base/common/objects.ts @@ -232,8 +232,7 @@ function _stableStringify(value: unknown, seen: WeakSet): string { result = '{' + parts.join(',') + '}'; } - // Only track the current ancestor path, so a value that is shared across - // sibling branches is serialized in full rather than reported as circular. + // Track only ancestors so shared sibling references are serialized in full. seen.delete(value as object); return result; }