diff options
author | Ruben Bridgewater <ruben@bridgewater.de> | 2019-11-30 11:07:12 +0100 |
---|---|---|
committer | Beth Griggs <Bethany.Griggs@uk.ibm.com> | 2020-02-06 02:49:39 +0000 |
commit | 7686865174ab637ecb8fbe96e2fccd0b395ef4b6 (patch) | |
tree | 9459b0894d897e6fb554b46017500cecf8e198f9 | |
parent | 0376e1cf4d08588b941c8e78072f4d8d687db207 (diff) | |
download | node-new-7686865174ab637ecb8fbe96e2fccd0b395ef4b6.tar.gz |
util: inspect (user defined) prototype properties
This is only active if the `showHidden` option is truthy.
The implementation is a trade-off between accuracy and performance.
This will miss properties such as properties added to built-in data
types.
The goal is mainly to visualize prototype getters and setters such as:
class Foo {
ownProperty = true
get bar() {
return 'Hello world!'
}
}
const a = new Foo()
The `bar` property is a non-enumerable property on the prototype while
`ownProperty` will be set directly on the created instance.
The output is similar to the one of Chromium when inspecting objects
closer. The output from Firefox is difficult to compare, since it's
always a structured interactive output and was therefore not taken
into account.
Backport-PR-URL: https://github.com/nodejs/node/pull/31431
PR-URL: https://github.com/nodejs/node/pull/30768
Fixes: https://github.com/nodejs/node/issues/30183
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Michaƫl Zasso <targos@protonmail.com>
-rw-r--r-- | doc/api/util.md | 7 | ||||
-rw-r--r-- | lib/internal/util/inspect.js | 116 | ||||
-rw-r--r-- | test/parallel/test-util-inspect.js | 80 | ||||
-rw-r--r-- | test/parallel/test-whatwg-encoding-custom-textdecoder.js | 17 |
4 files changed, 198 insertions, 22 deletions
diff --git a/doc/api/util.md b/doc/api/util.md index b26d2a9558..e202476467 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -398,6 +398,10 @@ stream.write('With ES6'); <!-- YAML added: v0.3.0 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/30768 + description: User defined prototype properties are inspected in case + `showHidden` is `true`. - version: v12.0.0 pr-url: https://github.com/nodejs/node/pull/27109 description: The `compact` options default is changed to `3` and the @@ -458,7 +462,8 @@ changes: * `options` {Object} * `showHidden` {boolean} If `true`, `object`'s non-enumerable symbols and properties are included in the formatted result. [`WeakMap`][] and - [`WeakSet`][] entries are also included. **Default:** `false`. + [`WeakSet`][] entries are also included as well as user defined prototype + properties (excluding method properties). **Default:** `false`. * `depth` {number} Specifies the number of times to recurse while formatting `object`. This is useful for inspecting large objects. To recurse up to the maximum call stack size pass `Infinity` or `null`. diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index dd8a863a81..0b773adada 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -450,7 +450,7 @@ function getEmptyFormatArray() { return []; } -function getConstructorName(obj, ctx, recurseTimes) { +function getConstructorName(obj, ctx, recurseTimes, protoProps) { let firstProto; const tmp = obj; while (obj) { @@ -458,6 +458,12 @@ function getConstructorName(obj, ctx, recurseTimes) { if (descriptor !== undefined && typeof descriptor.value === 'function' && descriptor.value.name !== '') { + if (protoProps !== undefined && + !builtInObjects.has(descriptor.value.name)) { + const isProto = firstProto !== undefined; + addPrototypeProperties( + ctx, tmp, obj, recurseTimes, isProto, protoProps); + } return descriptor.value.name; } @@ -477,7 +483,8 @@ function getConstructorName(obj, ctx, recurseTimes) { return `${res} <Complex prototype>`; } - const protoConstr = getConstructorName(firstProto, ctx, recurseTimes + 1); + const protoConstr = getConstructorName( + firstProto, ctx, recurseTimes + 1, protoProps); if (protoConstr === null) { return `${res} <${inspect(firstProto, { @@ -490,6 +497,68 @@ function getConstructorName(obj, ctx, recurseTimes) { return `${res} <${protoConstr}>`; } +// This function has the side effect of adding prototype properties to the +// `output` argument (which is an array). This is intended to highlight user +// defined prototype properties. +function addPrototypeProperties(ctx, main, obj, recurseTimes, isProto, output) { + let depth = 0; + let keys; + let keySet; + do { + if (!isProto) { + obj = ObjectGetPrototypeOf(obj); + // Stop as soon as a null prototype is encountered. + if (obj === null) { + return; + } + // Stop as soon as a built-in object type is detected. + const descriptor = ObjectGetOwnPropertyDescriptor(obj, 'constructor'); + if (descriptor !== undefined && + typeof descriptor.value === 'function' && + builtInObjects.has(descriptor.value.name)) { + return; + } + } else { + isProto = false; + } + + if (depth === 0) { + keySet = new Set(); + } else { + keys.forEach((key) => keySet.add(key)); + } + // Get all own property names and symbols. + keys = ObjectGetOwnPropertyNames(obj); + const symbols = ObjectGetOwnPropertySymbols(obj); + if (symbols.length !== 0) { + keys.push(...symbols); + } + for (const key of keys) { + // Ignore the `constructor` property and keys that exist on layers above. + if (key === 'constructor' || + ObjectPrototypeHasOwnProperty(main, key) || + (depth !== 0 && keySet.has(key))) { + continue; + } + const desc = ObjectGetOwnPropertyDescriptor(obj, key); + if (typeof desc.value === 'function') { + continue; + } + const value = formatProperty( + ctx, obj, recurseTimes, key, kObjectType, desc); + if (ctx.colors) { + // Faint! + output.push(`\u001b[2m${value}\u001b[22m`); + } else { + output.push(value); + } + } + // Limit the inspection to up to three prototype layers. Using `recurseTimes` + // is not a good choice here, because it's as if the properties are declared + // on the current object from the users perspective. + } while (++depth !== 3); +} + function getPrefix(constructor, tag, fallback) { if (constructor === null) { if (tag !== '') { @@ -693,8 +762,17 @@ function formatValue(ctx, value, recurseTimes, typedArray) { function formatRaw(ctx, value, recurseTimes, typedArray) { let keys; + let protoProps; + if (ctx.showHidden && (recurseTimes <= ctx.depth || ctx.depth === null)) { + protoProps = []; + } + + const constructor = getConstructorName(value, ctx, recurseTimes, protoProps); + // Reset the variable to check for this later on. + if (protoProps !== undefined && protoProps.length === 0) { + protoProps = undefined; + } - const constructor = getConstructorName(value, ctx, recurseTimes); let tag = value[SymbolToStringTag]; // Only list the tag in case it's non-enumerable / not an own property. // Otherwise we'd print this twice. @@ -724,21 +802,21 @@ function formatRaw(ctx, value, recurseTimes, typedArray) { // Only set the constructor for non ordinary ("Array [...]") arrays. const prefix = getPrefix(constructor, tag, 'Array'); braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']']; - if (value.length === 0 && keys.length === 0) + if (value.length === 0 && keys.length === 0 && protoProps === undefined) return `${braces[0]}]`; extrasType = kArrayExtrasType; formatter = formatArray; } else if (isSet(value)) { keys = getKeys(value, ctx.showHidden); const prefix = getPrefix(constructor, tag, 'Set'); - if (value.size === 0 && keys.length === 0) + if (value.size === 0 && keys.length === 0 && protoProps === undefined) return `${prefix}{}`; braces = [`${prefix}{`, '}']; formatter = formatSet; } else if (isMap(value)) { keys = getKeys(value, ctx.showHidden); const prefix = getPrefix(constructor, tag, 'Map'); - if (value.size === 0 && keys.length === 0) + if (value.size === 0 && keys.length === 0 && protoProps === undefined) return `${prefix}{}`; braces = [`${prefix}{`, '}']; formatter = formatMap; @@ -773,12 +851,12 @@ function formatRaw(ctx, value, recurseTimes, typedArray) { } else if (tag !== '') { braces[0] = `${getPrefix(constructor, tag, 'Object')}{`; } - if (keys.length === 0) { + if (keys.length === 0 && protoProps === undefined) { return `${braces[0]}}`; } } else if (typeof value === 'function') { base = getFunctionBase(value, constructor, tag); - if (keys.length === 0) + if (keys.length === 0 && protoProps === undefined) return ctx.stylize(base, 'special'); } else if (isRegExp(value)) { // Make RegExps say that they are RegExps @@ -788,8 +866,10 @@ function formatRaw(ctx, value, recurseTimes, typedArray) { const prefix = getPrefix(constructor, tag, 'RegExp'); if (prefix !== 'RegExp ') base = `${prefix}${base}`; - if (keys.length === 0 || (recurseTimes > ctx.depth && ctx.depth !== null)) + if ((keys.length === 0 && protoProps === undefined) || + (recurseTimes > ctx.depth && ctx.depth !== null)) { return ctx.stylize(base, 'regexp'); + } } else if (isDate(value)) { // Make dates with properties first say the date base = NumberIsNaN(DatePrototypeGetTime(value)) ? @@ -798,12 +878,12 @@ function formatRaw(ctx, value, recurseTimes, typedArray) { const prefix = getPrefix(constructor, tag, 'Date'); if (prefix !== 'Date ') base = `${prefix}${base}`; - if (keys.length === 0) { + if (keys.length === 0 && protoProps === undefined) { return ctx.stylize(base, 'date'); } } else if (isError(value)) { base = formatError(value, constructor, tag, ctx); - if (keys.length === 0) + if (keys.length === 0 && protoProps === undefined) return base; } else if (isAnyArrayBuffer(value)) { // Fast path for ArrayBuffer and SharedArrayBuffer. @@ -814,7 +894,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) { const prefix = getPrefix(constructor, tag, arrayType); if (typedArray === undefined) { formatter = formatArrayBuffer; - } else if (keys.length === 0) { + } else if (keys.length === 0 && protoProps === undefined) { return prefix + `{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`; } @@ -838,7 +918,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) { formatter = formatNamespaceObject; } else if (isBoxedPrimitive(value)) { base = getBoxedBase(value, ctx, keys, constructor, tag); - if (keys.length === 0) { + if (keys.length === 0 && protoProps === undefined) { return base; } } else { @@ -858,7 +938,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) { formatter = formatIterator; // Handle other regular objects again. } else { - if (keys.length === 0) { + if (keys.length === 0 && protoProps === undefined) { if (isExternal(value)) return ctx.stylize('[External]', 'special'); return `${getCtxStyle(value, constructor, tag)}{}`; @@ -886,6 +966,9 @@ function formatRaw(ctx, value, recurseTimes, typedArray) { output.push( formatProperty(ctx, value, recurseTimes, keys[i], extrasType)); } + if (protoProps !== undefined) { + output.push(...protoProps); + } } catch (err) { const constructorName = getCtxStyle(value, constructor, tag).slice(0, -1); return handleMaxCallStackSize(ctx, err, constructorName, indentationLvl); @@ -1349,6 +1432,7 @@ function formatTypedArray(ctx, value, recurseTimes) { } if (ctx.showHidden) { // .buffer goes last, it's not a primitive like the others. + // All besides `BYTES_PER_ELEMENT` are actually getters. ctx.indentationLvl += 2; for (const key of [ 'BYTES_PER_ELEMENT', @@ -1497,10 +1581,10 @@ function formatPromise(ctx, value, recurseTimes) { return output; } -function formatProperty(ctx, value, recurseTimes, key, type) { +function formatProperty(ctx, value, recurseTimes, key, type, desc) { let name, str; let extra = ' '; - const desc = ObjectGetOwnPropertyDescriptor(value, key) || + desc = desc || ObjectGetOwnPropertyDescriptor(value, key) || { value: value[key], enumerable: true }; if (desc.value !== undefined) { const diff = (type !== kObjectType || ctx.compact !== true) ? 2 : 3; diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index eef34ae314..5c1ee2b8d5 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -391,8 +391,24 @@ assert.strictEqual( { class CustomArray extends Array {} CustomArray.prototype[5] = 'foo'; + CustomArray.prototype[49] = 'bar'; + CustomArray.prototype.foo = true; const arr = new CustomArray(50); - assert.strictEqual(util.inspect(arr), 'CustomArray [ <50 empty items> ]'); + arr[49] = 'I win'; + assert.strictEqual( + util.inspect(arr), + "CustomArray [ <49 empty items>, 'I win' ]" + ); + assert.strictEqual( + util.inspect(arr, { showHidden: true }), + 'CustomArray [\n' + + ' <49 empty items>,\n' + + " 'I win',\n" + + ' [length]: 50,\n' + + " '5': 'foo',\n" + + ' foo: true\n' + + ']' + ); } // Array with extra properties. @@ -2585,3 +2601,65 @@ assert.strictEqual( throw err; } } + +// Inspect prototype properties. +{ + class Foo extends Map { + prop = false; + prop2 = true; + get abc() { + return true; + } + get def() { + return false; + } + set def(v) {} + get xyz() { + return 'Should be ignored'; + } + func(a) {} + [util.inspect.custom]() { + return this; + } + } + + class Bar extends Foo { + abc = true; + prop = true; + get xyz() { + return 'YES!'; + } + [util.inspect.custom]() { + return this; + } + } + + const bar = new Bar(); + + assert.strictEqual( + inspect(bar), + 'Bar [Map] { prop: true, prop2: true, abc: true }' + ); + assert.strictEqual( + inspect(bar, { showHidden: true, getters: true, colors: false }), + 'Bar [Map] {\n' + + ' [size]: 0,\n' + + ' prop: true,\n' + + ' prop2: true,\n' + + ' abc: true,\n' + + " [xyz]: [Getter: 'YES!'],\n" + + ' [def]: [Getter/Setter: false]\n' + + '}' + ); + assert.strictEqual( + inspect(bar, { showHidden: true, getters: false, colors: true }), + 'Bar [Map] {\n' + + ' [size]: \x1B[33m0\x1B[39m,\n' + + ' prop: \x1B[33mtrue\x1B[39m,\n' + + ' prop2: \x1B[33mtrue\x1B[39m,\n' + + ' abc: \x1B[33mtrue\x1B[39m,\n' + + ' \x1B[2m[xyz]: \x1B[36m[Getter]\x1B[39m\x1B[22m,\n' + + ' \x1B[2m[def]: \x1B[36m[Getter/Setter]\x1B[39m\x1B[22m\n' + + '}' + ); +} diff --git a/test/parallel/test-whatwg-encoding-custom-textdecoder.js b/test/parallel/test-whatwg-encoding-custom-textdecoder.js index e3779a945d..c475d94dc7 100644 --- a/test/parallel/test-whatwg-encoding-custom-textdecoder.js +++ b/test/parallel/test-whatwg-encoding-custom-textdecoder.js @@ -113,10 +113,19 @@ if (common.hasIntl) { } else { assert.strictEqual( util.inspect(dec, { showHidden: true }), - "TextDecoder {\n encoding: 'utf-8',\n fatal: false,\n " + - 'ignoreBOM: true,\n [Symbol(flags)]: 4,\n [Symbol(handle)]: ' + - "StringDecoder {\n encoding: 'utf8',\n " + - '[Symbol(kNativeDecoder)]: <Buffer 00 00 00 00 00 00 01>\n }\n}' + 'TextDecoder {\n' + + " encoding: 'utf-8',\n" + + ' fatal: false,\n' + + ' ignoreBOM: true,\n' + + ' [Symbol(flags)]: 4,\n' + + ' [Symbol(handle)]: StringDecoder {\n' + + " encoding: 'utf8',\n" + + ' [Symbol(kNativeDecoder)]: <Buffer 00 00 00 00 00 00 01>,\n' + + ' lastChar: [Getter],\n' + + ' lastNeed: [Getter],\n' + + ' lastTotal: [Getter]\n' + + ' }\n' + + '}' ); } } |