diff --git a/src/jsutils/__tests__/inspect-test.js b/src/jsutils/__tests__/inspect-test.js index db9eda5ed5..d1bfe89d25 100644 --- a/src/jsutils/__tests__/inspect-test.js +++ b/src/jsutils/__tests__/inspect-test.js @@ -54,6 +54,21 @@ describe('inspect', () => { expect(inspect([null])).to.equal('[null]'); expect(inspect([1, NaN])).to.equal('[1, NaN]'); expect(inspect([['a', 'b'], 'c'])).to.equal('[["a", "b"], "c"]'); + + expect(inspect([[[]]])).to.equal('[[[]]]'); + expect(inspect([[['a']]])).to.equal('[[[Array]]]'); + + expect(inspect([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])).to.equal( + '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]', + ); + + expect(inspect([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])).to.equal( + '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 1 more item]', + ); + + expect(inspect([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])).to.equal( + '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 2 more items]', + ); }); it('object', () => { @@ -62,6 +77,9 @@ describe('inspect', () => { expect(inspect({ a: 1, b: 2 })).to.equal('{ a: 1, b: 2 }'); expect(inspect({ array: [null, 0] })).to.equal('{ array: [null, 0] }'); + expect(inspect({ a: { b: {} } })).to.equal('{ a: { b: {} } }'); + expect(inspect({ a: { b: { c: 1 } } })).to.equal('{ a: { b: [Object] } }'); + const map = Object.create(null); map['a'] = true; map['b'] = null; @@ -78,6 +96,16 @@ describe('inspect', () => { expect(inspect(object)).to.equal(''); }); + it('custom inspect that return `this` should work', () => { + const object = { + inspect() { + return this; + }, + }; + + expect(inspect(object)).to.equal('{ inspect: [function inspect] }'); + }); + it('custom symbol inspect is take precedence', () => { invariant(nodejsCustomInspectSymbol); @@ -113,4 +141,50 @@ describe('inspect', () => { expect(inspect(object)).to.equal('Hello World!'); }); + + it('detect circular objects', () => { + const obj = {}; + obj.self = obj; + obj.deepSelf = { self: obj }; + + expect(inspect(obj)).to.equal( + '{ self: [Circular], deepSelf: { self: [Circular] } }', + ); + + const array = []; + array[0] = array; + array[1] = [array]; + + expect(inspect(array)).to.equal('[[Circular], [[Circular]]]'); + + const mixed = { array: [] }; + mixed.array[0] = mixed; + + expect(inspect(mixed)).to.equal('{ array: [[Circular]] }'); + + const customA = { + inspect: () => customB, + }; + + const customB = { + inspect: () => customA, + }; + + expect(inspect(customA)).to.equal('[Circular]'); + }); + + it('Use class names for the shortform of an object', () => { + class Foo { + foo: string; + + constructor() { + this.foo = 'bar'; + } + } + + expect(inspect([[new Foo()]])).to.equal('[[[Foo]]]'); + + (Foo.prototype: any)[Symbol.toStringTag] = 'Bar'; + expect(inspect([[new Foo()]])).to.equal('[[[Bar]]]'); + }); }); diff --git a/src/jsutils/inspect.js b/src/jsutils/inspect.js index a1d6fbb01d..e277dd1504 100644 --- a/src/jsutils/inspect.js +++ b/src/jsutils/inspect.js @@ -9,40 +9,102 @@ import nodejsCustomInspectSymbol from './nodejsCustomInspectSymbol'; +const MAX_ARRAY_LENGTH = 10; +const MAX_RECURSIVE_DEPTH = 2; + /** * Used to print values in error messages. */ export default function inspect(value: mixed): string { + return formatValue(value, []); +} + +function formatValue(value, seenValues) { switch (typeof value) { case 'string': return JSON.stringify(value); case 'function': return value.name ? `[function ${value.name}]` : '[function]'; case 'object': - if (value) { - const customInspectFn = getCustomFn(value); - - if (customInspectFn) { - // $FlowFixMe(>=0.90.0) - const customValue = customInspectFn.call(value); - return typeof customValue === 'string' - ? customValue - : inspect(customValue); - } else if (Array.isArray(value)) { - return '[' + value.map(inspect).join(', ') + ']'; - } - - const properties = Object.keys(value) - .map(k => `${k}: ${inspect(value[k])}`) - .join(', '); - return properties ? '{ ' + properties + ' }' : '{}'; - } - return String(value); + return formatObjectValue(value, seenValues); default: return String(value); } } +function formatObjectValue(value, previouslySeenValues) { + if (previouslySeenValues.indexOf(value) !== -1) { + return '[Circular]'; + } + const seenValues = [...previouslySeenValues, value]; + + if (value) { + const customInspectFn = getCustomFn(value); + + if (customInspectFn) { + // $FlowFixMe(>=0.90.0) + const customValue = customInspectFn.call(value); + + // check for infinite recursion + if (customValue !== value) { + return typeof customValue === 'string' + ? customValue + : formatValue(customValue, seenValues); + } + } else if (Array.isArray(value)) { + return formatArray(value, seenValues); + } + + return formatObject(value, seenValues); + } + + return String(value); +} + +function formatObject(object, seenValues) { + const keys = Object.keys(object); + if (keys.length === 0) { + return '{}'; + } + + if (seenValues.length > MAX_RECURSIVE_DEPTH) { + return '[' + getObjectTag(object) + ']'; + } + + const properties = keys.map(key => { + const value = formatValue(object[key], seenValues); + return key + ': ' + value; + }); + + return '{ ' + properties.join(', ') + ' }'; +} + +function formatArray(array, seenValues) { + if (array.length === 0) { + return '[]'; + } + + if (seenValues.length > MAX_RECURSIVE_DEPTH) { + return '[Array]'; + } + + const len = Math.min(MAX_ARRAY_LENGTH, array.length); + const remaining = array.length - len; + const items = []; + + for (let i = 0; i < len; ++i) { + items.push(formatValue(array[i], seenValues)); + } + + if (remaining === 1) { + items.push('... 1 more item'); + } else if (remaining > 1) { + items.push(`... ${remaining} more items`); + } + + return '[' + items.join(', ') + ']'; +} + function getCustomFn(object) { const customInspectFn = object[String(nodejsCustomInspectSymbol)]; @@ -54,3 +116,19 @@ function getCustomFn(object) { return object.inspect; } } + +function getObjectTag(object) { + const tag = Object.prototype.toString + .call(object) + .replace(/^\[object /, '') + .replace(/]$/, ''); + + if (tag === 'Object' && typeof object.constructor === 'function') { + const name = object.constructor.name; + if (typeof name === 'string') { + return name; + } + } + + return tag; +}