From c18ac52970b116b7d17ab7ff842e9ab5f60881f5 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sat, 9 Dec 2017 05:27:21 -0200 Subject: [PATCH] util: add util.inspect compact option The current default formatting is not ideal and this improves the situation by formatting the output more intuitiv. 1) All object keys are now indented by 2 characters instead of sometimes 2 and sometimes 3 characters. 2) Each object key will now use an individual line instead of sharing a line potentially with multiple object keys. 3) Long strings will now be split into multiple lines in case they exceed the "lineBreak" option length (including the current indentation). 4) Opening braces are now directly behind a object property instead of using a new line. 5) Switch inspect "base" order. In case the compact option is set to `false`, inspect will now print "[Function: foo] {\n property: 'data'\n}" instead of "{ [Function: foo]\n property: 'data'\n}". Backport-PR-URL: https://github.com/nodejs/node/pull/19230 PR-URL: https://github.com/nodejs/node/pull/17576 Reviewed-By: Anna Henningsen --- doc/api/util.md | 67 ++++++++++++++ lib/util.js | 84 +++++++++++++---- test/parallel/test-util-inspect.js | 143 +++++++++++++++++++++++++++++ 3 files changed, 277 insertions(+), 17 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index a001124824f0a5..bc1d3ae262ae50 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -316,6 +316,9 @@ stream.write('With ES6'); diff --git a/lib/util.js b/lib/util.js index bb805a082b27d6..70fd1a05564389 100644 --- a/lib/util.js +++ b/lib/util.js @@ -68,7 +68,8 @@ const inspectDefaultOptions = Object.seal({ customInspect: true, showProxy: false, maxArrayLength: 100, - breakLength: 60 + breakLength: 60, + compact: true }); const propertyIsEnumerable = Object.prototype.propertyIsEnumerable; @@ -86,6 +87,10 @@ const strEscapeSequencesReplacer = /[\x00-\x1f\x27\x5c]/g; const keyStrRegExp = /^[a-zA-Z_][a-zA-Z_0-9]*$/; const numberRegExp = /^(0|[1-9][0-9]*)$/; +const readableRegExps = {}; + +const MIN_LINE_LENGTH = 16; + // Escaped special characters. Use empty strings to fill up unused entries. const meta = [ '\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004', @@ -276,7 +281,8 @@ function inspect(value, opts) { showProxy: inspectDefaultOptions.showProxy, maxArrayLength: inspectDefaultOptions.maxArrayLength, breakLength: inspectDefaultOptions.breakLength, - indentationLvl: 0 + indentationLvl: 0, + compact: inspectDefaultOptions.compact }; // Legacy... if (arguments.length > 2) { @@ -374,7 +380,7 @@ function ensureDebugIsInitialized() { function formatValue(ctx, value, recurseTimes, ln) { // Primitive types cannot have properties if (typeof value !== 'object' && typeof value !== 'function') { - return formatPrimitive(ctx.stylize, value); + return formatPrimitive(ctx.stylize, value, ctx); } if (value === null) { return ctx.stylize('null', 'null'); @@ -481,10 +487,10 @@ function formatValue(ctx, value, recurseTimes, ln) { } catch (e) { /* ignore */ } if (typeof raw === 'string') { - const formatted = formatPrimitive(stylizeNoColor, raw); + const formatted = formatPrimitive(stylizeNoColor, raw, ctx); if (keyLength === raw.length) return ctx.stylize(`[String: ${formatted}]`, 'string'); - base = ` [String: ${formatted}]`; + base = `[String: ${formatted}]`; // For boxed Strings, we have to remove the 0-n indexed entries, // since they just noisy up the output and are redundant // Make boxed primitive Strings look like such @@ -505,12 +511,12 @@ function formatValue(ctx, value, recurseTimes, ln) { const name = `${constructor.name}${value.name ? `: ${value.name}` : ''}`; if (keyLength === 0) return ctx.stylize(`[${name}]`, 'special'); - base = ` [${name}]`; + base = `[${name}]`; } else if (isRegExp(value)) { // Make RegExps say that they are RegExps if (keyLength === 0 || recurseTimes < 0) return ctx.stylize(regExpToString.call(value), 'regexp'); - base = ` ${regExpToString.call(value)}`; + base = `${regExpToString.call(value)}`; } else if (isDate(value)) { if (keyLength === 0) { if (Number.isNaN(value.getTime())) @@ -518,12 +524,12 @@ function formatValue(ctx, value, recurseTimes, ln) { return ctx.stylize(dateToISOString.call(value), 'date'); } // Make dates with properties first say the date - base = ` ${dateToISOString.call(value)}`; + base = `${dateToISOString.call(value)}`; } else if (isError(value)) { // Make error with message first say the error if (keyLength === 0) return formatError(value); - base = ` ${formatError(value)}`; + base = `${formatError(value)}`; } else if (isAnyArrayBuffer(value)) { // Fast path for ArrayBuffer and SharedArrayBuffer. // Can't do the same for DataView because it has a non-primitive @@ -553,13 +559,13 @@ function formatValue(ctx, value, recurseTimes, ln) { const formatted = formatPrimitive(stylizeNoColor, raw); if (keyLength === 0) return ctx.stylize(`[Number: ${formatted}]`, 'number'); - base = ` [Number: ${formatted}]`; + base = `[Number: ${formatted}]`; } else if (typeof raw === 'boolean') { // Make boxed primitive Booleans look like such const formatted = formatPrimitive(stylizeNoColor, raw); if (keyLength === 0) return ctx.stylize(`[Boolean: ${formatted}]`, 'boolean'); - base = ` [Boolean: ${formatted}]`; + base = `[Boolean: ${formatted}]`; } else if (typeof raw === 'symbol') { const formatted = formatPrimitive(stylizeNoColor, raw); return ctx.stylize(`[Symbol: ${formatted}]`, 'symbol'); @@ -603,9 +609,42 @@ function formatNumber(fn, value) { return fn(`${value}`, 'number'); } -function formatPrimitive(fn, value) { - if (typeof value === 'string') +function formatPrimitive(fn, value, ctx) { + if (typeof value === 'string') { + if (ctx.compact === false && + value.length > MIN_LINE_LENGTH && + ctx.indentationLvl + value.length > ctx.breakLength) { + // eslint-disable-next-line max-len + const minLineLength = Math.max(ctx.breakLength - ctx.indentationLvl, MIN_LINE_LENGTH); + // eslint-disable-next-line max-len + const averageLineLength = Math.ceil(value.length / Math.ceil(value.length / minLineLength)); + const divisor = Math.max(averageLineLength, MIN_LINE_LENGTH); + var res = ''; + if (readableRegExps[divisor] === undefined) { + // Build a new RegExp that naturally breaks text into multiple lines. + // + // Rules + // 1. Greedy match all text up the max line length that ends with a + // whitespace or the end of the string. + // 2. If none matches, non-greedy match any text up to a whitespace or + // the end of the string. + // + // eslint-disable-next-line max-len, no-unescaped-regexp-dot + readableRegExps[divisor] = new RegExp(`(.|\\n){1,${divisor}}(\\s|$)|(\\n|.)+?(\\s|$)`, 'gm'); + } + const indent = ' '.repeat(ctx.indentationLvl); + const matches = value.match(readableRegExps[divisor]); + if (matches.length > 1) { + res += `${fn(strEscape(matches[0]), 'string')} +\n`; + for (var i = 1; i < matches.length - 1; i++) { + res += `${indent} ${fn(strEscape(matches[i]), 'string')} +\n`; + } + res += `${indent} ${fn(strEscape(matches[i]), 'string')}`; + return res; + } + } return fn(strEscape(value), 'string'); + } if (typeof value === 'number') return formatNumber(fn, value); if (typeof value === 'boolean') @@ -806,7 +845,7 @@ function formatProperty(ctx, value, recurseTimes, key, array) { const desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key], enumerable: true }; if (desc.value !== undefined) { - const diff = array === 0 ? 3 : 2; + const diff = array !== 0 || ctx.compact === false ? 2 : 3; ctx.indentationLvl += diff; str = formatValue(ctx, desc.value, recurseTimes, array === 0); ctx.indentationLvl -= diff; @@ -839,9 +878,19 @@ function formatProperty(ctx, value, recurseTimes, key, array) { function reduceToSingleString(ctx, output, base, braces, addLn) { const breakLength = ctx.breakLength; + var i = 0; + if (ctx.compact === false) { + const indentation = ' '.repeat(ctx.indentationLvl); + var res = `${base ? `${base} ` : ''}${braces[0]}\n${indentation} `; + for (; i < output.length - 1; i++) { + res += `${output[i]},\n${indentation} `; + } + res += `${output[i]}\n${indentation}${braces[1]}`; + return res; + } if (output.length * 2 <= breakLength) { var length = 0; - for (var i = 0; i < output.length && length <= breakLength; i++) { + for (; i < output.length && length <= breakLength; i++) { if (ctx.colors) { length += removeColors(output[i]).length + 1; } else { @@ -849,7 +898,8 @@ function reduceToSingleString(ctx, output, base, braces, addLn) { } } if (length <= breakLength) - return `${braces[0]}${base} ${join(output, ', ')} ${braces[1]}`; + return `${braces[0]}${base ? ` ${base}` : ''} ${join(output, ', ')} ` + + braces[1]; } // If the opening "brace" is too large, like in the case of "Set {", // we need to force the first item to be on the next line or the @@ -857,7 +907,7 @@ function reduceToSingleString(ctx, output, base, braces, addLn) { const indentation = ' '.repeat(ctx.indentationLvl); const extraLn = addLn === true ? `\n${indentation}` : ''; const ln = base === '' && braces[0].length === 1 ? - ' ' : `${base}\n${indentation} `; + ' ' : `${base ? ` ${base}` : base}\n${indentation} `; const str = join(output, `,\n${indentation} `); return `${extraLn}${braces[0]}${ln}${str} ${braces[1]}`; } diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index a99583d454b3f4..83f6b469d68055 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -1160,3 +1160,146 @@ if (typeof Symbol !== 'undefined') { assert.doesNotThrow(() => util.inspect(process)); /* eslint-enable accessor-pairs */ + +// Setting custom inspect property to a non-function should do nothing. +{ + const obj = { inspect: 'fhqwhgads' }; + assert.strictEqual(util.inspect(obj), "{ inspect: 'fhqwhgads' }"); +} + +{ + const o = { + a: [1, 2, [[ + 'Lorem ipsum dolor\nsit amet,\tconsectetur adipiscing elit, sed do ' + + 'eiusmod tempor incididunt ut labore et dolore magna aliqua.', + 'test', + 'foo']], 4], + b: new Map([['za', 1], ['zb', 'test']]) + }; + + let out = util.inspect(o, { compact: true, depth: 5, breakLength: 80 }); + let expect = [ + '{ a: ', + ' [ 1,', + ' 2,', + " [ [ 'Lorem ipsum dolor\\nsit amet,\\tconsectetur adipiscing elit, " + + "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',", + " 'test',", + " 'foo' ] ],", + ' 4 ],', + " b: Map { 'za' => 1, 'zb' => 'test' } }", + ].join('\n'); + assert.strictEqual(out, expect); + + out = util.inspect(o, { compact: false, depth: 5, breakLength: 60 }); + expect = [ + '{', + ' a: [', + ' 1,', + ' 2,', + ' [', + ' [', + ' \'Lorem ipsum dolor\\nsit amet,\\tconsectetur \' +', + ' \'adipiscing elit, sed do eiusmod tempor \' +', + ' \'incididunt ut labore et dolore magna \' +', + ' \'aliqua.\',', + ' \'test\',', + ' \'foo\'', + ' ]', + ' ],', + ' 4', + ' ],', + ' b: Map {', + ' \'za\' => 1,', + ' \'zb\' => \'test\'', + ' }', + '}' + ].join('\n'); + assert.strictEqual(out, expect); + + out = util.inspect(o.a[2][0][0], { compact: false, breakLength: 30 }); + expect = [ + '\'Lorem ipsum dolor\\nsit \' +', + ' \'amet,\\tconsectetur \' +', + ' \'adipiscing elit, sed do \' +', + ' \'eiusmod tempor incididunt \' +', + ' \'ut labore et dolore magna \' +', + ' \'aliqua.\'' + ].join('\n'); + assert.strictEqual(out, expect); + + out = util.inspect( + '12345678901234567890123456789012345678901234567890', + { compact: false, breakLength: 3 }); + expect = '\'12345678901234567890123456789012345678901234567890\''; + assert.strictEqual(out, expect); + + out = util.inspect( + '12 45 78 01 34 67 90 23 56 89 123456789012345678901234567890', + { compact: false, breakLength: 3 }); + expect = [ + '\'12 45 78 01 34 \' +', + ' \'67 90 23 56 89 \' +', + ' \'123456789012345678901234567890\'' + ].join('\n'); + assert.strictEqual(out, expect); + + out = util.inspect( + '12 45 78 01 34 67 90 23 56 89 1234567890123 0', + { compact: false, breakLength: 3 }); + expect = [ + '\'12 45 78 01 34 \' +', + ' \'67 90 23 56 89 \' +', + ' \'1234567890123 0\'' + ].join('\n'); + assert.strictEqual(out, expect); + + out = util.inspect( + '12 45 78 01 34 67 90 23 56 89 12345678901234567 0', + { compact: false, breakLength: 3 }); + expect = [ + '\'12 45 78 01 34 \' +', + ' \'67 90 23 56 89 \' +', + ' \'12345678901234567 \' +', + ' \'0\'' + ].join('\n'); + assert.strictEqual(out, expect); + + o.a = () => {}; + o.b = new Number(3); + out = util.inspect(o, { compact: false, breakLength: 3 }); + expect = [ + '{', + ' a: [Function],', + ' b: [Number: 3]', + '}' + ].join('\n'); + assert.strictEqual(out, expect); + + out = util.inspect(o, { compact: false, breakLength: 3, showHidden: true }); + expect = [ + '{', + ' a: [Function] {', + ' [length]: 0,', + " [name]: ''", + ' },', + ' b: [Number: 3]', + '}' + ].join('\n'); + assert.strictEqual(out, expect); + + o[util.inspect.custom] = () => 42; + out = util.inspect(o, { compact: false, breakLength: 3 }); + expect = '42'; + assert.strictEqual(out, expect); + + o[util.inspect.custom] = () => '12 45 78 01 34 67 90 23'; + out = util.inspect(o, { compact: false, breakLength: 3 }); + expect = '12 45 78 01 34 67 90 23'; + assert.strictEqual(out, expect); + + o[util.inspect.custom] = () => ({ a: '12 45 78 01 34 67 90 23' }); + out = util.inspect(o, { compact: false, breakLength: 3 }); + expect = '{\n a: \'12 45 78 01 34 \' +\n \'67 90 23\'\n}'; + assert.strictEqual(out, expect); +}