summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuben Bridgewater <ruben@bridgewater.de>2019-11-30 11:07:12 +0100
committerBeth Griggs <Bethany.Griggs@uk.ibm.com>2020-02-06 02:49:39 +0000
commit7686865174ab637ecb8fbe96e2fccd0b395ef4b6 (patch)
tree9459b0894d897e6fb554b46017500cecf8e198f9
parent0376e1cf4d08588b941c8e78072f4d8d687db207 (diff)
downloadnode-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.md7
-rw-r--r--lib/internal/util/inspect.js116
-rw-r--r--test/parallel/test-util-inspect.js80
-rw-r--r--test/parallel/test-whatwg-encoding-custom-textdecoder.js17
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' +
+ '}'
);
}
}