Skip to content

Commit 7068712

Browse files
larshvileclaudecpojer
authored
fix: prevent infinite recursion with self-referential getters (#15822) (#15831)
Co-authored-by: Claude <[email protected]> Co-authored-by: Christoph Nakazawa <[email protected]>
1 parent b1fe77c commit 7068712

File tree

7 files changed

+158
-6
lines changed

7 files changed

+158
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
### Fixes
1919

20+
- `[jest-matcher-utils]` Fix infinite recursion with self-referential getters in `deepCyclicCopyReplaceable` ([#15831](https://github.com/jestjs/jest/pull/15831))
2021
- `[babel-jest]` Export the `TransformerConfig` interface ([#15820](https://github.com/jestjs/jest/pull/15820))
2122
- `[jest-config]` Fix `jest.config.ts` with TS loader specified in docblock pragma ([#15839](https://github.com/jestjs/jest/pull/15839))
2223

packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4595,6 +4595,19 @@ Expected: not <g>Set {2, 1}</>
45954595
Received: <r>Set {1, 2}</>
45964596
`;
45974597

4598+
exports[`toMatchObject() circular references getter circular references handles self-referential getter without infinite recursion 1`] = `
4599+
<d>expect(</><r>received</><d>).</>toMatchObject<d>(</><g>expected</><d>)</>
4600+
4601+
<g>- Expected - 2</>
4602+
<r>+ Received + 2</>
4603+
4604+
<g>- TestClass {</>
4605+
<g>- "value": "def",</>
4606+
<r>+ Object {</>
4607+
<r>+ "value": "abc",</>
4608+
<d> }</>
4609+
`;
4610+
45984611
exports[`toMatchObject() circular references simple circular references {pass: false} expect({"a": "hello", "ref": [Circular]}).toMatchObject({"a": "world", "ref": [Circular]}) 1`] = `
45994612
<d>expect(</><r>received</><d>).</>toMatchObject<d>(</><g>expected</><d>)</>
46004613

packages/expect/src/__tests__/matchers.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2213,6 +2213,30 @@ describe('toMatchObject()', () => {
22132213
[primitiveInsteadOfRef, transitiveCircularObjA1],
22142214
]);
22152215
});
2216+
2217+
describe('getter circular references', () => {
2218+
test('handles self-referential getter without infinite recursion', () => {
2219+
class TestClass {
2220+
constructor(value) {
2221+
this.value = value;
2222+
}
2223+
2224+
get selfRef() {
2225+
return new TestClass(this.value.toLowerCase());
2226+
}
2227+
}
2228+
2229+
const abc = new TestClass('abc');
2230+
const def = new TestClass('def');
2231+
2232+
jestExpect(abc).toMatchObject(abc);
2233+
jestExpect(abc).not.toMatchObject(def);
2234+
2235+
expect(() =>
2236+
jestExpect(abc).toMatchObject(def),
2237+
).toThrowErrorMatchingSnapshot();
2238+
});
2239+
});
22162240
});
22172241

22182242
testNotToMatchSnapshots([

packages/jest-matcher-utils/src/__tests__/__snapshots__/printDiffOrStringify.test.ts.snap

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,16 @@ Expected: <g>"multi</>
184184
Received: <r>""</>
185185
`;
186186
187+
exports[`printDiffOrStringify getters handles self-referential getters without infinite recursion 1`] = `
188+
<g>- Expected - 1</>
189+
<r>+ Received + 1</>
190+
191+
<d> TestClass {</>
192+
<g>- "value": "hello",</>
193+
<r>+ "value": "world",</>
194+
<d> }</>
195+
`;
196+
187197
exports[`printDiffOrStringify has no common after clean up chaff multiline 1`] = `
188198
<g>- Expected - 2</>
189199
<r>+ Received + 2</>

packages/jest-matcher-utils/src/__tests__/deepCyclicCopyReplaceable.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,76 @@ test('json from Response', async () => {
184184
const json = await response().json();
185185
deepCyclicCopyReplaceable(json);
186186
});
187+
188+
test('handles self-referential getters without infinite recursion', () => {
189+
class TestClass {
190+
constructor(public value: string) {}
191+
192+
get selfRef() {
193+
return new TestClass(this.value.toLowerCase());
194+
}
195+
}
196+
197+
const obj = new TestClass('HELLO');
198+
const copy = deepCyclicCopyReplaceable(obj);
199+
200+
expect(copy.value).toBe('HELLO');
201+
expect(copy.selfRef).toBe('[Getter]');
202+
});
203+
204+
test('handles getters returning different class instances', () => {
205+
class OtherClass {
206+
constructor(public value: string) {}
207+
}
208+
209+
class WithGetter {
210+
constructor(public value: string) {}
211+
212+
get other() {
213+
return new OtherClass(this.value.toLowerCase());
214+
}
215+
}
216+
217+
const obj = new WithGetter('HELLO');
218+
const copy = deepCyclicCopyReplaceable(obj);
219+
220+
expect(copy.value).toBe('HELLO');
221+
expect(copy.other).toEqual({value: 'hello'});
222+
expect(copy.other.constructor).toBe(OtherClass);
223+
});
224+
225+
test('handles nested objects with self-referential getters', () => {
226+
class InnerClass {
227+
constructor(public value: string) {}
228+
229+
get self() {
230+
return new InnerClass(`${this.value}_self`);
231+
}
232+
}
233+
234+
class OuterClass {
235+
constructor(public inner: InnerClass) {}
236+
}
237+
238+
const obj = new OuterClass(new InnerClass('test'));
239+
const copy = deepCyclicCopyReplaceable(obj);
240+
241+
expect(copy.inner.value).toBe('test');
242+
expect(copy.inner.self).toBe('[Getter]');
243+
});
244+
245+
test('handles getters returning primitive values', () => {
246+
class TestClass {
247+
constructor(public value: string) {}
248+
249+
get upperCase() {
250+
return this.value.toUpperCase();
251+
}
252+
}
253+
254+
const obj = new TestClass('hello');
255+
const copy = deepCyclicCopyReplaceable(obj);
256+
257+
expect(copy.value).toBe('hello');
258+
expect(copy.upperCase).toBe('HELLO');
259+
});

packages/jest-matcher-utils/src/__tests__/printDiffOrStringify.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,4 +269,21 @@ describe('printDiffOrStringify', () => {
269269
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
270270
});
271271
});
272+
273+
describe('getters', () => {
274+
test('handles self-referential getters without infinite recursion', () => {
275+
class TestClass {
276+
constructor(public value: string) {}
277+
278+
get selfRef() {
279+
return new TestClass(`${this.value}_ref`);
280+
}
281+
}
282+
283+
const expected = new TestClass('hello');
284+
const received = new TestClass('world');
285+
286+
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
287+
});
288+
});
272289
});

packages/jest-matcher-utils/src/deepCyclicCopyReplaceable.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,16 +101,30 @@ function deepCyclicCopyObject<T>(object: T, cycles: WeakMap<any, unknown>): T {
101101
//https://github.com/microsoft/TypeScript/issues/1863
102102
(newDescriptors: {[x: string]: PropertyDescriptor}, key: string) => {
103103
const enumerable = descriptors[key].enumerable;
104+
const descriptor = descriptors[key];
105+
106+
let value;
107+
108+
if (descriptor.get) {
109+
const getterRes = (object as Record<string | symbol, unknown>)[key];
110+
const isSelfReferential =
111+
getterRes?.constructor === (object as any).constructor &&
112+
getterRes?.constructor !== Object;
113+
114+
value = isSelfReferential
115+
? '[Getter]'
116+
: deepCyclicCopyReplaceable(getterRes, cycles);
117+
} else {
118+
value = deepCyclicCopyReplaceable(
119+
(object as Record<string | symbol, unknown>)[key],
120+
cycles,
121+
);
122+
}
104123

105124
newDescriptors[key] = {
106125
configurable: true,
107126
enumerable,
108-
value: deepCyclicCopyReplaceable(
109-
// this accesses the value or getter, depending. We just care about the value anyways, and this allows us to not mess with accessors
110-
// it has the side effect of invoking the getter here though, rather than copying it over
111-
(object as Record<string | symbol, unknown>)[key],
112-
cycles,
113-
),
127+
value,
114128
writable: true,
115129
};
116130
return newDescriptors;

0 commit comments

Comments
 (0)