Skip to content

util: inspect prototype properties if showHidden is truthy #30768

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: v13.0.0
pr-url: https://github.com/nodejs/node/pull/27685
description: Circular references now include a marker to the reference.
Expand Down Expand Up @@ -461,7 +465,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`.
Expand Down
118 changes: 101 additions & 17 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const { NativeModule } = require('internal/bootstrap/loaders');
let hexSlice;

const builtInObjects = new Set(
ObjectGetOwnPropertyNames(global).filter((e) => /^([A-Z][a-z]+)+$/.test(e))
ObjectGetOwnPropertyNames(global).filter((e) => /^[A-Z][a-zA-Z0-9]+$/.test(e))
);

const inspectDefaultOptions = ObjectSeal({
Expand Down Expand Up @@ -380,14 +380,20 @@ function getEmptyFormatArray() {
return [];
}

function getConstructorName(obj, ctx, recurseTimes) {
function getConstructorName(obj, ctx, recurseTimes, protoProps) {
let firstProto;
const tmp = obj;
while (obj) {
const descriptor = ObjectGetOwnPropertyDescriptor(obj, 'constructor');
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;
}

Expand All @@ -407,7 +413,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, {
Expand All @@ -420,6 +427,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 !== '') {
Expand Down Expand Up @@ -623,8 +692,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.
Expand Down Expand Up @@ -654,21 +732,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;
Expand Down Expand Up @@ -703,12 +781,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
Expand All @@ -718,8 +796,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)) ?
Expand All @@ -728,12 +808,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.
Expand All @@ -744,7 +824,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)} }`;
}
Expand All @@ -768,7 +848,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 {
Expand All @@ -788,7 +868,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)}{}`;
Expand Down Expand Up @@ -816,6 +896,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);
Expand Down Expand Up @@ -1282,6 +1365,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',
Expand Down Expand Up @@ -1430,10 +1514,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;
Expand Down
80 changes: 79 additions & 1 deletion test/parallel/test-util-inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,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.
Expand Down Expand Up @@ -2556,3 +2572,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' +
'}'
);
}
17 changes: 13 additions & 4 deletions test/parallel/test-whatwg-encoding-custom-textdecoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' +
'}'
);
}
}
Expand Down