Skip to content

Commit

Permalink
Earlier cycle detection in stringify
Browse files Browse the repository at this point in the history
  • Loading branch information
dubzzz committed Feb 26, 2019
1 parent 6691b99 commit 8e97887
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 9 deletions.
27 changes: 18 additions & 9 deletions src/utils/stringify.ts
Expand Up @@ -13,46 +13,55 @@ function stringifyNumber(numValue: number) {
}

/** @hidden */
export function stringify<Ts>(value: Ts): string {
export function stringifyInternal<Ts>(value: Ts, previousValues: any[]): string {
const currentValues = previousValues.concat([value]);
if (typeof value === 'object') {
// early cycle detection for objects
if (previousValues.indexOf(value) !== -1) return '[cyclic]';
}
switch (Object.prototype.toString.call(value)) {
case '[object Array]':
return `[${(value as any).map(stringify).join(',')}]`;
return `[${(value as any).map((v: any) => stringifyInternal(v, currentValues)).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))})`;
return `new Map(${stringifyInternal(Array.from(value as any), currentValues)})`;
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 {
const defaultRepr: string = value.toString();
if (defaultRepr !== '[object Object]') return defaultRepr;
return (
'{' +
Object.keys(value)
.map(k => `${JSON.stringify(k)}:${stringify((value as any)[k])}`)
.map(k => `${JSON.stringify(k)}:${stringifyInternal((value as any)[k], currentValues)}`)
.join(',') +
'}'
);
} catch (err) {
if (err instanceof RangeError) return '[cyclic]';
return '[object Object]';
}
case '[object Set]':
return `new Set(${stringify(Array.from(value as any))})`;
return `new Set(${stringifyInternal(Array.from(value as any), currentValues)})`;
case '[object String]':
return typeof value === 'string' ? JSON.stringify(value) : `new String(${JSON.stringify(value)})`;
case '[object Undefined]':
return `undefined`;
default:
try {
return `${value}`;
return value.toString();
} catch {
return Object.prototype.toString.call(value);
}
}
}

/** @hidden */
export function stringify<Ts>(value: Ts): string {
return stringifyInternal(value, []);
}
44 changes: 44 additions & 0 deletions test/unit/utils/stringify.spec.ts
Expand Up @@ -13,6 +13,19 @@ const checkEqual = (a: any, b: any): boolean => {
}
};

class ThrowingToString {
toString = () => {
throw new Error('No toString');
};
}

class CustomTagThrowingToString {
[Symbol.toStringTag] = 'CustomTagThrowingToString';
toString = () => {
throw new Error('No toString');
};
}

describe('stringify', () => {
it('Should be able to stringify fc.anything()', () =>
fc.assert(fc.property(fc.anything(), a => typeof stringify(a) === 'string')));
Expand Down Expand Up @@ -52,6 +65,30 @@ describe('stringify', () => {
expect(repr).toContain('"b"');
expect(repr).toContain('"c"');
expect(repr).toContain('[cyclic]');
expect(repr).toEqual('{"a":1,"b":[cyclic],"c":3}');
});
it('Should be able to stringify cyclic arrays', () => {
let cyclic: any[] = [1, 2, 3];
cyclic.push(cyclic);
cyclic.push(4);
const repr = stringify(cyclic);
expect(repr).toEqual('[1,2,3,[cyclic],4]');
});
it('Should be able to stringify cyclic sets', () => {
let cyclic: Set<any> = new Set([1, 2, 3]);
cyclic.add(cyclic);
cyclic.add(4);
const repr = stringify(cyclic);
expect(repr).toEqual('new Set([1,2,3,[cyclic],4])');
});
it('Should be able to stringify cyclic maps', () => {
let cyclic: Map<any, any> = new Map();
cyclic.set(1, 2);
cyclic.set(3, cyclic);
cyclic.set(cyclic, 4);
cyclic.set(5, 6);
const repr = stringify(cyclic);
expect(repr).toEqual('new Map([[1,2],[3,[cyclic]],[[cyclic],4],[5,6]])');
});
it('Should be able to stringify values', () => {
expect(stringify(null)).toEqual('null');
Expand Down Expand Up @@ -82,4 +119,11 @@ describe('stringify', () => {
it('Should be able to stringify Map', () => {
expect(stringify(new Map([[1, 2]]))).toEqual('new Map([[1,2]])');
});
it('Should be able to stringify Symbol', () => {
expect(stringify(Symbol.for('fc'))).toEqual('Symbol(fc)');
});
it('Should be only produce toStringTag for failing toString', () => {
expect(stringify(new ThrowingToString())).toEqual('[object Object]');
expect(stringify(new CustomTagThrowingToString())).toEqual('[object CustomTagThrowingToString]');
});
});

0 comments on commit 8e97887

Please sign in to comment.