Skip to content

Commit

Permalink
fix: resolve circular ref when cloning object (#1444)
Browse files Browse the repository at this point in the history
  • Loading branch information
nieyuyao committed Jun 6, 2022
1 parent 24af3fe commit 8452a7d
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 23 deletions.
14 changes: 10 additions & 4 deletions packages/vitest/src/runtime/error.ts
@@ -1,7 +1,7 @@
import { format } from 'util'
import { util } from 'chai'
import { stringify } from '../integrations/chai/jest-matcher-utils'
import { clone, getType } from '../utils'
import { deepClone, getType } from '../utils'

const OBJECT_PROTO = Object.getPrototypeOf({})

Expand Down Expand Up @@ -63,8 +63,8 @@ export function processError(err: any) {
if (err.name)
err.nameStr = String(err.name)

const clonedActual = clone(err.actual)
const clonedExpected = clone(err.expected)
const clonedActual = deepClone(err.actual)
const clonedExpected = deepClone(err.expected)

const { replacedActual, replacedExpected } = replaceAsymmetricMatcher(clonedActual, clonedExpected)

Expand Down Expand Up @@ -105,9 +105,13 @@ function isReplaceable(obj1: any, obj2: any) {
return obj1Type === obj2Type && obj1Type === 'Object'
}

export function replaceAsymmetricMatcher(actual: any, expected: any) {
export function replaceAsymmetricMatcher(actual: any, expected: any, actualReplaced = new WeakMap(), expectedReplaced = new WeakMap()) {
if (!isReplaceable(actual, expected))
return { replacedActual: actual, replacedExpected: expected }
if (actualReplaced.has(actual) || expectedReplaced.has(expected))
return { replacedActual: actual, replacedExpected: expected }
actualReplaced.set(actual, true)
expectedReplaced.set(expected, true)
util.getOwnEnumerableProperties(expected).forEach((key) => {
const expectedValue = expected[key]
const actualValue = actual[key]
Expand All @@ -123,6 +127,8 @@ export function replaceAsymmetricMatcher(actual: any, expected: any) {
const replaced = replaceAsymmetricMatcher(
actualValue,
expectedValue,
actualReplaced,
expectedReplaced,
)
actual[key] = replaced.replacedActual
expected[key] = replaced.replacedExpected
Expand Down
21 changes: 13 additions & 8 deletions packages/vitest/src/utils/base.ts
Expand Up @@ -51,25 +51,30 @@ function getOwnProperties(obj: any) {
return Array.from(ownProps)
}

export function clone<T>(val: T): T {
let k: any, out: any, tmp: any
export function deepClone<T>(val: T): T {
const seen = new WeakMap()
return clone(val, seen)
}

export function clone<T>(val: T, seen: WeakMap<any, any>): T {
let k: any, out: any
if (seen.has(val))
return seen.get(val)
if (Array.isArray(val)) {
out = Array(k = val.length)
seen.set(val, out)
while (k--)
// eslint-disable-next-line no-cond-assign
out[k] = (tmp = val[k]) && typeof tmp === 'object' ? clone(tmp) : tmp
out[k] = clone(val[k], seen)
return out as any
}

if (Object.prototype.toString.call(val) === '[object Object]') {
out = Object.create(Object.getPrototypeOf(val))
seen.set(val, out)
// we don't need properties from prototype
const props = getOwnProperties(val)
for (const k of props) {
// eslint-disable-next-line no-cond-assign
out[k] = (tmp = (val as any)[k]) && typeof tmp === 'object' ? clone(tmp) : tmp
}
for (const k of props)
out[k] = clone((val as any)[k], seen)
return out
}

Expand Down
3 changes: 3 additions & 0 deletions test/core/test/replace-matcher.test.ts
Expand Up @@ -45,5 +45,8 @@ describe('replace asymmetric matcher', () => {
str: expect.any(String),
arr: [1, expect.anything()],
})
const circleObj: any = { name: 'circle', ref: null }
circleObj.ref = circleObj
expectReplaceAsymmetricMatcher(circleObj, circleObj)
})
})
25 changes: 14 additions & 11 deletions test/core/test/utils.spec.ts
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest'
import { assertTypes, clone, deepMerge, toArray } from '../../../packages/vitest/src/utils'
import { assertTypes, deepClone, deepMerge, toArray } from '../../../packages/vitest/src/utils'
import { deepMergeSnapshot } from '../../../packages/vitest/src/integrations/snapshot/port/utils'

describe('assertTypes', () => {
Expand Down Expand Up @@ -122,25 +122,28 @@ describe('toArray', () => {
})
})

describe('clone', () => {
describe('deepClone', () => {
test('various types should be cloned correctly', () => {
expect(clone(1)).toBe(1)
expect(clone(true)).toBe(true)
expect(clone(undefined)).toBe(undefined)
expect(clone(null)).toBe(null)
expect(clone({ a: 1 })).toEqual({ a: 1 })
expect(clone([1, 2])).toEqual([1, 2])
expect(deepClone(1)).toBe(1)
expect(deepClone(true)).toBe(true)
expect(deepClone(undefined)).toBe(undefined)
expect(deepClone(null)).toBe(null)
expect(deepClone({ a: 1 })).toEqual({ a: 1 })
expect(deepClone([1, 2])).toEqual([1, 2])
const symbolA = Symbol('a')
expect(clone(symbolA)).toBe(symbolA)
expect(deepClone(symbolA)).toBe(symbolA)
const objB: any = {}
Object.defineProperty(objB, 'value', {
configurable: false,
enumerable: false,
value: 1,
writable: false,
})
expect(clone(objB).value).toEqual(objB.value)
expect(deepClone(objB).value).toEqual(objB.value)
const objC = Object.create(objB)
expect(clone(objC).value).toEqual(objC.value)
expect(deepClone(objC).value).toEqual(objC.value)
const objD: any = { name: 'd', ref: null }
objD.ref = objD
expect(deepClone(objD)).toEqual(objD)
})
})

0 comments on commit 8452a7d

Please sign in to comment.