Skip to content

Commit a9ba0a5

Browse files
authored
feat(core): support cycles in deep cloning (#3322)
Before this change, deepClone function supported directed acyclic trees. With this change the function will handle directed cyclic graphs. Refs #3290
1 parent e750e72 commit a9ba0a5

File tree

2 files changed

+100
-28
lines changed

2 files changed

+100
-28
lines changed

packages/apidom-core/src/clone/index.ts

+64-12
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,78 @@ import ShallowCloneError from './errors/ShallowCloneError';
66

77
type FinalCloneTypes = KeyValuePair | ArraySlice | ObjectSlice;
88

9-
const invokeClone = <T extends Element | FinalCloneTypes>(value: T): T => {
10-
if (typeof value?.clone === 'function') {
11-
return value.clone() as T;
12-
}
13-
return value;
9+
type DeepCloneOptions<T extends Element | FinalCloneTypes> = {
10+
visited?: WeakMap<T, T>;
1411
};
1512

16-
export const cloneDeep = <T extends Element | FinalCloneTypes>(value: T): T => {
13+
export const cloneDeep = <T extends Element | FinalCloneTypes>(
14+
value: T,
15+
options: DeepCloneOptions<T> = {},
16+
): T => {
17+
const { visited = new WeakMap<T, T>() } = options;
18+
const passThroughOptions = { ...options, visited };
19+
20+
// detect cycle and return memoized value
21+
if (visited.has(value)) {
22+
return visited.get(value) as T;
23+
}
24+
25+
if (value instanceof KeyValuePair) {
26+
const { key, value: val } = value;
27+
const keyCopy = isElement(key)
28+
? cloneDeep(key, passThroughOptions as DeepCloneOptions<Element>)
29+
: key;
30+
const valueCopy = isElement(val)
31+
? cloneDeep(val, passThroughOptions as DeepCloneOptions<Element>)
32+
: val;
33+
const copy = new KeyValuePair(keyCopy, valueCopy) as T;
34+
visited.set(value, copy);
35+
return copy;
36+
}
37+
1738
if (value instanceof ObjectSlice) {
18-
const items = [...(value as ObjectSlice)].map(invokeClone) as T[];
19-
return new ObjectSlice(items) as T;
39+
const mapper = (element: T) => cloneDeep(element, passThroughOptions);
40+
const items = [...(value as ObjectSlice)].map(mapper) as T[];
41+
const copy = new ObjectSlice(items) as T;
42+
visited.set(value, copy);
43+
return copy;
2044
}
2145

2246
if (value instanceof ArraySlice) {
23-
const items = [...(value as ArraySlice)].map(invokeClone) as T[];
24-
return new ArraySlice(items) as T;
47+
const mapper = (element: T) => cloneDeep(element, passThroughOptions);
48+
const items = [...(value as ArraySlice)].map(mapper) as T[];
49+
const copy = new ArraySlice(items) as T;
50+
visited.set(value, copy);
51+
return copy;
2552
}
2653

27-
if (typeof value?.clone === 'function') {
28-
return value.clone() as T;
54+
if (isElement(value)) {
55+
const copy = cloneShallow(value); // eslint-disable-line @typescript-eslint/no-use-before-define
56+
57+
visited.set(value, copy);
58+
59+
if (value.content) {
60+
if (isElement(value.content)) {
61+
copy.content = cloneDeep(
62+
value.content,
63+
passThroughOptions as DeepCloneOptions<Element>,
64+
) as any;
65+
} else if ((value.content as unknown) instanceof KeyValuePair) {
66+
copy.content = cloneDeep(
67+
value.content as unknown as KeyValuePair,
68+
passThroughOptions as DeepCloneOptions<KeyValuePair>,
69+
) as any;
70+
} else if (Array.isArray(value.content)) {
71+
const mapper = (element: unknown) => cloneDeep(element as T, passThroughOptions);
72+
copy.content = value.content.map(mapper);
73+
} else {
74+
copy.content = value.content;
75+
}
76+
} else {
77+
copy.content = value.content;
78+
}
79+
80+
return copy;
2981
}
3082

3183
throw new DeepCloneError("Value provided to cloneDeep function couldn't be cloned", {

packages/apidom-core/test/clone/index.ts

+36-16
Original file line numberDiff line numberDiff line change
@@ -119,28 +119,48 @@ describe('clone', function () {
119119
});
120120

121121
context('cloneDeep', function () {
122-
specify('should deep clone ObjectElement', function () {
123-
const valueElement = new ArrayElement([1]);
124-
const objectElement = new ObjectElement({ a: valueElement });
125-
const clone = cloneDeep(objectElement);
122+
context('given ObjectElement', function () {
123+
specify('should deep clone', function () {
124+
const valueElement = new ArrayElement([1]);
125+
const objectElement = new ObjectElement({ a: valueElement });
126+
const clone = cloneDeep(objectElement);
126127

127-
objectElement.set('c', 'd');
128-
valueElement.push(2);
128+
objectElement.set('c', 'd');
129+
valueElement.push(2);
129130

130-
assert.notStrictEqual(clone, objectElement);
131-
assert.deepEqual(toValue(clone), { a: [1] });
131+
assert.notStrictEqual(clone, objectElement);
132+
assert.deepEqual(toValue(clone), { a: [1] });
133+
});
134+
135+
specify('should deep clone with cycles', function () {
136+
const objectElement = new ObjectElement({ a: 'b' });
137+
objectElement.set('c', objectElement);
138+
const clone = cloneDeep(objectElement);
139+
140+
assert.strictEqual(clone, clone.get('c'));
141+
});
132142
});
133143

134-
specify('should deep clone ArrayElement', function () {
135-
const firstItemElement = new ObjectElement({ a: 'b' });
136-
const arrayElement = new ArrayElement([firstItemElement, 2, 3]);
137-
const clone = cloneDeep(arrayElement);
144+
context('given ArrayElement', function () {
145+
specify('should deep clone', function () {
146+
const firstItemElement = new ObjectElement({ a: 'b' });
147+
const arrayElement = new ArrayElement([firstItemElement, 2, 3]);
148+
const clone = cloneDeep(arrayElement);
138149

139-
arrayElement.push(4);
140-
firstItemElement.set('a', 'c');
150+
arrayElement.push(4);
151+
firstItemElement.set('a', 'c');
141152

142-
assert.notStrictEqual(clone, arrayElement);
143-
assert.deepEqual(toValue(clone), [{ a: 'b' }, 2, 3]);
153+
assert.notStrictEqual(clone, arrayElement);
154+
assert.deepEqual(toValue(clone), [{ a: 'b' }, 2, 3]);
155+
});
156+
157+
specify('should deep clone with cycles', function () {
158+
const arrayElement = new ArrayElement([1]);
159+
arrayElement.push(arrayElement);
160+
const clone = cloneDeep(arrayElement);
161+
162+
assert.strictEqual(clone, clone.get(1));
163+
});
144164
});
145165

146166
specify('should deep clone NumberElement', function () {

0 commit comments

Comments
 (0)