diff --git a/src/utils/stringify.ts b/src/utils/stringify.ts index aaf6605269d..31aa4618ec5 100644 --- a/src/utils/stringify.ts +++ b/src/utils/stringify.ts @@ -1,25 +1,58 @@ /** @hidden */ -function stringifyOne(value: Ts): string { - if (typeof value === 'string') return JSON.stringify(value); - - const defaultRepr: string = `${value}`; - if (/^\[object (Object|Null|Undefined)\]$/.exec(defaultRepr) === null) return defaultRepr; - try { - return JSON.stringify(value, (k, v) => { - if (typeof v === 'bigint') { - return v.toString() + 'n'; - } else { - return v; - } - }); - } catch (err) { - // ignored: object cannot be stringified using JSON.stringify +function stringifyNumber(numValue: number) { + switch (numValue) { + case 0: + return 1 / numValue === Number.NEGATIVE_INFINITY ? '-0' : '0'; + case Number.NEGATIVE_INFINITY: + return 'Number.NEGATIVE_INFINITY'; + case Number.POSITIVE_INFINITY: + return 'Number.POSITIVE_INFINITY'; + default: + return numValue === numValue ? String(numValue) : 'Number.NaN'; } - return defaultRepr; } /** @hidden */ export function stringify(value: Ts): string { - if (Array.isArray(value)) return `[${value.map(stringify).join(',')}]`; - return stringifyOne(value); + switch (Object.prototype.toString.call(value)) { + case '[object Array]': + return `[${(value as any).map(stringify).join(',')}]`; + case '[object BigInt]': + return `${value}n`; + case '[object Boolean]': + return typeof value === 'boolean' ? JSON.stringify(value) : `new Boolean(${JSON.stringify(value)})`; + case '[object Map]': + return `new Map(${stringify(Array.from(value as any))})`; + case '[object Null]': + return `null`; + case '[object Number]': + return typeof value === 'number' ? stringifyNumber(value) : `new Number(${stringifyNumber(Number(value))})`; + case '[object Object]': + const defaultRepr: string = `${value}`; + if (defaultRepr !== '[object Object]') return defaultRepr; + try { + return ( + '{' + + Object.keys(value) + .map(k => `${JSON.stringify(k)}:${stringify((value as any)[k])}`) + .join(',') + + '}' + ); + } catch (err) { + if (err instanceof RangeError) return '[cyclic]'; + return '[object Object]'; + } + case '[object Set]': + return `new Set(${stringify(Array.from(value as any))})`; + case '[object String]': + return typeof value === 'string' ? JSON.stringify(value) : `new String(${JSON.stringify(value)})`; + case '[object Undefined]': + return `undefined`; + default: + try { + return `${value}`; + } catch { + return Object.prototype.toString.call(value); + } + } } diff --git a/test/unit/utils/stringify.spec.ts b/test/unit/utils/stringify.spec.ts index 8e5cd11262c..78ab29777ea 100644 --- a/test/unit/utils/stringify.spec.ts +++ b/test/unit/utils/stringify.spec.ts @@ -4,6 +4,15 @@ import { stringify } from '../../../src/utils/stringify'; declare function BigInt(n: number | bigint | string): bigint; +const checkEqual = (a: any, b: any): boolean => { + try { + expect(a).toEqual(b); + return true; + } catch (err) { + return false; + } +}; + describe('stringify', () => { it('Should be able to stringify fc.anything()', () => fc.assert(fc.property(fc.anything(), a => typeof stringify(a) === 'string'))); @@ -11,6 +20,66 @@ describe('stringify', () => { fc.assert(fc.property(fc.char16bits(), a => typeof stringify(a) === 'string'))); if (typeof BigInt !== 'undefined') { it('Should be able to stringify bigint in object correctly', () => - fc.assert(fc.property(fc.bigInt(), b => stringify({ b }) === '{"b":"' + b + 'n"}'))); + fc.assert(fc.property(fc.bigInt(), b => stringify({ b }) === '{"b":' + b + 'n}'))); } + it('Should be equivalent to JSON.stringify for JSON compliant objects', () => + fc.assert( + fc.property( + fc.anything({ values: [fc.boolean(), fc.integer(), fc.double(), fc.fullUnicodeString(), fc.constant(null)] }), + obj => { + expect(stringify(obj)).toEqual(JSON.stringify(obj)); + } + ) + )); + it('Should be readable from eval', () => + fc.assert( + fc.property(fc.anything(), obj => { + expect(eval(`(function() { return ${stringify(obj)}; })()`)).toStrictEqual(obj); + }) + )); + it('Should stringify differently distinct objects', () => + fc.assert( + fc.property(fc.anything(), fc.anything(), (a, b) => { + fc.pre(!checkEqual(a, b)); + expect(stringify(a)).not.toEqual(stringify(b)); + }) + )); + it('Should be able to stringify cyclic object', () => { + let cyclic: any = { a: 1, b: 2, c: 3 }; + cyclic.b = cyclic; + const repr = stringify(cyclic); + expect(repr).toContain('"a"'); + expect(repr).toContain('"b"'); + expect(repr).toContain('"c"'); + expect(repr).toContain('[cyclic]'); + }); + it('Should be able to stringify values', () => { + expect(stringify(null)).toEqual('null'); + expect(stringify(undefined)).toEqual('undefined'); + expect(stringify(false)).toEqual('false'); + expect(stringify(42)).toEqual('42'); + expect(stringify(-0)).toEqual('-0'); + expect(stringify(Number.POSITIVE_INFINITY)).toEqual('Number.POSITIVE_INFINITY'); + expect(stringify(Number.NEGATIVE_INFINITY)).toEqual('Number.NEGATIVE_INFINITY'); + expect(stringify(Number.NaN)).toEqual('Number.NaN'); + expect(stringify('Hello')).toEqual('"Hello"'); + if (typeof BigInt !== 'undefined') { + expect(stringify(BigInt(42))).toEqual('42n'); + } + }); + it('Should be able to stringify boxed values', () => { + expect(stringify(new Boolean(false))).toEqual('new Boolean(false)'); + expect(stringify(new Number(42))).toEqual('new Number(42)'); + expect(stringify(new Number(-0))).toEqual('new Number(-0)'); + expect(stringify(new Number(Number.POSITIVE_INFINITY))).toEqual('new Number(Number.POSITIVE_INFINITY)'); + expect(stringify(new Number(Number.NEGATIVE_INFINITY))).toEqual('new Number(Number.NEGATIVE_INFINITY)'); + expect(stringify(new Number(Number.NaN))).toEqual('new Number(Number.NaN)'); + expect(stringify(new String('Hello'))).toEqual('new String("Hello")'); + }); + it('Should be able to stringify Set', () => { + expect(stringify(new Set([1, 2]))).toEqual('new Set([1,2])'); + }); + it('Should be able to stringify Map', () => { + expect(stringify(new Map([[1, 2]]))).toEqual('new Map([[1,2]])'); + }); });