diff --git a/src/vs/base/common/objects.ts b/src/vs/base/common/objects.ts index 234816f4ade535..ee357967364036 100644 --- a/src/vs/base/common/objects.ts +++ b/src/vs/base/common/objects.ts @@ -215,19 +215,26 @@ 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(',') + '}'; + + // Track only ancestors so shared sibling references are serialized in full. + 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', () => {