Skip to content

Commit cb45abc

Browse files
Fix infinite recursion on circular objects (#91)
* tsconfig target bump * refactor and circular object fix for deep equal * small test to verify fix
1 parent b348368 commit cb45abc

File tree

3 files changed

+61
-20
lines changed

3 files changed

+61
-20
lines changed

spec/issues/90.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import test from 'ava';
2+
3+
import { Substitute } from '../../src/index';
4+
5+
test('can handle circular referenced objects', t => {
6+
interface Service {
7+
foo(a: any): string;
8+
}
9+
10+
const s = Substitute.for<Service>();
11+
12+
const parent = {} as any;
13+
parent.child = parent;
14+
15+
const root = {} as any;
16+
root.path = { to: { nested: root } };
17+
18+
s.foo(parent).returns('even-circular');
19+
s.foo(root).returns('even-nested-circular');
20+
21+
t.is(s.foo(parent), 'even-circular');
22+
t.is(s.foo(root), 'even-nested-circular');
23+
});
24+
25+
test('can handle non circular referenced objects', t => {
26+
interface Service {
27+
foo(a: any): string;
28+
}
29+
30+
const s = Substitute.for<Service>();
31+
32+
const parent = {} as any;
33+
parent.child = { family: true };
34+
35+
s.foo(parent).returns('even-non-circular');
36+
t.is(s.foo(parent), 'even-non-circular');
37+
});

src/Utilities.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Argument, AllArguments } from "./Arguments";
1+
import { Argument, AllArguments } from './Arguments';
22
import { GetPropertyState } from './states/GetPropertyState';
33
import { InitialState } from './states/InitialState';
44
import { Context } from './Context';
@@ -21,6 +21,7 @@ export enum SubstituteMethods {
2121
rejects = 'rejects'
2222
}
2323

24+
const seenObject = Symbol();
2425
export const Nothing = Symbol();
2526
export type Nothing = typeof Nothing
2627

@@ -73,27 +74,31 @@ export function areArgumentsEqual(a: any, b: any) {
7374
return deepEqual(a, b);
7475
};
7576

76-
function deepEqual(a: any, b: any): boolean {
77-
if (Array.isArray(a)) {
78-
if (!Array.isArray(b) || a.length !== b.length) return false;
79-
for (let i = 0; i < a.length; i++) {
80-
if (!deepEqual(a[i], b[i])) return false;
81-
}
82-
return true;
83-
}
84-
if (typeof a === 'object' && a !== null && b !== null) {
85-
if (!(typeof b === 'object')) return false;
77+
function deepEqual(realA: any, realB: any, objectReferences: Object[] = []): boolean {
78+
const a = objectReferences.includes(realA) ? seenObject : realA;
79+
const b = objectReferences.includes(realB) ? seenObject : realB;
80+
const newObjectReferences = updateObjectReferences(objectReferences, a, b);
81+
82+
if (nonNullObject(a) && nonNullObject(b)) {
8683
if (a.constructor !== b.constructor) return false;
87-
const keys = Object.keys(a);
88-
if (keys.length !== Object.keys(b).length) return false;
84+
if (Object.keys(a).length !== Object.keys(b).length) return false;
8985
for (const key in a) {
90-
if (!deepEqual(a[key], b[key])) return false;
86+
if (!deepEqual(a[key], b[key], newObjectReferences)) return false;
9187
}
9288
return true;
9389
}
9490
return a === b;
9591
}
9692

93+
function updateObjectReferences(objectReferences: Array<Object>, a: any, b: any) {
94+
const tempObjectReferences = [...objectReferences, nonNullObject(a) && !objectReferences.includes(a) ? a : void 0];
95+
return [...tempObjectReferences, nonNullObject(b) && !tempObjectReferences.includes(b) ? b : void 0];
96+
}
97+
98+
function nonNullObject(value: any) {
99+
return typeof value === 'object' && value !== null;
100+
}
101+
97102
export function Get(recorder: InitialState, context: Context, property: PropertyKey) {
98103
const existingGetState = recorder.getPropertyStates.find(state => state.property === property);
99104
if (existingGetState) {

tsconfig.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
{
22
"compileOnSave": true,
33
"compilerOptions": {
4+
"module": "commonjs",
5+
"moduleResolution": "node",
46
"baseUrl": ".",
5-
"declaration": true,
7+
"declaration": true,
68
"sourceMap": true,
79
"inlineSources": true,
810
"downlevelIteration": true,
911
"outDir": "./dist",
1012
"strict": true,
1113
"strictNullChecks": false,
12-
"target": "es5",
13-
"lib": [
14-
"es2015"
15-
]
14+
"target": "ES2016"
1615
}
17-
}
16+
}

0 commit comments

Comments
 (0)