From ab343e8b56f11f1434823db21d09b0a9b504547d Mon Sep 17 00:00:00 2001 From: li6in9muyou Date: Mon, 11 Mar 2024 19:07:45 +0800 Subject: [PATCH 1/4] test(util): add a circular test for clone --- test/ut/spec/core/util.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/ut/spec/core/util.test.ts b/test/ut/spec/core/util.test.ts index 618471853..bf053e446 100755 --- a/test/ut/spec/core/util.test.ts +++ b/test/ut/spec/core/util.test.ts @@ -146,5 +146,28 @@ describe('zrUtil', function() { }); + it("circular", function () { + class TreeNode { + public children: TreeNode[] + public parent: TreeNode | null + + constructor(parent: TreeNode = null, children: TreeNode[] = []) { + this.children = children; + this.parent = parent; + } + } + + const root = new TreeNode(); + const a = new TreeNode(root, [new TreeNode(), new TreeNode()]); + a.children.forEach(c => c.parent = a); + root.children.push(a) + root.children.push(new TreeNode(root, [new TreeNode()])) + expect(zrUtil.clone(root)).toEqual(root); + + const b: { key: any } = {key: null}; + b.key = b; + expect(zrUtil.clone(b)).toEqual(b); + }); + }); }); \ No newline at end of file From 86b5156f5d1a5d76c0e7dd0e81dd00d7c66f70cb Mon Sep 17 00:00:00 2001 From: li6in9muyou Date: Mon, 11 Mar 2024 19:56:52 +0800 Subject: [PATCH 2/4] fix(util): prevent infinite recursion when source contains circular references --- src/core/util.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/core/util.ts b/src/core/util.ts index 9100c7f39..3f323488e 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -75,10 +75,14 @@ export function logError(...args: any[]) { * (There might be a large number of date in `series.data`). * So date should not be modified in and out of echarts. */ -export function clone(source: T): T { +function cloneHelper(source: T, alreadyCloned: Set): T { if (source == null || typeof source !== 'object') { return source; } + if (alreadyCloned.has(source)) { + return source; + } + alreadyCloned.add(source); let result = source as any; const typeStr = objToString.call(source); @@ -87,7 +91,7 @@ export function clone(source: T): T { if (!isPrimitive(source)) { result = [] as any; for (let i = 0, len = (source as any[]).length; i < len; i++) { - result[i] = clone((source as any[])[i]); + result[i] = cloneHelper((source as any[])[i], alreadyCloned); } } } @@ -111,14 +115,20 @@ export function clone(source: T): T { for (let key in source) { // Check if key is __proto__ to avoid prototype pollution if (source.hasOwnProperty(key) && key !== protoKey) { - result[key] = clone(source[key]); + result[key] = cloneHelper(source[key], alreadyCloned); } } } + alreadyCloned.delete(source); return result; } +export function clone(source: T): T { + const alreadyCloned = new Set(); + return cloneHelper(source, alreadyCloned); +} + export function merge< T extends Dictionary, S extends Dictionary From e9589e275d4f55ff78d954a7b6f256f6633918d2 Mon Sep 17 00:00:00 2001 From: li6in9muyou Date: Tue, 12 Mar 2024 09:22:54 +0800 Subject: [PATCH 3/4] test(util): ensure that circular references correctly point to the newly cloned object instead of the original one --- test/ut/spec/core/util.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/ut/spec/core/util.test.ts b/test/ut/spec/core/util.test.ts index bf053e446..6e25b8d84 100755 --- a/test/ut/spec/core/util.test.ts +++ b/test/ut/spec/core/util.test.ts @@ -166,7 +166,12 @@ describe('zrUtil', function() { const b: { key: any } = {key: null}; b.key = b; - expect(zrUtil.clone(b)).toEqual(b); + const bCloned = zrUtil.clone(b); + expect(bCloned === b).toBeFalsy(); + expect(bCloned.key === b).toBeFalsy(); + expect(bCloned === b.key).toBeFalsy(); + expect(bCloned.key === b.key).toBeFalsy(); + expect(bCloned).toEqual(b); }); }); From 6fb96a1421d0399c03671d9d97891838575e582f Mon Sep 17 00:00:00 2001 From: li6in9muyou Date: Tue, 12 Mar 2024 09:23:53 +0800 Subject: [PATCH 4/4] fix(util): cloned circular references incorrectly point to the original object --- src/core/util.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/core/util.ts b/src/core/util.ts index 3f323488e..264210de6 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -75,14 +75,13 @@ export function logError(...args: any[]) { * (There might be a large number of date in `series.data`). * So date should not be modified in and out of echarts. */ -function cloneHelper(source: T, alreadyCloned: Set): T { +function cloneHelper(source: T, alreadyCloned: Map): T { if (source == null || typeof source !== 'object') { return source; } if (alreadyCloned.has(source)) { - return source; + return alreadyCloned.get(source) as T; } - alreadyCloned.add(source); let result = source as any; const typeStr = objToString.call(source); @@ -90,6 +89,7 @@ function cloneHelper(source: T, alreadyCloned: Set): T { if (typeStr === '[object Array]') { if (!isPrimitive(source)) { result = [] as any; + alreadyCloned.set(source, []); for (let i = 0, len = (source as any[]).length; i < len; i++) { result[i] = cloneHelper((source as any[])[i], alreadyCloned); } @@ -112,6 +112,7 @@ function cloneHelper(source: T, alreadyCloned: Set): T { } else if (!BUILTIN_OBJECT[typeStr] && !isPrimitive(source) && !isDom(source)) { result = {} as any; + alreadyCloned.set(source, result); for (let key in source) { // Check if key is __proto__ to avoid prototype pollution if (source.hasOwnProperty(key) && key !== protoKey) { @@ -125,7 +126,7 @@ function cloneHelper(source: T, alreadyCloned: Set): T { } export function clone(source: T): T { - const alreadyCloned = new Set(); + const alreadyCloned = new Map(); return cloneHelper(source, alreadyCloned); }