Skip to content

Commit 68a1dd1

Browse files
committed
feat: resolve maximum call stack exceeded error
1 parent 1ba0a73 commit 68a1dd1

9 files changed

+122
-29
lines changed

.editorconfig

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
root = true
2+
3+
[*]
4+
insert_final_newline = true
5+
charset = utf-8
6+
trim_trailing_whitespace = true
7+
end_of_line = lf
8+
9+
[*.{ts,js,json}]
10+
indent_style = space
11+
indent_size = 2

src/differ.ts

+13-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import concat from './utils/concat';
12
import detectCircular from './utils/detect-circular';
23
import diffArrayLCS from './utils/diff-array-lcs';
34
import diffArrayNormal from './utils/diff-array-normal';
@@ -122,6 +123,10 @@ export type ArrayDiffFunc = (
122123
...args: any[]
123124
) => [DiffResult[], DiffResult[]];
124125

126+
const EQUAL_EMPTY_LINE: DiffResult = { level: 0, type: 'equal', text: '' };
127+
const EQUAL_LEFT_BRACKET_LINE: DiffResult = { level: 0, type: 'equal', text: '{' };
128+
const EQUAL_RIGHT_BRACKET_LINE: DiffResult = { level: 0, type: 'equal', text: '}' };
129+
125130
class Differ {
126131
private options: DifferOptions;
127132
private arrayDiffFunc: ArrayDiffFunc;
@@ -243,14 +248,14 @@ class Differ {
243248
}));
244249
const lLength = resultLeft.length;
245250
const rLength = resultRight.length;
246-
resultLeft.push(...Array(rLength).fill({ level: 0, type: 'equal', text: '' }));
247-
resultRight.unshift(...Array(lLength).fill({ level: 0, type: 'equal', text: '' }));
251+
resultLeft = concat(resultLeft, Array(rLength).fill(EQUAL_EMPTY_LINE));
252+
resultRight = concat(resultRight, Array(lLength).fill(EQUAL_EMPTY_LINE), true);
248253
} else if (typeLeft === 'object') {
249254
[resultLeft, resultRight] = diffObject(sourceLeft, sourceRight, 1, this.options, this.arrayDiffFunc);
250-
resultLeft.unshift({ level: 0, type: 'equal', text: '{' });
251-
resultLeft.push({ level: 0, type: 'equal', text: '}' });
252-
resultRight.unshift({ level: 0, type: 'equal', text: '{' });
253-
resultRight.push({ level: 0, type: 'equal', text: '}' });
255+
resultLeft.unshift(EQUAL_LEFT_BRACKET_LINE);
256+
resultLeft.push(EQUAL_RIGHT_BRACKET_LINE);
257+
resultRight.unshift(EQUAL_LEFT_BRACKET_LINE);
258+
resultRight.push(EQUAL_RIGHT_BRACKET_LINE);
254259
} else if (typeLeft === 'array') {
255260
[resultLeft, resultRight] = this.arrayDiffFunc(sourceLeft, sourceRight, '', '', 0, this.options);
256261
} else if (sourceLeft !== sourceRight) {
@@ -269,10 +274,10 @@ class Differ {
269274
} else {
270275
resultLeft = [
271276
{ level: 0, type: 'remove', text: stringify(sourceLeft, null, null, this.options.maxDepth) },
272-
{ level: 0, type: 'equal', text: '' },
277+
EQUAL_EMPTY_LINE,
273278
];
274279
resultRight = [
275-
{ level: 0, type: 'equal', text: '' },
280+
EQUAL_EMPTY_LINE,
276281
{ level: 0, type: 'add', text: stringify(sourceRight, null, null, this.options.maxDepth) },
277282
];
278283
}

src/utils/cmp.spec.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import cmp from './cmp';
2+
3+
describe('Utility function: cmp', () => {
4+
5+
it('should respect the order', () => {
6+
const arr = [true, false, 1, '1', null, [1, 2, 3], { a: 1, b: 2 }];
7+
arr.sort(() => Math.random() > 0.5 ? 1 : -1);
8+
arr.sort(cmp);
9+
expect(arr).toEqual([false, true, 1, '1', null, [1, 2, 3], { a: 1, b: 2 }]);
10+
});
11+
12+
it('should correctly handle `ignoreCase`', () => {
13+
const arr = ['a', 'B', 'c', 'D', 'e', 'F'];
14+
15+
arr.sort(() => Math.random() > 0.5 ? 1 : -1);
16+
arr.sort(cmp);
17+
expect(arr).toEqual(['B', 'D', 'F', 'a', 'c', 'e']);
18+
19+
arr.sort(() => Math.random() > 0.5 ? 1 : -1);
20+
arr.sort((x, y) => cmp(x, y, { ignoreCase: true }));
21+
expect(arr).toEqual(['a', 'B', 'c', 'D', 'e', 'F']);
22+
});
23+
24+
});

src/utils/cmp.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ const cmp = (a: any, b: any, options?: DifferOptions) => {
4545
return a - b;
4646
case 'string':
4747
if (options?.ignoreCase) {
48-
return a.toLowerCase().localeCompare(b.toLowerCase());
48+
a = a.toLowerCase();
49+
b = b.toLowerCase();
4950
}
50-
return a.localeCompare(b);
51+
return a < b ? -1 : a > b ? 1 : 0;
5152
case 'boolean':
5253
return (+a) - (+b);
5354
}

src/utils/concat.spec.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import concat from './concat';
2+
3+
describe('Utility function: concat', () => {
4+
5+
it('should work for `append` mode', () => {
6+
expect(concat([1, 2, 3], ['a', 'b', 'c'])).toEqual([1, 2, 3, 'a', 'b', 'c']);
7+
});
8+
9+
it('should work for `prepend` mode (`unshift` mode)', () => {
10+
expect(concat([1, 2, 3], ['a', 'b', 'c'], true)).toEqual(['c', 'b', 'a', 1, 2, 3]);
11+
});
12+
13+
it('should throw error when parameter is not an array', () => {
14+
expect(() => concat(1 as any, ['a', 'b', 'c'])).toThrowError();
15+
expect(() => concat([1, 2, 3], 'abc' as any)).toThrowError();
16+
});
17+
18+
});

src/utils/concat.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* If we use `a.push(...b)`, it will result in `Maximum call stack size exceeded` error.
3+
* The reason is unclear, it may be a bug of V8, so we should implement a push method by ourselves.
4+
*/
5+
const concat = <T, U>(a: T[], b: U[], prependEach = false): (T | U)[] => {
6+
if (!Array.isArray(a) || !Array.isArray(b)) {
7+
throw new Error('Both arguments should be arrays.');
8+
}
9+
const lenA = a.length;
10+
const lenB = b.length;
11+
const len = lenA + lenB;
12+
const result = new Array(len);
13+
if (prependEach) {
14+
for (let i = 0; i < lenB; i++) {
15+
result[i] = b[lenB - i - 1];
16+
}
17+
for (let i = 0; i < lenA; i++) {
18+
result[i + lenB] = a[i];
19+
}
20+
return result;
21+
}
22+
for (let i = 0; i < lenA; i++) {
23+
result[i] = a[i];
24+
}
25+
for (let i = 0; i < lenB; i++) {
26+
result[i + lenA] = b[i];
27+
}
28+
return result;
29+
};
30+
31+
export default concat;

src/utils/diff-array-lcs.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import stringify from './stringify';
66
import type { DifferOptions, DiffResult } from '../differ';
77
import isEqual from './is-equal';
88
import shallowSimilarity from './shallow-similarity';
9+
import concat from './concat';
910

1011
const lcs = (
1112
arrLeft: any[],
@@ -64,8 +65,8 @@ const lcs = (
6465

6566
let i = arrLeft.length;
6667
let j = arrRight.length;
67-
const tLeft: DiffResult[] = [];
68-
const tRight: DiffResult[] = [];
68+
let tLeft: DiffResult[] = [];
69+
let tRight: DiffResult[] = [];
6970
// this is a backtracking process, all new lines should be unshifted to the result, not
7071
// pushed to the result
7172
while (i > 0 || j > 0) {
@@ -80,14 +81,14 @@ const lcs = (
8081
tRight.unshift({ level: level + 1, type: 'equal', text: formatValue(arrRight[j - 1]) });
8182
} else if (type === 'array') {
8283
const [l, r] = diffArrayLCS(arrLeft[i - 1], arrRight[j - 1], keyLeft, keyRight, level + 2, options);
83-
tLeft.unshift(...l);
84-
tRight.unshift(...r);
84+
tLeft = concat(tLeft, l, true);
85+
tRight = concat(tRight, r, true);
8586
} else if (type === 'object') {
8687
const [l, r] = diffObject(arrLeft[i - 1], arrRight[j - 1], level + 2, options, diffArrayLCS);
8788
tLeft.unshift({ level: level + 1, type: 'equal', text: '}' });
8889
tRight.unshift({ level: level + 1, type: 'equal', text: '}' });
89-
tLeft.unshift(...l);
90-
tRight.unshift(...r);
90+
tLeft = concat(tLeft, l, true);
91+
tRight = concat(tRight, r, true);
9192
tLeft.unshift({ level: level + 1, type: 'equal', text: '{' });
9293
tRight.unshift({ level: level + 1, type: 'equal', text: '{' });
9394
} else {
@@ -154,8 +155,8 @@ const diffArrayLCS = (
154155
linesRight.push({ level: level + 1, type: 'equal', text: '...' });
155156
} else {
156157
const [tLeftReverse, tRightReverse] = lcs(arrLeft, arrRight, keyLeft, keyRight, level, options);
157-
linesLeft.push(...tLeftReverse);
158-
linesRight.push(...tRightReverse);
158+
linesLeft = concat(linesLeft, tLeftReverse);
159+
linesRight = concat(linesRight, tRightReverse);;
159160
}
160161

161162
linesLeft.push({ level, type: 'equal', text: ']' });

src/utils/diff-array-normal.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import concat from './concat';
12
import formatValue from './format-value';
23
import diffObject from './diff-object';
34
import getType from './get-type';
@@ -51,14 +52,14 @@ const diffArrayNormal = (
5152
linesLeft.push({ level: level + 1, type: 'equal', text: '{' });
5253
linesRight.push({ level: level + 1, type: 'equal', text: '{' });
5354
const [leftLines, rightLines] = diffObject(itemLeft, itemRight, level + 2, options, diffArrayNormal);
54-
linesLeft.push(...leftLines);
55-
linesRight.push(...rightLines);
55+
linesLeft = concat(linesLeft, leftLines);
56+
linesRight = concat(linesRight, rightLines);
5657
linesLeft.push({ level: level + 1, type: 'equal', text: '}' });
5758
linesRight.push({ level: level + 1, type: 'equal', text: '}' });
5859
} else if (leftType === 'array') {
5960
const [resLeft, resRight] = diffArrayNormal(itemLeft, itemRight, '', '', level + 2, options, [], []);
60-
linesLeft.push(...resLeft);
61-
linesRight.push(...resRight);
61+
linesLeft = concat(linesLeft, resLeft);
62+
linesRight = concat(linesRight, resRight);
6263
} else if (itemLeft === itemRight) {
6364
linesLeft.push({ level: level + 1, type: 'equal', text: formatValue(itemLeft) });
6465
linesRight.push({ level: level + 1, type: 'equal', text: formatValue(itemRight) });

src/utils/diff-object.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import concat from './concat';
12
import formatValue from './format-value';
23
import getType from './get-type';
4+
import sortStrings from './sort-strings';
35
import stringify from './stringify';
46

57
import type { DifferOptions, DiffResult, ArrayDiffFunc } from '../differ';
6-
import sortStrings from './sort-strings';
78

89
const diffObject = (
910
lhs: Record<string, any>,
@@ -19,8 +20,8 @@ const diffObject = (
1920
];
2021
}
2122

22-
const linesLeft: DiffResult[] = [];
23-
const linesRight: DiffResult[] = [];
23+
let linesLeft: DiffResult[] = [];
24+
let linesRight: DiffResult[] = [];
2425

2526
if (lhs === null && rhs === null || lhs === undefined && rhs === undefined) {
2627
return [linesLeft, linesRight];
@@ -119,8 +120,8 @@ const diffObject = (
119120
const arrLeft = [...lhs[keyLeft]];
120121
const arrRight = [...rhs[keyRight]];
121122
const [resLeft, resRight] = arrayDiffFunc(arrLeft, arrRight, keyLeft, keyRight, level, options, [], []);
122-
linesLeft.push(...resLeft);
123-
linesRight.push(...resRight);
123+
linesLeft = concat(linesLeft, resLeft);
124+
linesRight = concat(linesRight, resRight);
124125
} else if (typeof lhs[keyLeft] === 'object') {
125126
const result = diffObject(
126127
lhs[keyLeft],
@@ -130,10 +131,10 @@ const diffObject = (
130131
arrayDiffFunc,
131132
);
132133
linesLeft.push({ level, type: 'equal', text: `"${keyLeft}": {` });
133-
linesLeft.push(...result[0]);
134+
linesLeft = concat(linesLeft, result[0]);
134135
linesLeft.push({ level, type: 'equal', text: '}' });
135136
linesRight.push({ level, type: 'equal', text: `"${keyRight}": {` });
136-
linesRight.push(...result[1]);
137+
linesRight = concat(linesRight, result[1]);
137138
linesRight.push({ level, type: 'equal', text: '}' });
138139
} else {
139140
if (lhs[keyLeft] !== rhs[keyRight]) {

0 commit comments

Comments
 (0)