Skip to content

Commit 10c850b

Browse files
committed
util: harden util.inspect
This makes sure values without prototype will still be inspected properly and do not cause errors. It restores the original information if possible. Besides that it fixes an issue with boxed symbols: extra keys were not visualized so far. PR-URL: #21869 Reviewed-By: Gus Caplan <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: John-David Dalton <[email protected]>
1 parent 28a3e28 commit 10c850b

File tree

2 files changed

+243
-80
lines changed

2 files changed

+243
-80
lines changed

lib/util.js

+160-80
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const types = internalBinding('types');
4343
Object.assign(types, require('internal/util/types'));
4444
const {
4545
isAnyArrayBuffer,
46+
isArrayBuffer,
4647
isArgumentsObject,
4748
isDataView,
4849
isExternal,
@@ -55,7 +56,23 @@ const {
5556
isWeakSet,
5657
isRegExp,
5758
isDate,
58-
isTypedArray
59+
isTypedArray,
60+
isStringObject,
61+
isNumberObject,
62+
isBooleanObject,
63+
isSymbolObject,
64+
isBigIntObject,
65+
isUint8Array,
66+
isUint8ClampedArray,
67+
isUint16Array,
68+
isUint32Array,
69+
isInt8Array,
70+
isInt16Array,
71+
isInt32Array,
72+
isFloat32Array,
73+
isFloat64Array,
74+
isBigInt64Array,
75+
isBigUint64Array
5976
} = types;
6077

6178
const {
@@ -79,10 +96,31 @@ const inspectDefaultOptions = Object.seal({
7996
compact: true
8097
});
8198

82-
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable;
83-
const regExpToString = RegExp.prototype.toString;
84-
const dateToISOString = Date.prototype.toISOString;
85-
const errorToString = Error.prototype.toString;
99+
const ReflectApply = Reflect.apply;
100+
101+
// This function is borrowed from the function with the same name on V8 Extras'
102+
// `utils` object. V8 implements Reflect.apply very efficiently in conjunction
103+
// with the spread syntax, such that no additional special case is needed for
104+
// function calls w/o arguments.
105+
// Refs: https://github.com/v8/v8/blob/d6ead37d265d7215cf9c5f768f279e21bd170212/src/js/prologue.js#L152-L156
106+
function uncurryThis(func) {
107+
return (thisArg, ...args) => ReflectApply(func, thisArg, args);
108+
}
109+
110+
const propertyIsEnumerable = uncurryThis(Object.prototype.propertyIsEnumerable);
111+
const regExpToString = uncurryThis(RegExp.prototype.toString);
112+
const dateToISOString = uncurryThis(Date.prototype.toISOString);
113+
const errorToString = uncurryThis(Error.prototype.toString);
114+
115+
const bigIntValueOf = uncurryThis(BigInt.prototype.valueOf);
116+
const booleanValueOf = uncurryThis(Boolean.prototype.valueOf);
117+
const numberValueOf = uncurryThis(Number.prototype.valueOf);
118+
const symbolValueOf = uncurryThis(Symbol.prototype.valueOf);
119+
const stringValueOf = uncurryThis(String.prototype.valueOf);
120+
121+
const setValues = uncurryThis(Set.prototype.values);
122+
const mapEntries = uncurryThis(Map.prototype.entries);
123+
const dateGetTime = uncurryThis(Date.prototype.getTime);
86124

87125
let CIRCULAR_ERROR_MESSAGE;
88126
let internalDeepEqual;
@@ -445,7 +483,7 @@ function getConstructorName(obj) {
445483
return '';
446484
}
447485

448-
function getPrefix(constructor, tag) {
486+
function getPrefix(constructor, tag, fallback) {
449487
if (constructor !== '') {
450488
if (tag !== '' && constructor !== tag) {
451489
return `${constructor} [${tag}] `;
@@ -456,9 +494,42 @@ function getPrefix(constructor, tag) {
456494
if (tag !== '')
457495
return `[${tag}] `;
458496

497+
if (fallback !== undefined)
498+
return `${fallback} `;
499+
459500
return '';
460501
}
461502

503+
function addExtraKeys(source, target, keys) {
504+
for (const key of keys) {
505+
target[key] = source[key];
506+
}
507+
return target;
508+
}
509+
510+
function findTypedConstructor(value) {
511+
for (const [check, clazz] of [
512+
[isUint8Array, Uint8Array],
513+
[isUint8ClampedArray, Uint8ClampedArray],
514+
[isUint16Array, Uint16Array],
515+
[isUint32Array, Uint32Array],
516+
[isInt8Array, Int8Array],
517+
[isInt16Array, Int16Array],
518+
[isInt32Array, Int32Array],
519+
[isFloat32Array, Float32Array],
520+
[isFloat64Array, Float64Array],
521+
[isBigInt64Array, BigInt64Array],
522+
[isBigUint64Array, BigUint64Array]
523+
]) {
524+
if (check(value)) {
525+
return new clazz(value);
526+
}
527+
}
528+
return value;
529+
}
530+
531+
const getBoxedValue = formatPrimitive.bind(null, stylizeNoColor);
532+
462533
function formatValue(ctx, value, recurseTimes) {
463534
// Primitive types cannot have properties
464535
if (typeof value !== 'object' && typeof value !== 'function') {
@@ -539,7 +610,7 @@ function formatValue(ctx, value, recurseTimes) {
539610
}
540611

541612
if (symbols.length !== 0)
542-
symbols = symbols.filter((key) => propertyIsEnumerable.call(value, key));
613+
symbols = symbols.filter((key) => propertyIsEnumerable(value, key));
543614
}
544615

545616
const keyLength = keys.length + symbols.length;
@@ -552,8 +623,8 @@ function formatValue(ctx, value, recurseTimes) {
552623
let formatter = formatObject;
553624
let braces;
554625
let noIterator = true;
555-
let raw;
556626
let extra;
627+
let i = 0;
557628

558629
// Iterators and the rest are split to reduce checks
559630
if (value[Symbol.iterator]) {
@@ -587,34 +658,16 @@ function formatValue(ctx, value, recurseTimes) {
587658
braces = [`[${tag}] {`, '}'];
588659
formatter = formatSetIterator;
589660
} else {
590-
// Check for boxed strings with valueOf()
591-
// The .valueOf() call can fail for a multitude of reasons
592-
try {
593-
raw = value.valueOf();
594-
} catch (e) { /* ignore */ }
595-
596-
if (typeof raw === 'string') {
597-
const formatted = formatPrimitive(stylizeNoColor, raw, ctx);
598-
if (keyLength === raw.length)
599-
return ctx.stylize(`[String: ${formatted}]`, 'string');
600-
base = `[String: ${formatted}]`;
601-
// For boxed Strings, we have to remove the 0-n indexed entries,
602-
// since they just noisy up the output and are redundant
603-
// Make boxed primitive Strings look like such
604-
keys = keys.slice(value.length);
605-
braces = ['{', '}'];
606-
} else {
607-
noIterator = true;
608-
}
661+
noIterator = true;
609662
}
610663
}
611664
if (noIterator) {
612665
braces = ['{', '}'];
613666
if (constructor === 'Object') {
614667
if (isArgumentsObject(value)) {
615-
braces[0] = '[Arguments] {';
616668
if (keyLength === 0)
617669
return '[Arguments] {}';
670+
braces[0] = '[Arguments] {';
618671
} else if (tag !== '') {
619672
braces[0] = `${getPrefix(constructor, tag)}{`;
620673
if (keyLength === 0) {
@@ -624,24 +677,24 @@ function formatValue(ctx, value, recurseTimes) {
624677
return '{}';
625678
}
626679
} else if (typeof value === 'function') {
627-
const name =
628-
`${constructor || tag}${value.name ? `: ${value.name}` : ''}`;
680+
const type = constructor || tag || 'Function';
681+
const name = `${type}${value.name ? `: ${value.name}` : ''}`;
629682
if (keyLength === 0)
630683
return ctx.stylize(`[${name}]`, 'special');
631684
base = `[${name}]`;
632685
} else if (isRegExp(value)) {
633686
// Make RegExps say that they are RegExps
634687
if (keyLength === 0 || recurseTimes < 0)
635-
return ctx.stylize(regExpToString.call(value), 'regexp');
636-
base = `${regExpToString.call(value)}`;
688+
return ctx.stylize(regExpToString(value), 'regexp');
689+
base = `${regExpToString(value)}`;
637690
} else if (isDate(value)) {
691+
// Make dates with properties first say the date
638692
if (keyLength === 0) {
639-
if (Number.isNaN(value.getTime()))
640-
return ctx.stylize(value.toString(), 'date');
641-
return ctx.stylize(dateToISOString.call(value), 'date');
693+
if (Number.isNaN(dateGetTime(value)))
694+
return ctx.stylize(String(value), 'date');
695+
return ctx.stylize(dateToISOString(value), 'date');
642696
}
643-
// Make dates with properties first say the date
644-
base = dateToISOString.call(value);
697+
base = dateToISOString(value);
645698
} else if (isError(value)) {
646699
// Make error with message first say the error
647700
base = formatError(value);
@@ -666,28 +719,31 @@ function formatValue(ctx, value, recurseTimes) {
666719
// Fast path for ArrayBuffer and SharedArrayBuffer.
667720
// Can't do the same for DataView because it has a non-primitive
668721
// .buffer property that we need to recurse for.
669-
const prefix = getPrefix(constructor, tag);
722+
let prefix = getPrefix(constructor, tag);
723+
if (prefix === '') {
724+
prefix = isArrayBuffer(value) ? 'ArrayBuffer ' : 'SharedArrayBuffer ';
725+
}
670726
if (keyLength === 0)
671727
return prefix +
672728
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
673729
braces[0] = `${prefix}{`;
674730
keys.unshift('byteLength');
675731
} else if (isDataView(value)) {
676-
braces[0] = `${getPrefix(constructor, tag)}{`;
732+
braces[0] = `${getPrefix(constructor, tag, 'DataView')}{`;
677733
// .buffer goes last, it's not a primitive like the others.
678734
keys.unshift('byteLength', 'byteOffset', 'buffer');
679735
} else if (isPromise(value)) {
680-
braces[0] = `${getPrefix(constructor, tag)}{`;
736+
braces[0] = `${getPrefix(constructor, tag, 'Promise')}{`;
681737
formatter = formatPromise;
682738
} else if (isWeakSet(value)) {
683-
braces[0] = `${getPrefix(constructor, tag)}{`;
739+
braces[0] = `${getPrefix(constructor, tag, 'WeakSet')}{`;
684740
if (ctx.showHidden) {
685741
formatter = formatWeakSet;
686742
} else {
687743
extra = '<items unknown>';
688744
}
689745
} else if (isWeakMap(value)) {
690-
braces[0] = `${getPrefix(constructor, tag)}{`;
746+
braces[0] = `${getPrefix(constructor, tag, 'WeakMap')}{`;
691747
if (ctx.showHidden) {
692748
formatter = formatWeakMap;
693749
} else {
@@ -696,43 +752,67 @@ function formatValue(ctx, value, recurseTimes) {
696752
} else if (types.isModuleNamespaceObject(value)) {
697753
braces[0] = `[${tag}] {`;
698754
formatter = formatNamespaceObject;
755+
} else if (isNumberObject(value)) {
756+
base = `[Number: ${getBoxedValue(numberValueOf(value))}]`;
757+
if (keyLength === 0)
758+
return ctx.stylize(base, 'number');
759+
} else if (isBooleanObject(value)) {
760+
base = `[Boolean: ${getBoxedValue(booleanValueOf(value))}]`;
761+
if (keyLength === 0)
762+
return ctx.stylize(base, 'boolean');
763+
} else if (isBigIntObject(value)) {
764+
base = `[BigInt: ${getBoxedValue(bigIntValueOf(value))}]`;
765+
if (keyLength === 0)
766+
return ctx.stylize(base, 'bigint');
767+
} else if (isSymbolObject(value)) {
768+
base = `[Symbol: ${getBoxedValue(symbolValueOf(value))}]`;
769+
if (keyLength === 0)
770+
return ctx.stylize(base, 'symbol');
771+
} else if (isStringObject(value)) {
772+
const raw = stringValueOf(value);
773+
base = `[String: ${getBoxedValue(raw, ctx)}]`;
774+
if (keyLength === raw.length)
775+
return ctx.stylize(base, 'string');
776+
// For boxed Strings, we have to remove the 0-n indexed entries,
777+
// since they just noisy up the output and are redundant
778+
// Make boxed primitive Strings look like such
779+
keys = keys.slice(value.length);
780+
braces = ['{', '}'];
781+
// The input prototype got manipulated. Special handle these.
782+
// We have to rebuild the information so we are able to display everything.
783+
} else if (isSet(value)) {
784+
const newVal = addExtraKeys(value, new Set(setValues(value)), keys);
785+
return formatValue(ctx, newVal, recurseTimes);
786+
} else if (isMap(value)) {
787+
const newVal = addExtraKeys(value, new Map(mapEntries(value)), keys);
788+
return formatValue(ctx, newVal, recurseTimes);
789+
} else if (Array.isArray(value)) {
790+
// The prefix is not always possible to fully reconstruct.
791+
const prefix = getPrefix(constructor, tag);
792+
braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']'];
793+
formatter = formatArray;
794+
const newValue = [];
795+
newValue.length = value.length;
796+
value = addExtraKeys(value, newValue, keys);
797+
} else if (isTypedArray(value)) {
798+
const newValue = findTypedConstructor(value);
799+
value = addExtraKeys(value, newValue, keys.slice(newValue.length));
800+
// The prefix is not always possible to fully reconstruct.
801+
braces = [`${getPrefix(getConstructorName(value), tag)}[`, ']'];
802+
formatter = formatTypedArray;
803+
} else if (isMapIterator(value)) {
804+
braces = [`[${tag || 'Map Iterator'}] {`, '}'];
805+
formatter = formatMapIterator;
806+
} else if (isSetIterator(value)) {
807+
braces = [`[${tag || 'Set Iterator'}] {`, '}'];
808+
formatter = formatSetIterator;
809+
// Handle other regular objects again.
810+
} else if (keyLength === 0) {
811+
if (isExternal(value))
812+
return ctx.stylize('[External]', 'special');
813+
return `${getPrefix(constructor, tag)}{}`;
699814
} else {
700-
// Check boxed primitives other than string with valueOf()
701-
// NOTE: `Date` has to be checked first!
702-
// The .valueOf() call can fail for a multitude of reasons
703-
try {
704-
raw = value.valueOf();
705-
} catch (e) { /* ignore */ }
706-
707-
if (typeof raw === 'number') {
708-
// Make boxed primitive Numbers look like such
709-
const formatted = formatPrimitive(stylizeNoColor, raw);
710-
if (keyLength === 0)
711-
return ctx.stylize(`[Number: ${formatted}]`, 'number');
712-
base = `[Number: ${formatted}]`;
713-
} else if (typeof raw === 'boolean') {
714-
// Make boxed primitive Booleans look like such
715-
const formatted = formatPrimitive(stylizeNoColor, raw);
716-
if (keyLength === 0)
717-
return ctx.stylize(`[Boolean: ${formatted}]`, 'boolean');
718-
base = `[Boolean: ${formatted}]`;
719-
// eslint-disable-next-line valid-typeof
720-
} else if (typeof raw === 'bigint') {
721-
// Make boxed primitive BigInts look like such
722-
const formatted = formatPrimitive(stylizeNoColor, raw);
723-
if (keyLength === 0)
724-
return ctx.stylize(`[BigInt: ${formatted}]`, 'bigint');
725-
base = `[BigInt: ${formatted}]`;
726-
} else if (typeof raw === 'symbol') {
727-
const formatted = formatPrimitive(stylizeNoColor, raw);
728-
return ctx.stylize(`[Symbol: ${formatted}]`, 'symbol');
729-
} else if (keyLength === 0) {
730-
if (isExternal(value))
731-
return ctx.stylize('[External]', 'special');
732-
return `${getPrefix(constructor, tag)}{}`;
733-
} else {
734-
braces[0] = `${getPrefix(constructor, tag)}{`;
735-
}
815+
braces[0] = `${getPrefix(constructor, tag)}{`;
736816
}
737817
}
738818

@@ -765,7 +845,7 @@ function formatValue(ctx, value, recurseTimes) {
765845
if (extra !== undefined)
766846
output.unshift(extra);
767847

768-
for (var i = 0; i < symbols.length; i++) {
848+
for (i = 0; i < symbols.length; i++) {
769849
output.push(formatProperty(ctx, value, recurseTimes, symbols[i], 0));
770850
}
771851

@@ -835,7 +915,7 @@ function formatPrimitive(fn, value, ctx) {
835915
}
836916

837917
function formatError(value) {
838-
return value.stack || errorToString.call(value);
918+
return value.stack || errorToString(value);
839919
}
840920

841921
function formatObject(ctx, value, recurseTimes, keys) {

0 commit comments

Comments
 (0)