Skip to content

Commit 459d676

Browse files
BridgeARtargos
authored andcommitted
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 ad97314 commit 459d676

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;
@@ -407,7 +445,7 @@ function getConstructorName(obj) {
407445
return '';
408446
}
409447

410-
function getPrefix(constructor, tag) {
448+
function getPrefix(constructor, tag, fallback) {
411449
if (constructor !== '') {
412450
if (tag !== '' && constructor !== tag) {
413451
return `${constructor} [${tag}] `;
@@ -418,9 +456,42 @@ function getPrefix(constructor, tag) {
418456
if (tag !== '')
419457
return `[${tag}] `;
420458

459+
if (fallback !== undefined)
460+
return `${fallback} `;
461+
421462
return '';
422463
}
423464

465+
function addExtraKeys(source, target, keys) {
466+
for (const key of keys) {
467+
target[key] = source[key];
468+
}
469+
return target;
470+
}
471+
472+
function findTypedConstructor(value) {
473+
for (const [check, clazz] of [
474+
[isUint8Array, Uint8Array],
475+
[isUint8ClampedArray, Uint8ClampedArray],
476+
[isUint16Array, Uint16Array],
477+
[isUint32Array, Uint32Array],
478+
[isInt8Array, Int8Array],
479+
[isInt16Array, Int16Array],
480+
[isInt32Array, Int32Array],
481+
[isFloat32Array, Float32Array],
482+
[isFloat64Array, Float64Array],
483+
[isBigInt64Array, BigInt64Array],
484+
[isBigUint64Array, BigUint64Array]
485+
]) {
486+
if (check(value)) {
487+
return new clazz(value);
488+
}
489+
}
490+
return value;
491+
}
492+
493+
const getBoxedValue = formatPrimitive.bind(null, stylizeNoColor);
494+
424495
function formatValue(ctx, value, recurseTimes) {
425496
// Primitive types cannot have properties
426497
if (typeof value !== 'object' && typeof value !== 'function') {
@@ -511,7 +582,7 @@ function formatValue(ctx, value, recurseTimes) {
511582
}
512583

513584
if (symbols.length !== 0)
514-
symbols = symbols.filter((key) => propertyIsEnumerable.call(value, key));
585+
symbols = symbols.filter((key) => propertyIsEnumerable(value, key));
515586
}
516587

517588
const keyLength = keys.length + symbols.length;
@@ -524,8 +595,8 @@ function formatValue(ctx, value, recurseTimes) {
524595
let formatter = formatObject;
525596
let braces;
526597
let noIterator = true;
527-
let raw;
528598
let extra;
599+
let i = 0;
529600

530601
// Iterators and the rest are split to reduce checks
531602
if (value[Symbol.iterator]) {
@@ -559,34 +630,16 @@ function formatValue(ctx, value, recurseTimes) {
559630
braces = [`[${tag}] {`, '}'];
560631
formatter = formatSetIterator;
561632
} else {
562-
// Check for boxed strings with valueOf()
563-
// The .valueOf() call can fail for a multitude of reasons
564-
try {
565-
raw = value.valueOf();
566-
} catch (e) { /* ignore */ }
567-
568-
if (typeof raw === 'string') {
569-
const formatted = formatPrimitive(stylizeNoColor, raw, ctx);
570-
if (keyLength === raw.length)
571-
return ctx.stylize(`[String: ${formatted}]`, 'string');
572-
base = `[String: ${formatted}]`;
573-
// For boxed Strings, we have to remove the 0-n indexed entries,
574-
// since they just noisy up the output and are redundant
575-
// Make boxed primitive Strings look like such
576-
keys = keys.slice(value.length);
577-
braces = ['{', '}'];
578-
} else {
579-
noIterator = true;
580-
}
633+
noIterator = true;
581634
}
582635
}
583636
if (noIterator) {
584637
braces = ['{', '}'];
585638
if (constructor === 'Object') {
586639
if (isArgumentsObject(value)) {
587-
braces[0] = '[Arguments] {';
588640
if (keyLength === 0)
589641
return '[Arguments] {}';
642+
braces[0] = '[Arguments] {';
590643
} else if (tag !== '') {
591644
braces[0] = `${getPrefix(constructor, tag)}{`;
592645
if (keyLength === 0) {
@@ -596,24 +649,24 @@ function formatValue(ctx, value, recurseTimes) {
596649
return '{}';
597650
}
598651
} else if (typeof value === 'function') {
599-
const name =
600-
`${constructor || tag}${value.name ? `: ${value.name}` : ''}`;
652+
const type = constructor || tag || 'Function';
653+
const name = `${type}${value.name ? `: ${value.name}` : ''}`;
601654
if (keyLength === 0)
602655
return ctx.stylize(`[${name}]`, 'special');
603656
base = `[${name}]`;
604657
} else if (isRegExp(value)) {
605658
// Make RegExps say that they are RegExps
606659
if (keyLength === 0 || recurseTimes < 0)
607-
return ctx.stylize(regExpToString.call(value), 'regexp');
608-
base = `${regExpToString.call(value)}`;
660+
return ctx.stylize(regExpToString(value), 'regexp');
661+
base = `${regExpToString(value)}`;
609662
} else if (isDate(value)) {
663+
// Make dates with properties first say the date
610664
if (keyLength === 0) {
611-
if (Number.isNaN(value.getTime()))
612-
return ctx.stylize(value.toString(), 'date');
613-
return ctx.stylize(dateToISOString.call(value), 'date');
665+
if (Number.isNaN(dateGetTime(value)))
666+
return ctx.stylize(String(value), 'date');
667+
return ctx.stylize(dateToISOString(value), 'date');
614668
}
615-
// Make dates with properties first say the date
616-
base = dateToISOString.call(value);
669+
base = dateToISOString(value);
617670
} else if (isError(value)) {
618671
// Make error with message first say the error
619672
base = formatError(value);
@@ -638,28 +691,31 @@ function formatValue(ctx, value, recurseTimes) {
638691
// Fast path for ArrayBuffer and SharedArrayBuffer.
639692
// Can't do the same for DataView because it has a non-primitive
640693
// .buffer property that we need to recurse for.
641-
const prefix = getPrefix(constructor, tag);
694+
let prefix = getPrefix(constructor, tag);
695+
if (prefix === '') {
696+
prefix = isArrayBuffer(value) ? 'ArrayBuffer ' : 'SharedArrayBuffer ';
697+
}
642698
if (keyLength === 0)
643699
return prefix +
644700
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
645701
braces[0] = `${prefix}{`;
646702
keys.unshift('byteLength');
647703
} else if (isDataView(value)) {
648-
braces[0] = `${getPrefix(constructor, tag)}{`;
704+
braces[0] = `${getPrefix(constructor, tag, 'DataView')}{`;
649705
// .buffer goes last, it's not a primitive like the others.
650706
keys.unshift('byteLength', 'byteOffset', 'buffer');
651707
} else if (isPromise(value)) {
652-
braces[0] = `${getPrefix(constructor, tag)}{`;
708+
braces[0] = `${getPrefix(constructor, tag, 'Promise')}{`;
653709
formatter = formatPromise;
654710
} else if (isWeakSet(value)) {
655-
braces[0] = `${getPrefix(constructor, tag)}{`;
711+
braces[0] = `${getPrefix(constructor, tag, 'WeakSet')}{`;
656712
if (ctx.showHidden) {
657713
formatter = formatWeakSet;
658714
} else {
659715
extra = '[items unknown]';
660716
}
661717
} else if (isWeakMap(value)) {
662-
braces[0] = `${getPrefix(constructor, tag)}{`;
718+
braces[0] = `${getPrefix(constructor, tag, 'WeakMap')}{`;
663719
if (ctx.showHidden) {
664720
formatter = formatWeakMap;
665721
} else {
@@ -668,43 +724,67 @@ function formatValue(ctx, value, recurseTimes) {
668724
} else if (types.isModuleNamespaceObject(value)) {
669725
braces[0] = `[${tag}] {`;
670726
formatter = formatNamespaceObject;
727+
} else if (isNumberObject(value)) {
728+
base = `[Number: ${getBoxedValue(numberValueOf(value))}]`;
729+
if (keyLength === 0)
730+
return ctx.stylize(base, 'number');
731+
} else if (isBooleanObject(value)) {
732+
base = `[Boolean: ${getBoxedValue(booleanValueOf(value))}]`;
733+
if (keyLength === 0)
734+
return ctx.stylize(base, 'boolean');
735+
} else if (isBigIntObject(value)) {
736+
base = `[BigInt: ${getBoxedValue(bigIntValueOf(value))}]`;
737+
if (keyLength === 0)
738+
return ctx.stylize(base, 'bigint');
739+
} else if (isSymbolObject(value)) {
740+
base = `[Symbol: ${getBoxedValue(symbolValueOf(value))}]`;
741+
if (keyLength === 0)
742+
return ctx.stylize(base, 'symbol');
743+
} else if (isStringObject(value)) {
744+
const raw = stringValueOf(value);
745+
base = `[String: ${getBoxedValue(raw, ctx)}]`;
746+
if (keyLength === raw.length)
747+
return ctx.stylize(base, 'string');
748+
// For boxed Strings, we have to remove the 0-n indexed entries,
749+
// since they just noisy up the output and are redundant
750+
// Make boxed primitive Strings look like such
751+
keys = keys.slice(value.length);
752+
braces = ['{', '}'];
753+
// The input prototype got manipulated. Special handle these.
754+
// We have to rebuild the information so we are able to display everything.
755+
} else if (isSet(value)) {
756+
const newVal = addExtraKeys(value, new Set(setValues(value)), keys);
757+
return formatValue(ctx, newVal, recurseTimes);
758+
} else if (isMap(value)) {
759+
const newVal = addExtraKeys(value, new Map(mapEntries(value)), keys);
760+
return formatValue(ctx, newVal, recurseTimes);
761+
} else if (Array.isArray(value)) {
762+
// The prefix is not always possible to fully reconstruct.
763+
const prefix = getPrefix(constructor, tag);
764+
braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']'];
765+
formatter = formatArray;
766+
const newValue = [];
767+
newValue.length = value.length;
768+
value = addExtraKeys(value, newValue, keys);
769+
} else if (isTypedArray(value)) {
770+
const newValue = findTypedConstructor(value);
771+
value = addExtraKeys(value, newValue, keys.slice(newValue.length));
772+
// The prefix is not always possible to fully reconstruct.
773+
braces = [`${getPrefix(getConstructorName(value), tag)}[`, ']'];
774+
formatter = formatTypedArray;
775+
} else if (isMapIterator(value)) {
776+
braces = [`[${tag || 'Map Iterator'}] {`, '}'];
777+
formatter = formatMapIterator;
778+
} else if (isSetIterator(value)) {
779+
braces = [`[${tag || 'Set Iterator'}] {`, '}'];
780+
formatter = formatSetIterator;
781+
// Handle other regular objects again.
782+
} else if (keyLength === 0) {
783+
if (isExternal(value))
784+
return ctx.stylize('[External]', 'special');
785+
return `${getPrefix(constructor, tag)}{}`;
671786
} else {
672-
// Check boxed primitives other than string with valueOf()
673-
// NOTE: `Date` has to be checked first!
674-
// The .valueOf() call can fail for a multitude of reasons
675-
try {
676-
raw = value.valueOf();
677-
} catch (e) { /* ignore */ }
678-
679-
if (typeof raw === 'number') {
680-
// Make boxed primitive Numbers look like such
681-
const formatted = formatPrimitive(stylizeNoColor, raw);
682-
if (keyLength === 0)
683-
return ctx.stylize(`[Number: ${formatted}]`, 'number');
684-
base = `[Number: ${formatted}]`;
685-
} else if (typeof raw === 'boolean') {
686-
// Make boxed primitive Booleans look like such
687-
const formatted = formatPrimitive(stylizeNoColor, raw);
688-
if (keyLength === 0)
689-
return ctx.stylize(`[Boolean: ${formatted}]`, 'boolean');
690-
base = `[Boolean: ${formatted}]`;
691-
// eslint-disable-next-line valid-typeof
692-
} else if (typeof raw === 'bigint') {
693-
// Make boxed primitive BigInts look like such
694-
const formatted = formatPrimitive(stylizeNoColor, raw);
695-
if (keyLength === 0)
696-
return ctx.stylize(`[BigInt: ${formatted}]`, 'bigint');
697-
base = `[BigInt: ${formatted}]`;
698-
} else if (typeof raw === 'symbol') {
699-
const formatted = formatPrimitive(stylizeNoColor, raw);
700-
return ctx.stylize(`[Symbol: ${formatted}]`, 'symbol');
701-
} else if (keyLength === 0) {
702-
if (isExternal(value))
703-
return ctx.stylize('[External]', 'special');
704-
return `${getPrefix(constructor, tag)}{}`;
705-
} else {
706-
braces[0] = `${getPrefix(constructor, tag)}{`;
707-
}
787+
braces[0] = `${getPrefix(constructor, tag)}{`;
708788
}
709789
}
710790

@@ -737,7 +817,7 @@ function formatValue(ctx, value, recurseTimes) {
737817
if (extra !== undefined)
738818
output.unshift(extra);
739819

740-
for (var i = 0; i < symbols.length; i++) {
820+
for (i = 0; i < symbols.length; i++) {
741821
output.push(formatProperty(ctx, value, recurseTimes, symbols[i], 0));
742822
}
743823

@@ -803,7 +883,7 @@ function formatPrimitive(fn, value, ctx) {
803883
}
804884

805885
function formatError(value) {
806-
return value.stack || errorToString.call(value);
886+
return value.stack || errorToString(value);
807887
}
808888

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

0 commit comments

Comments
 (0)