From 169d52767eee4de541e86df05ab1a972edde218d Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Sun, 3 Mar 2019 21:44:49 +0200 Subject: [PATCH 1/6] inspect: Limit maximum number of printed items to 10 --- src/jsutils/__tests__/inspect-test.js | 12 ++++++++++++ src/jsutils/inspect.js | 22 +++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/jsutils/__tests__/inspect-test.js b/src/jsutils/__tests__/inspect-test.js index db9eda5ed5..37f63a632f 100644 --- a/src/jsutils/__tests__/inspect-test.js +++ b/src/jsutils/__tests__/inspect-test.js @@ -54,6 +54,18 @@ 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([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', () => { diff --git a/src/jsutils/inspect.js b/src/jsutils/inspect.js index a1d6fbb01d..9d2f76ac32 100644 --- a/src/jsutils/inspect.js +++ b/src/jsutils/inspect.js @@ -9,6 +9,8 @@ import nodejsCustomInspectSymbol from './nodejsCustomInspectSymbol'; +const MAX_ARRAY_LENGTH = 10; + /** * Used to print values in error messages. */ @@ -29,7 +31,7 @@ export default function inspect(value: mixed): string { ? customValue : inspect(customValue); } else if (Array.isArray(value)) { - return '[' + value.map(inspect).join(', ') + ']'; + return inspectArray(value); } const properties = Object.keys(value) @@ -43,6 +45,24 @@ export default function inspect(value: mixed): string { } } +function inspectArray(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(inspect(array[i])); + } + + 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)]; From e4678f557e402ccc6ef7c5696fdab403ca57c769 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Sun, 3 Mar 2019 22:33:35 +0200 Subject: [PATCH 2/6] inspect: move implementation into `formatValue` --- src/jsutils/inspect.js | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/jsutils/inspect.js b/src/jsutils/inspect.js index 9d2f76ac32..cfafd9dfc5 100644 --- a/src/jsutils/inspect.js +++ b/src/jsutils/inspect.js @@ -15,6 +15,10 @@ const MAX_ARRAY_LENGTH = 10; * Used to print values in error messages. */ export default function inspect(value: mixed): string { + return formatValue(value); +} + +function formatValue(value) { switch (typeof value) { case 'string': return JSON.stringify(value); @@ -29,29 +33,45 @@ export default function inspect(value: mixed): string { const customValue = customInspectFn.call(value); return typeof customValue === 'string' ? customValue - : inspect(customValue); + : formatValue(customValue); } else if (Array.isArray(value)) { - return inspectArray(value); + return formatArray(value); } - const properties = Object.keys(value) - .map(k => `${k}: ${inspect(value[k])}`) - .join(', '); - return properties ? '{ ' + properties + ' }' : '{}'; + return formatObject(value); } + return String(value); default: return String(value); } } -function inspectArray(array) { +function formatObject(object) { + const keys = Object.keys(object); + if (keys.length === 0) { + return '{}'; + } + + const properties = keys.map(key => { + const value = formatValue(object[key]); + return key + ': ' + value; + }); + + return '{ ' + properties.join(', ') + ' }'; +} + +function formatArray(array) { + if (array.length === 0) { + return '[]'; + } + 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(inspect(array[i])); + items.push(formatValue(array[i])); } if (remaining === 1) { From 5eb3bb69c59656a454e1527273fb3a805a2b7848 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Mon, 4 Mar 2019 19:20:59 +0200 Subject: [PATCH 3/6] inspect: Limit maximum depth of printed objects --- src/jsutils/__tests__/inspect-test.js | 21 +++++++++++++ src/jsutils/inspect.js | 43 +++++++++++++++++++++------ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/jsutils/__tests__/inspect-test.js b/src/jsutils/__tests__/inspect-test.js index 37f63a632f..c89e7f976b 100644 --- a/src/jsutils/__tests__/inspect-test.js +++ b/src/jsutils/__tests__/inspect-test.js @@ -55,6 +55,9 @@ describe('inspect', () => { 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]', ); @@ -74,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; @@ -125,4 +131,19 @@ describe('inspect', () => { expect(inspect(object)).to.equal('Hello World!'); }); + + 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 cfafd9dfc5..dc7efde8b2 100644 --- a/src/jsutils/inspect.js +++ b/src/jsutils/inspect.js @@ -10,15 +10,16 @@ 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); + return formatValue(value, 0); } -function formatValue(value) { +function formatValue(value, recurseTimes) { switch (typeof value) { case 'string': return JSON.stringify(value); @@ -33,12 +34,12 @@ function formatValue(value) { const customValue = customInspectFn.call(value); return typeof customValue === 'string' ? customValue - : formatValue(customValue); + : formatValue(customValue, recurseTimes); } else if (Array.isArray(value)) { - return formatArray(value); + return formatArray(value, recurseTimes); } - return formatObject(value); + return formatObject(value, recurseTimes); } return String(value); @@ -47,31 +48,39 @@ function formatValue(value) { } } -function formatObject(object) { +function formatObject(object, recurseTimes) { const keys = Object.keys(object); if (keys.length === 0) { return '{}'; } + if (recurseTimes === MAX_RECURSIVE_DEPTH) { + return '[' + getObjectTag(object) + ']'; + } + const properties = keys.map(key => { - const value = formatValue(object[key]); + const value = formatValue(object[key], recurseTimes + 1); return key + ': ' + value; }); return '{ ' + properties.join(', ') + ' }'; } -function formatArray(array) { +function formatArray(array, recurseTimes) { if (array.length === 0) { return '[]'; } + if (recurseTimes === 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])); + items.push(formatValue(array[i], recurseTimes + 1)); } if (remaining === 1) { @@ -94,3 +103,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; +} From 8f2c38333a47e63a12d0e9e77e485154dabc018b Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Thu, 14 Mar 2019 11:40:56 +0200 Subject: [PATCH 4/6] inspect: Correctly handle custom inspect returning `this` --- src/jsutils/__tests__/inspect-test.js | 10 ++++++++++ src/jsutils/inspect.js | 10 +++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/jsutils/__tests__/inspect-test.js b/src/jsutils/__tests__/inspect-test.js index c89e7f976b..b8e9ae4d1a 100644 --- a/src/jsutils/__tests__/inspect-test.js +++ b/src/jsutils/__tests__/inspect-test.js @@ -96,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); diff --git a/src/jsutils/inspect.js b/src/jsutils/inspect.js index dc7efde8b2..5ff273cda5 100644 --- a/src/jsutils/inspect.js +++ b/src/jsutils/inspect.js @@ -32,9 +32,13 @@ function formatValue(value, recurseTimes) { if (customInspectFn) { // $FlowFixMe(>=0.90.0) const customValue = customInspectFn.call(value); - return typeof customValue === 'string' - ? customValue - : formatValue(customValue, recurseTimes); + + // check for infinite recursion + if (customValue !== value) { + return typeof customValue === 'string' + ? customValue + : formatValue(customValue, recurseTimes); + } } else if (Array.isArray(value)) { return formatArray(value, recurseTimes); } From 2700388936350f1a1495de743ce3c65ef1fd9546 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Thu, 14 Mar 2019 13:00:07 +0200 Subject: [PATCH 5/6] inspect: detect circular objects --- src/jsutils/__tests__/inspect-test.js | 21 +++++++++ src/jsutils/inspect.js | 67 +++++++++++++++------------ 2 files changed, 59 insertions(+), 29 deletions(-) diff --git a/src/jsutils/__tests__/inspect-test.js b/src/jsutils/__tests__/inspect-test.js index b8e9ae4d1a..140e91c2bc 100644 --- a/src/jsutils/__tests__/inspect-test.js +++ b/src/jsutils/__tests__/inspect-test.js @@ -86,6 +86,27 @@ describe('inspect', () => { expect(inspect(map)).to.equal('{ a: true, b: null }'); }); + 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]] }'); + }); + it('custom inspect', () => { const object = { inspect() { diff --git a/src/jsutils/inspect.js b/src/jsutils/inspect.js index 5ff273cda5..e277dd1504 100644 --- a/src/jsutils/inspect.js +++ b/src/jsutils/inspect.js @@ -16,66 +16,75 @@ const MAX_RECURSIVE_DEPTH = 2; * Used to print values in error messages. */ export default function inspect(value: mixed): string { - return formatValue(value, 0); + return formatValue(value, []); } -function formatValue(value, recurseTimes) { +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); - - // check for infinite recursion - if (customValue !== value) { - return typeof customValue === 'string' - ? customValue - : formatValue(customValue, recurseTimes); - } - } else if (Array.isArray(value)) { - return formatArray(value, recurseTimes); - } - - return formatObject(value, recurseTimes); - } - - return String(value); + return formatObjectValue(value, seenValues); default: return String(value); } } -function formatObject(object, recurseTimes) { +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 (recurseTimes === MAX_RECURSIVE_DEPTH) { + if (seenValues.length > MAX_RECURSIVE_DEPTH) { return '[' + getObjectTag(object) + ']'; } const properties = keys.map(key => { - const value = formatValue(object[key], recurseTimes + 1); + const value = formatValue(object[key], seenValues); return key + ': ' + value; }); return '{ ' + properties.join(', ') + ' }'; } -function formatArray(array, recurseTimes) { +function formatArray(array, seenValues) { if (array.length === 0) { return '[]'; } - if (recurseTimes === MAX_RECURSIVE_DEPTH) { + if (seenValues.length > MAX_RECURSIVE_DEPTH) { return '[Array]'; } @@ -84,7 +93,7 @@ function formatArray(array, recurseTimes) { const items = []; for (let i = 0; i < len; ++i) { - items.push(formatValue(array[i], recurseTimes + 1)); + items.push(formatValue(array[i], seenValues)); } if (remaining === 1) { From fadbc7844aacfa09e068f55dbaeeb184d005d1c7 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Fri, 15 Mar 2019 04:21:44 +0200 Subject: [PATCH 6/6] inspect: Add test for circular object as result of custom inspect --- src/jsutils/__tests__/inspect-test.js | 52 ++++++++++++++++----------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/jsutils/__tests__/inspect-test.js b/src/jsutils/__tests__/inspect-test.js index 140e91c2bc..d1bfe89d25 100644 --- a/src/jsutils/__tests__/inspect-test.js +++ b/src/jsutils/__tests__/inspect-test.js @@ -86,27 +86,6 @@ describe('inspect', () => { expect(inspect(map)).to.equal('{ a: true, b: null }'); }); - 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]] }'); - }); - it('custom inspect', () => { const object = { inspect() { @@ -163,6 +142,37 @@ 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;