diff options
author | Leo Kettmeir <crowlkats@toaxl.com> | 2023-04-30 09:24:13 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-30 07:24:13 +0000 |
commit | 9c8ebce3dcc784f1a6ecd29d5fe0b3d35256ab82 (patch) | |
tree | e423a712e05448d5c895777a810abdf7c9be59ae /ext/console/02_console.js | |
parent | 64e072e499d36ca824db297a493667415ed67cdf (diff) |
refactor: merge Deno & Node inspectors (#18691)
Diffstat (limited to 'ext/console/02_console.js')
-rw-r--r-- | ext/console/02_console.js | 3309 |
1 files changed, 2243 insertions, 1066 deletions
diff --git a/ext/console/02_console.js b/ext/console/02_console.js index 5873a2ec2..51e827876 100644 --- a/ext/console/02_console.js +++ b/ext/console/02_console.js @@ -6,1009 +6,1508 @@ const core = globalThis.Deno.core; const internals = globalThis.__bootstrap.internals; const primordials = globalThis.__bootstrap.primordials; const { - AggregateErrorPrototype, - ArrayPrototypeUnshift, - isNaN, - DatePrototype, DateNow, - DatePrototypeGetTime, - DatePrototypeToISOString, Boolean, - BooleanPrototype, - BooleanPrototypeToString, ObjectKeys, ObjectAssign, ObjectCreate, ObjectFreeze, - ObjectIs, ObjectValues, ObjectFromEntries, - ObjectGetPrototypeOf, - ObjectGetOwnPropertyDescriptor, - ObjectGetOwnPropertySymbols, ObjectPrototypeHasOwnProperty, ObjectPrototypeIsPrototypeOf, - ObjectPrototypePropertyIsEnumerable, - PromisePrototype, + ObjectDefineProperty, String, - StringPrototype, + SafeStringIterator, + DatePrototype, + MapPrototypeEntries, + SetPrototypeGetSize, StringPrototypeRepeat, + StringPrototypeEndsWith, + StringPrototypeIndexOf, + RegExpPrototypeExec, + RegExpPrototypeSymbolReplace, StringPrototypeReplace, StringPrototypeReplaceAll, + ObjectPrototype, + FunctionPrototypeCall, StringPrototypeSplit, StringPrototypeSlice, - StringPrototypeCodePointAt, StringPrototypeCharCodeAt, - StringPrototypeNormalize, + MathFloor, + StringPrototypePadEnd, + ObjectGetOwnPropertySymbols, + ObjectGetOwnPropertyNames, + SymbolPrototypeGetDescription, + SymbolPrototypeToString, + ArrayPrototypePushApply, + ObjectPrototypePropertyIsEnumerable, StringPrototypeMatch, StringPrototypePadStart, - StringPrototypeLocaleCompare, - StringPrototypeToString, StringPrototypeTrim, StringPrototypeIncludes, - StringPrototypeStartsWith, - TypeError, NumberIsInteger, NumberParseInt, - RegExpPrototype, - RegExpPrototypeTest, - RegExpPrototypeToString, SafeArrayIterator, SafeMap, - SafeStringIterator, - SafeSetIterator, + ArrayPrototypeShift, + AggregateErrorPrototype, + RegExpPrototypeTest, + ObjectPrototypeToString, + ArrayPrototypeSort, + ArrayPrototypeUnshift, + DatePrototypeGetTime, + DatePrototypeToISOString, SafeRegExp, SetPrototype, - SetPrototypeEntries, - SetPrototypeGetSize, Symbol, - SymbolPrototype, - SymbolPrototypeToString, - SymbolPrototypeValueOf, - SymbolPrototypeGetDescription, SymbolToStringTag, SymbolHasInstance, SymbolFor, + ObjectGetOwnPropertyDescriptor, + ObjectIs, + Uint8Array, + isNaN, + TypedArrayPrototypeGetSymbolToStringTag, + TypedArrayPrototypeGetLength, + ReflectOwnKeys, Array, + RegExpPrototypeToString, ArrayIsArray, + SymbolIterator, + ArrayBufferIsView, ArrayPrototypeJoin, ArrayPrototypeMap, ArrayPrototypeReduce, - ArrayPrototypeEntries, + ObjectSetPrototypeOf, ArrayPrototypePush, - ArrayPrototypePop, - ArrayPrototypeSort, - ArrayPrototypeSlice, - ArrayPrototypeShift, ArrayPrototypeIncludes, ArrayPrototypeFill, ArrayPrototypeFilter, ArrayPrototypeFind, FunctionPrototypeBind, - FunctionPrototypeToString, MapPrototype, MapPrototypeHas, MapPrototypeGet, MapPrototypeSet, MapPrototypeDelete, - MapPrototypeEntries, MapPrototypeForEach, MapPrototypeGetSize, Error, ErrorPrototype, ErrorCaptureStackTrace, + MathSqrt, MathAbs, MathMax, MathMin, - MathSqrt, MathRound, - MathFloor, Number, - NumberPrototype, NumberPrototypeToString, - NumberPrototypeValueOf, - BigIntPrototype, - BigIntPrototypeToString, Proxy, ReflectGet, ReflectGetOwnPropertyDescriptor, ReflectGetPrototypeOf, ReflectHas, - TypedArrayPrototypeGetLength, - TypedArrayPrototypeGetSymbolToStringTag, - WeakMapPrototype, - WeakSetPrototype, + BigIntPrototypeValueOf, + ObjectGetPrototypeOf, + FunctionPrototypeToString, + StringPrototypeStartsWith, + SetPrototypeValues, + SafeSet, + SafeSetIterator, + TypedArrayPrototypeGetByteLength, + SafeMapIterator, + ArrayBufferPrototype, } = primordials; -import * as colors from "ext:deno_console/01_colors.js"; +import * as colors_ from "ext:deno_console/01_colors.js"; + +// Don't use 'blue' not visible on cmd.exe +const styles = { + special: "cyan", + number: "yellow", + bigint: "yellow", + boolean: "yellow", + undefined: "grey", + null: "bold", + string: "green", + symbol: "green", + date: "magenta", + // "name": intentionally not styling + // TODO(BridgeAR): Highlight regular expressions properly. + regexp: "red", + module: "underline", +}; + +const defaultFG = 39; +const defaultBG = 49; + +// Set Graphics Rendition https://en.wikipedia.org/wiki/ANSI_escape_code#graphics +// Each color consists of an array with the color code as first entry and the +// reset code as second entry. +const colors = { + reset: [0, 0], + bold: [1, 22], + dim: [2, 22], // Alias: faint + italic: [3, 23], + underline: [4, 24], + blink: [5, 25], + // Swap foreground and background colors + inverse: [7, 27], // Alias: swapcolors, swapColors + hidden: [8, 28], // Alias: conceal + strikethrough: [9, 29], // Alias: strikeThrough, crossedout, crossedOut + doubleunderline: [21, 24], // Alias: doubleUnderline + black: [30, defaultFG], + red: [31, defaultFG], + green: [32, defaultFG], + yellow: [33, defaultFG], + blue: [34, defaultFG], + magenta: [35, defaultFG], + cyan: [36, defaultFG], + white: [37, defaultFG], + bgBlack: [40, defaultBG], + bgRed: [41, defaultBG], + bgGreen: [42, defaultBG], + bgYellow: [43, defaultBG], + bgBlue: [44, defaultBG], + bgMagenta: [45, defaultBG], + bgCyan: [46, defaultBG], + bgWhite: [47, defaultBG], + framed: [51, 54], + overlined: [53, 55], + gray: [90, defaultFG], // Alias: grey, blackBright + redBright: [91, defaultFG], + greenBright: [92, defaultFG], + yellowBright: [93, defaultFG], + blueBright: [94, defaultFG], + magentaBright: [95, defaultFG], + cyanBright: [96, defaultFG], + whiteBright: [97, defaultFG], + bgGray: [100, defaultBG], // Alias: bgGrey, bgBlackBright + bgRedBright: [101, defaultBG], + bgGreenBright: [102, defaultBG], + bgYellowBright: [103, defaultBG], + bgBlueBright: [104, defaultBG], + bgMagentaBright: [105, defaultBG], + bgCyanBright: [106, defaultBG], + bgWhiteBright: [107, defaultBG], +}; -function isInvalidDate(x) { - return isNaN(DatePrototypeGetTime(x)); +function defineColorAlias(target, alias) { + ObjectDefineProperty(colors, alias, { + get() { + return this[target]; + }, + set(value) { + this[target] = value; + }, + configurable: true, + enumerable: false, + }); } -function hasOwnProperty(obj, v) { - if (obj == null) { - return false; - } - return ObjectPrototypeHasOwnProperty(obj, v); +defineColorAlias("gray", "grey"); +defineColorAlias("gray", "blackBright"); +defineColorAlias("bgGray", "bgGrey"); +defineColorAlias("bgGray", "bgBlackBright"); +defineColorAlias("dim", "faint"); +defineColorAlias("strikethrough", "crossedout"); +defineColorAlias("strikethrough", "strikeThrough"); +defineColorAlias("strikethrough", "crossedOut"); +defineColorAlias("hidden", "conceal"); +defineColorAlias("inverse", "swapColors"); +defineColorAlias("inverse", "swapcolors"); +defineColorAlias("doubleunderline", "doubleUnderline"); + +// https://tc39.es/ecma262/#sec-boolean.prototype.valueof +const _booleanValueOf = Boolean.prototype.valueOf; + +// https://tc39.es/ecma262/#sec-number.prototype.valueof +const _numberValueOf = Number.prototype.valueOf; + +// https://tc39.es/ecma262/#sec-string.prototype.valueof +const _stringValueOf = String.prototype.valueOf; + +// https://tc39.es/ecma262/#sec-symbol.prototype.valueof +const _symbolValueOf = Symbol.prototype.valueOf; + +// https://tc39.es/ecma262/#sec-weakmap.prototype.has +const _weakMapHas = WeakMap.prototype.has; + +// https://tc39.es/ecma262/#sec-weakset.prototype.has +const _weakSetHas = WeakSet.prototype.has; + +// https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.bytelength +const _getArrayBufferByteLength = ObjectGetOwnPropertyDescriptor( + ArrayBufferPrototype, + "byteLength", +).get; + +// https://tc39.es/ecma262/#sec-get-sharedarraybuffer.prototype.bytelength +let _getSharedArrayBufferByteLength; + +// https://tc39.es/ecma262/#sec-get-set.prototype.size +const _getSetSize = ObjectGetOwnPropertyDescriptor( + SetPrototype, + "size", +).get; + +// https://tc39.es/ecma262/#sec-get-map.prototype.size +const _getMapSize = ObjectGetOwnPropertyDescriptor( + MapPrototype, + "size", +).get; + +function isObjectLike(value) { + return value !== null && typeof value === "object"; } -function propertyIsEnumerable(obj, prop) { - if ( - obj == null || - typeof obj.propertyIsEnumerable !== "function" - ) { +export function isAnyArrayBuffer(value) { + return isArrayBuffer(value) || isSharedArrayBuffer(value); +} + +export function isArgumentsObject(value) { + return ( + isObjectLike(value) && + value[SymbolToStringTag] === undefined && + ObjectPrototypeToString(value) === "[object Arguments]" + ); +} + +export function isArrayBuffer(value) { + try { + _getArrayBufferByteLength.call(value); + return true; + } catch { return false; } - - return ObjectPrototypePropertyIsEnumerable(obj, prop); } -// Copyright Joyent, Inc. and other Node contributors. MIT license. -// Forked from Node's lib/internal/cli_table.js +export function isAsyncFunction(value) { + return ( + typeof value === "function" && + (value[SymbolToStringTag] === "AsyncFunction") + ); +} -function isTypedArray(x) { - return TypedArrayPrototypeGetSymbolToStringTag(x) !== undefined; +export function isAsyncGeneratorFunction(value) { + return ( + typeof value === "function" && + (value[SymbolToStringTag] === "AsyncGeneratorFunction") + ); } -const tableChars = { - middleMiddle: "\u2500", - rowMiddle: "\u253c", - topRight: "\u2510", - topLeft: "\u250c", - leftMiddle: "\u251c", - topMiddle: "\u252c", - bottomRight: "\u2518", - bottomLeft: "\u2514", - bottomMiddle: "\u2534", - rightMiddle: "\u2524", - left: "\u2502 ", - right: " \u2502", - middle: " \u2502 ", -}; +export function isBooleanObject(value) { + if (!isObjectLike(value)) { + return false; + } -function isFullWidthCodePoint(code) { - // Code points are partially derived from: - // http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt + try { + _booleanValueOf.call(value); + return true; + } catch { + return false; + } +} + +export function isBoxedPrimitive( + value, +) { return ( - code >= 0x1100 && - (code <= 0x115f || // Hangul Jamo - code === 0x2329 || // LEFT-POINTING ANGLE BRACKET - code === 0x232a || // RIGHT-POINTING ANGLE BRACKET - // CJK Radicals Supplement .. Enclosed CJK Letters and Months - (code >= 0x2e80 && code <= 0x3247 && code !== 0x303f) || - // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A - (code >= 0x3250 && code <= 0x4dbf) || - // CJK Unified Ideographs .. Yi Radicals - (code >= 0x4e00 && code <= 0xa4c6) || - // Hangul Jamo Extended-A - (code >= 0xa960 && code <= 0xa97c) || - // Hangul Syllables - (code >= 0xac00 && code <= 0xd7a3) || - // CJK Compatibility Ideographs - (code >= 0xf900 && code <= 0xfaff) || - // Vertical Forms - (code >= 0xfe10 && code <= 0xfe19) || - // CJK Compatibility Forms .. Small Form Variants - (code >= 0xfe30 && code <= 0xfe6b) || - // Halfwidth and Fullwidth Forms - (code >= 0xff01 && code <= 0xff60) || - (code >= 0xffe0 && code <= 0xffe6) || - // Kana Supplement - (code >= 0x1b000 && code <= 0x1b001) || - // Enclosed Ideographic Supplement - (code >= 0x1f200 && code <= 0x1f251) || - // Miscellaneous Symbols and Pictographs 0x1f300 - 0x1f5ff - // Emoticons 0x1f600 - 0x1f64f - (code >= 0x1f300 && code <= 0x1f64f) || - // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane - (code >= 0x20000 && code <= 0x3fffd)) + isBooleanObject(value) || + isStringObject(value) || + isNumberObject(value) || + isSymbolObject(value) || + isBigIntObject(value) ); } -function getStringWidth(str) { - str = StringPrototypeNormalize(colors.stripColor(str), "NFC"); - let width = 0; +export function isDataView(value) { + return ( + ArrayBufferIsView(value) && + TypedArrayPrototypeGetSymbolToStringTag(value) === undefined + ); +} - for (const ch of new SafeStringIterator(str)) { - width += isFullWidthCodePoint(StringPrototypeCodePointAt(ch, 0)) ? 2 : 1; - } +export function isTypedArray(value) { + return TypedArrayPrototypeGetSymbolToStringTag(value) !== undefined; +} - return width; +export function isGeneratorFunction( + value, +) { + return ( + typeof value === "function" && + value[SymbolToStringTag] === "GeneratorFunction" + ); } -function renderRow(row, columnWidths, columnRightAlign) { - let out = tableChars.left; - for (let i = 0; i < row.length; i++) { - const cell = row[i]; - const len = getStringWidth(cell); - const padding = StringPrototypeRepeat(" ", columnWidths[i] - len); - if (columnRightAlign?.[i]) { - out += `${padding}${cell}`; - } else { - out += `${cell}${padding}`; - } - if (i !== row.length - 1) { - out += tableChars.middle; - } +export function isMap(value) { + try { + _getMapSize.call(value); + return true; + } catch { + return false; } - out += tableChars.right; - return out; } -function cliTable(head, columns) { - const rows = []; - const columnWidths = ArrayPrototypeMap(head, (h) => getStringWidth(h)); - const longestColumn = ArrayPrototypeReduce( - columns, - (n, a) => MathMax(n, a.length), - 0, +export function isMapIterator( + value, +) { + return ( + isObjectLike(value) && + value[SymbolToStringTag] === "Map Iterator" ); - const columnRightAlign = new Array(columnWidths.length).fill(true); +} - for (let i = 0; i < head.length; i++) { - const column = columns[i]; - for (let j = 0; j < longestColumn; j++) { - if (rows[j] === undefined) { - rows[j] = []; - } - const value = (rows[j][i] = hasOwnProperty(column, j) ? column[j] : ""); - const width = columnWidths[i] || 0; - const counted = getStringWidth(value); - columnWidths[i] = MathMax(width, counted); - columnRightAlign[i] &= NumberIsInteger(+value); - } - } +export function isModuleNamespaceObject( + value, +) { + return ( + isObjectLike(value) && + value[SymbolToStringTag] === "Module" + ); +} - const divider = ArrayPrototypeMap( - columnWidths, - (i) => StringPrototypeRepeat(tableChars.middleMiddle, i + 2), +export function isNativeError(value) { + return ( + isObjectLike(value) && + value[SymbolToStringTag] === undefined && + ObjectPrototypeToString(value) === "[object Error]" ); +} - let result = - `${tableChars.topLeft}${ - ArrayPrototypeJoin(divider, tableChars.topMiddle) - }` + - `${tableChars.topRight}\n${renderRow(head, columnWidths)}\n` + - `${tableChars.leftMiddle}${ - ArrayPrototypeJoin(divider, tableChars.rowMiddle) - }` + - `${tableChars.rightMiddle}\n`; +export function isNumberObject(value) { + if (!isObjectLike(value)) { + return false; + } - for (let i = 0; i < rows.length; ++i) { - const row = rows[i]; - result += `${renderRow(row, columnWidths, columnRightAlign)}\n`; + try { + _numberValueOf.call(value); + return true; + } catch { + return false; } +} - result += - `${tableChars.bottomLeft}${ - ArrayPrototypeJoin(divider, tableChars.bottomMiddle) - }` + - tableChars.bottomRight; +export function isBigIntObject(value) { + if (!isObjectLike(value)) { + return false; + } - return result; + try { + BigIntPrototypeValueOf(value); + return true; + } catch { + return false; + } } -/* End of forked part */ -// We can match Node's quoting behavior exactly by swapping the double quote and -// single quote in this array. That would give preference to single quotes. -// However, we prefer double quotes as the default. -const QUOTES = ['"', "'", "`"]; - -const DEFAULT_INSPECT_OPTIONS = { - depth: 4, - indentLevel: 0, - sorted: false, - trailingComma: false, - compact: true, - iterableLimit: 100, - showProxy: false, - colors: false, - getters: false, - showHidden: false, - strAbbreviateSize: 100, - /** You can override the quotes preference in inspectString. - * Used by util.inspect() */ - // TODO(kt3k): Consider using symbol as a key to hide this from the public - // API. - quotes: QUOTES, -}; +export function isPromise(value) { + return ( + isObjectLike(value) && + value[SymbolToStringTag] === "Promise" + ); +} +export function isRegExp(value) { + return ( + isObjectLike(value) && + value[SymbolToStringTag] === undefined && + ObjectPrototypeToString(value) === "[object RegExp]" + ); +} -const DEFAULT_INDENT = " "; // Default indent string +export function isSet(value) { + try { + _getSetSize.call(value); + return true; + } catch { + return false; + } +} -const LINE_BREAKING_LENGTH = 80; -const MIN_GROUP_LENGTH = 6; -const STR_ABBREVIATE_SIZE = 100; +export function isSetIterator( + value, +) { + return ( + isObjectLike(value) && + value[SymbolToStringTag] === "Set Iterator" + ); +} -const PROMISE_STRING_BASE_LENGTH = 12; +export function isSharedArrayBuffer( + value, +) { + // TODO(kt3k): add SharedArrayBuffer to primordials + _getSharedArrayBufferByteLength ??= ObjectGetOwnPropertyDescriptor( + // deno-lint-ignore prefer-primordials + SharedArrayBuffer.prototype, + "byteLength", + ).get; -class CSI { - static kClear = "\x1b[1;1H"; - static kClearScreenDown = "\x1b[0J"; + try { + _getSharedArrayBufferByteLength.call(value); + return true; + } catch { + return false; + } } -function getClassInstanceName(instance) { - if (typeof instance != "object") { - return ""; +export function isStringObject(value) { + if (!isObjectLike(value)) { + return false; } - const constructor = instance?.constructor; - if (typeof constructor == "function") { - return constructor.name ?? ""; + + try { + _stringValueOf.call(value); + return true; + } catch { + return false; } - return ""; } -function maybeColor(fn, inspectOptions) { - return inspectOptions.colors ? fn : (s) => s; -} +export function isSymbolObject(value) { + if (!isObjectLike(value)) { + return false; + } -function inspectFunction(value, inspectOptions) { - const cyan = maybeColor(colors.cyan, inspectOptions); - if ( - ReflectHas(value, customInspect) && - typeof value[customInspect] === "function" - ) { - return String(value[customInspect](inspect, inspectOptions)); - } - // Might be Function/AsyncFunction/GeneratorFunction/AsyncGeneratorFunction - let cstrName = ObjectGetPrototypeOf(value)?.constructor?.name; - if (!cstrName) { - // If prototype is removed or broken, - // use generic 'Function' instead. - cstrName = "Function"; - } - const stringValue = FunctionPrototypeToString(value); - // Might be Class - if (StringPrototypeStartsWith(stringValue, "class")) { - cstrName = "Class"; - } - - // Our function may have properties, so we want to format those - // as if our function was an object - // If we didn't find any properties, we will just append an - // empty suffix. - let suffix = ``; - let refStr = ""; - if ( - ObjectKeys(value).length > 0 || - ObjectGetOwnPropertySymbols(value).length > 0 - ) { - const { 0: propString, 1: refIndex } = inspectRawObject( - value, - inspectOptions, - ); - refStr = refIndex; - // Filter out the empty string for the case we only have - // non-enumerable symbols. - if ( - propString.length > 0 && - propString !== "{}" - ) { - suffix = ` ${propString}`; - } + try { + _symbolValueOf.call(value); + return true; + } catch { + return false; } +} - if (value.name && value.name !== "anonymous") { - // from MDN spec - return cyan(`${refStr}[${cstrName}: ${value.name}]`) + suffix; +export function isWeakMap( + value, +) { + try { + _weakMapHas.call(value, null); + return true; + } catch { + return false; } - return cyan(`${refStr}[${cstrName} (anonymous)]`) + suffix; } -function inspectIterable( +export function isWeakSet( value, - options, - inspectOptions, ) { - const cyan = maybeColor(colors.cyan, inspectOptions); - if (inspectOptions.indentLevel >= inspectOptions.depth) { - return cyan(`[${options.typeName}]`); + try { + _weakSetHas.call(value, null); + return true; + } catch { + return false; } +} - const entries = []; - let iter; - let valueIsTypedArray = false; - let entriesLength; +const kObjectType = 0; +const kArrayType = 1; +const kArrayExtrasType = 2; + +const kMinLineLength = 16; + +// Constants to map the iterator state. +const kWeak = 0; +const kIterator = 1; +const kMapEntries = 2; + +// Escaped control characters (plus the single quote and the backslash). Use +// empty strings to fill up unused entries. +// deno-fmt-ignore +const meta = [ + '\\x00', '\\x01', '\\x02', '\\x03', '\\x04', '\\x05', '\\x06', '\\x07', // x07 + '\\b', '\\t', '\\n', '\\x0B', '\\f', '\\r', '\\x0E', '\\x0F', // x0F + '\\x10', '\\x11', '\\x12', '\\x13', '\\x14', '\\x15', '\\x16', '\\x17', // x17 + '\\x18', '\\x19', '\\x1A', '\\x1B', '\\x1C', '\\x1D', '\\x1E', '\\x1F', // x1F + '', '', '', '', '', '', '', "\\'", '', '', '', '', '', '', '', '', // x2F + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', // x3F + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', // x4F + '', '', '', '', '', '', '', '', '', '', '', '', '\\\\', '', '', '', // x5F + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', // x6F + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '\\x7F', // x7F + '\\x80', '\\x81', '\\x82', '\\x83', '\\x84', '\\x85', '\\x86', '\\x87', // x87 + '\\x88', '\\x89', '\\x8A', '\\x8B', '\\x8C', '\\x8D', '\\x8E', '\\x8F', // x8F + '\\x90', '\\x91', '\\x92', '\\x93', '\\x94', '\\x95', '\\x96', '\\x97', // x97 + '\\x98', '\\x99', '\\x9A', '\\x9B', '\\x9C', '\\x9D', '\\x9E', '\\x9F', // x9F +]; + +// https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot +const isUndetectableObject = (v) => typeof v === "undefined" && v !== undefined; + +const strEscapeSequencesReplacer = new SafeRegExp( + "[\x00-\x1f\x27\x5c\x7f-\x9f]", + "g", +); - switch (options.typeName) { - case "Map": - iter = MapPrototypeEntries(value); - entriesLength = MapPrototypeGetSize(value); - break; - case "Set": - iter = SetPrototypeEntries(value); - entriesLength = SetPrototypeGetSize(value); - break; - case "Array": - entriesLength = value.length; - break; - default: - if (isTypedArray(value)) { - entriesLength = TypedArrayPrototypeGetLength(value); - iter = ArrayPrototypeEntries(value); - valueIsTypedArray = true; - } else { - throw new TypeError("unreachable"); - } - } +const keyStrRegExp = new SafeRegExp("^[a-zA-Z_][a-zA-Z_0-9]*$"); +const numberRegExp = new SafeRegExp("^(0|[1-9][0-9]*)$"); - let entriesLengthWithoutEmptyItems = entriesLength; - if (options.typeName === "Array") { - for ( - let i = 0, j = 0; - i < entriesLength && j < inspectOptions.iterableLimit; - i++, j++ - ) { - inspectOptions.indentLevel++; - const { entry, skipTo } = options.entryHandler( - [i, value[i]], - inspectOptions, - ); - ArrayPrototypePush(entries, entry); - inspectOptions.indentLevel--; +// TODO(wafuwafu13): Figure out +const escapeFn = (str) => meta[str.charCodeAt(0)]; + +function stylizeNoColor(str) { + return str; +} + +// node custom inspect symbol +const nodeCustomInspectSymbol = SymbolFor("nodejs.util.inspect.custom"); + +// This non-unique symbol is used to support op_crates, ie. +// in extensions/web we don't want to depend on public +// Symbol.for("Deno.customInspect") symbol defined in the public API. +// Internal only, shouldn't be used by users. +const privateCustomInspect = SymbolFor("Deno.privateCustomInspect"); + +function getUserOptions(ctx, isCrossContext) { + const ret = { + stylize: ctx.stylize, + showHidden: ctx.showHidden, + depth: ctx.depth, + colors: ctx.colors, + customInspect: ctx.customInspect, + showProxy: ctx.showProxy, + maxArrayLength: ctx.maxArrayLength, + maxStringLength: ctx.maxStringLength, + breakLength: ctx.breakLength, + compact: ctx.compact, + sorted: ctx.sorted, + getters: ctx.getters, + numericSeparator: ctx.numericSeparator, + ...ctx.userOptions, + }; - if (skipTo) { - // subtract skipped (empty) items - entriesLengthWithoutEmptyItems -= skipTo - i; - i = skipTo; + // Typically, the target value will be an instance of `Object`. If that is + // *not* the case, the object may come from another vm.Context, and we want + // to avoid passing it objects from this Context in that case, so we remove + // the prototype from the returned object itself + the `stylize()` function, + // and remove all other non-primitives, including non-primitive user options. + if (isCrossContext) { + ObjectSetPrototypeOf(ret, null); + for (const key of new SafeArrayIterator(ObjectKeys(ret))) { + if ( + (typeof ret[key] === "object" || typeof ret[key] === "function") && + ret[key] !== null + ) { + delete ret[key]; } } - } else { - let i = 0; - while (true) { - let el; + ret.stylize = ObjectSetPrototypeOf((value, flavour) => { + let stylized; try { - const res = iter.next(); - if (res.done) { - break; - } - el = res.value; - } catch (err) { - if (valueIsTypedArray) { - // TypedArray.prototype.entries doesn't throw, unless the ArrayBuffer - // is detached. We don't want to show the exception in that case, so - // we catch it here and pretend the ArrayBuffer has no entries (like - // Chrome DevTools does). - break; - } - throw err; + stylized = `${ctx.stylize(value, flavour)}`; + } catch { + // Continue regardless of error. } - if (i < inspectOptions.iterableLimit) { - inspectOptions.indentLevel++; - ArrayPrototypePush( - entries, - options.entryHandler( - el, - inspectOptions, - ), + + if (typeof stylized !== "string") return value; + // `stylized` is a string as it should be, which is safe to pass along. + return stylized; + }, null); + } + + return ret; +} + +// Note: using `formatValue` directly requires the indentation level to be +// corrected by setting `ctx.indentationLvL += diff` and then to decrease the +// value afterwards again. +function formatValue( + ctx, + value, + recurseTimes, + typedArray, +) { + // Primitive types cannot have properties. + if ( + typeof value !== "object" && + typeof value !== "function" && + !isUndetectableObject(value) + ) { + return formatPrimitive(ctx.stylize, value, ctx); + } + if (value === null) { + return ctx.stylize("null", "null"); + } + + // Memorize the context for custom inspection on proxies. + const context = value; + // Always check for proxies to prevent side effects and to prevent triggering + // any proxy handlers. + // TODO(wafuwafu13): Set Proxy + const proxyDetails = core.getProxyDetails(value); + // const proxy = getProxyDetails(value, !!ctx.showProxy); + // if (proxy !== undefined) { + // if (ctx.showProxy) { + // return formatProxy(ctx, proxy, recurseTimes); + // } + // value = proxy; + // } + + // Provide a hook for user-specified inspect functions. + // Check that value is an object with an inspect function on it. + if (ctx.customInspect) { + if ( + ReflectHas(value, customInspect) && + typeof value[customInspect] === "function" + ) { + return String(value[customInspect](inspect, ctx)); + } else if ( + ReflectHas(value, privateCustomInspect) && + typeof value[privateCustomInspect] === "function" + ) { + // TODO(nayeemrmn): `inspect` is passed as an argument because custom + // inspect implementations in `extensions` need it, but may not have access + // to the `Deno` namespace in web workers. Remove when the `Deno` + // namespace is always enabled. + return String(value[privateCustomInspect](inspect, ctx)); + } else if (ReflectHas(value, nodeCustomInspectSymbol)) { + const maybeCustom = value[nodeCustomInspectSymbol]; + if ( + typeof maybeCustom === "function" && + // Filter out the util module, its inspect function is special. + maybeCustom !== ctx.inspect && + // Also filter out any prototype objects using the circular check. + !(value.constructor && value.constructor.prototype === value) + ) { + // This makes sure the recurseTimes are reported as before while using + // a counter internally. + const depth = ctx.depth === null ? null : ctx.depth - recurseTimes; + // TODO(@crowlKats): proxy handling + const isCrossContext = !ObjectPrototypeIsPrototypeOf( + ObjectPrototype, + context, ); - inspectOptions.indentLevel--; - } else { - break; + const ret = FunctionPrototypeCall( + maybeCustom, + context, + depth, + getUserOptions(ctx, isCrossContext), + ctx.inspect, + ); + // If the custom inspection method returned `this`, don't go into + // infinite recursion. + if (ret !== context) { + if (typeof ret !== "string") { + return formatValue(ctx, ret, recurseTimes); + } + return StringPrototypeReplaceAll( + ret, + "\n", + `\n${StringPrototypeRepeat(" ", ctx.indentationLvl)}`, + ); + } } - i++; } } - if (options.sort) { - ArrayPrototypeSort(entries); + // Using an array here is actually better for the average case than using + // a Set. `seen` will only check for the depth and will never grow too large. + if (ctx.seen.includes(value)) { + let index = 1; + if (ctx.circular === undefined) { + ctx.circular = new SafeMap(); + ctx.circular.set(value, index); + } else { + index = ctx.circular.get(value); + if (index === undefined) { + index = ctx.circular.size + 1; + ctx.circular.set(value, index); + } + } + return ctx.stylize(`[Circular *${index}]`, "special"); } - if (entriesLengthWithoutEmptyItems > inspectOptions.iterableLimit) { - const nmore = entriesLengthWithoutEmptyItems - - inspectOptions.iterableLimit; - ArrayPrototypePush(entries, `... ${nmore} more items`); + return formatRaw(ctx, value, recurseTimes, typedArray, proxyDetails); +} + +function getClassBase(value, constructor, tag) { + const hasName = ObjectPrototypeHasOwnProperty(value, "name"); + const name = (hasName && value.name) || "(anonymous)"; + let base = `class ${name}`; + if (constructor !== "Function" && constructor !== null) { + base += ` [${constructor}]`; } + if (tag !== "" && constructor !== tag) { + base += ` [${tag}]`; + } + if (constructor !== null) { + const superName = ObjectGetPrototypeOf(value).name; + if (superName) { + base += ` extends ${superName}`; + } + } else { + base += " extends [null prototype]"; + } + return `[${base}]`; +} - const iPrefix = `${options.displayName ? options.displayName + " " : ""}`; +const stripCommentsRegExp = new SafeRegExp( + "(\\/\\/.*?\\n)|(\\/\\*(.|\\n)*?\\*\\/)", + "g", +); +const classRegExp = new SafeRegExp("^(\\s+[^(]*?)\\s*{"); - const level = inspectOptions.indentLevel; - const initIndentation = `\n${ - StringPrototypeRepeat(DEFAULT_INDENT, level + 1) - }`; - const entryIndentation = `,\n${ - StringPrototypeRepeat(DEFAULT_INDENT, level + 1) - }`; - const closingDelimIndentation = StringPrototypeRepeat( - DEFAULT_INDENT, - level, - ); - const closingIndentation = `${ - inspectOptions.trailingComma ? "," : "" - }\n${closingDelimIndentation}`; - - let iContent; - if (entries.length === 0 && !inspectOptions.compact) { - iContent = `\n${closingDelimIndentation}`; - } else if (options.group && entries.length > MIN_GROUP_LENGTH) { - const groups = groupEntries(entries, level, value); - iContent = `${initIndentation}${ - ArrayPrototypeJoin(groups, entryIndentation) - }${closingIndentation}`; - } else { - iContent = entries.length === 0 - ? "" - : ` ${ArrayPrototypeJoin(entries, ", ")} `; +function getFunctionBase(value, constructor, tag) { + const stringified = FunctionPrototypeToString(value); + if ( + StringPrototypeStartsWith(stringified, "class") && + StringPrototypeEndsWith(stringified, "}") + ) { + const slice = StringPrototypeSlice(stringified, 5, -1); + const bracketIndex = StringPrototypeIndexOf(slice, "{"); if ( - colors.stripColor(iContent).length > LINE_BREAKING_LENGTH || - !inspectOptions.compact + bracketIndex !== -1 && + (!StringPrototypeIncludes( + StringPrototypeSlice(slice, 0, bracketIndex), + "(", + ) || + // Slow path to guarantee that it's indeed a class. + RegExpPrototypeExec( + classRegExp, + RegExpPrototypeSymbolReplace(stripCommentsRegExp, slice), + ) !== null) ) { - iContent = `${initIndentation}${ - ArrayPrototypeJoin(entries, entryIndentation) - }${closingIndentation}`; + return getClassBase(value, constructor, tag); } } - - return `${iPrefix}${options.delims[0]}${iContent}${options.delims[1]}`; + let type = "Function"; + if (isGeneratorFunction(value)) { + type = `Generator${type}`; + } + if (isAsyncFunction(value)) { + type = `Async${type}`; + } + if (isAsyncGeneratorFunction(value)) { + type = `AsyncGenerator${type}`; + } + let base = `[${type}`; + if (constructor === null) { + base += " (null prototype)"; + } + if (value.name === "") { + base += " (anonymous)"; + } else { + base += `: ${value.name}`; + } + base += "]"; + if (constructor !== type && constructor !== null) { + base += ` ${constructor}`; + } + if (tag !== "" && constructor !== tag) { + base += ` [${tag}]`; + } + return base; } -// Ported from Node.js -// Copyright Node.js contributors. All rights reserved. -function groupEntries( - entries, - level, - value, - iterableLimit = 100, -) { - let totalLength = 0; - let maxLength = 0; - let entriesLength = entries.length; - if (iterableLimit < entriesLength) { - // This makes sure the "... n more items" part is not taken into account. - entriesLength--; +function formatRaw(ctx, value, recurseTimes, typedArray, proxyDetails) { + let keys; + let protoProps; + if (ctx.showHidden && (recurseTimes <= ctx.depth || ctx.depth === null)) { + protoProps = []; } - const separatorSpace = 2; // Add 1 for the space and 1 for the separator. - const dataLen = new Array(entriesLength); - // Calculate the total length of all output entries and the individual max - // entries length of all output entries. - // IN PROGRESS: Colors are being taken into account. - for (let i = 0; i < entriesLength; i++) { - // Taking colors into account: removing the ANSI color - // codes from the string before measuring its length - const len = colors.stripColor(entries[i]).length; - dataLen[i] = len; - totalLength += len + separatorSpace; - if (maxLength < len) maxLength = len; + + const constructor = getConstructorName(value, ctx, recurseTimes, protoProps); + // Reset the variable to check for this later on. + if (protoProps !== undefined && protoProps.length === 0) { + protoProps = undefined; } - // Add two to `maxLength` as we add a single whitespace character plus a comma - // in-between two entries. - const actualMax = maxLength + separatorSpace; - // Check if at least three entries fit next to each other and prevent grouping - // of arrays that contains entries of very different length (i.e., if a single - // entry is longer than 1/5 of all other entries combined). Otherwise the - // space in-between small entries would be enormous. + + let tag = value[SymbolToStringTag]; + // Only list the tag in case it's non-enumerable / not an own property. + // Otherwise we'd print this twice. if ( - actualMax * 3 + (level + 1) < LINE_BREAKING_LENGTH && - (totalLength / actualMax > 5 || maxLength <= 6) + typeof tag !== "string" + // TODO(wafuwafu13): Implement + // (tag !== "" && + // (ctx.showHidden + // ? Object.prototype.hasOwnProperty + // : Object.prototype.propertyIsEnumerable)( + // value, + // Symbol.toStringTag, + // )) ) { - const approxCharHeights = 2.5; - const averageBias = MathSqrt(actualMax - totalLength / entries.length); - const biasedMax = MathMax(actualMax - 3 - averageBias, 1); - // Dynamically check how many columns seem possible. - const columns = MathMin( - // Ideally a square should be drawn. We expect a character to be about 2.5 - // times as high as wide. This is the area formula to calculate a square - // which contains n rectangles of size `actualMax * approxCharHeights`. - // Divide that by `actualMax` to receive the correct number of columns. - // The added bias increases the columns for short entries. - MathRound( - MathSqrt(approxCharHeights * biasedMax * entriesLength) / biasedMax, - ), - // Do not exceed the breakLength. - MathFloor((LINE_BREAKING_LENGTH - (level + 1)) / actualMax), - // Limit the columns to a maximum of fifteen. - 15, - ); - // Return with the original output if no grouping should happen. - if (columns <= 1) { - return entries; - } - const tmp = []; - const maxLineLength = []; - for (let i = 0; i < columns; i++) { - let lineMaxLength = 0; - for (let j = i; j < entries.length; j += columns) { - if (dataLen[j] > lineMaxLength) lineMaxLength = dataLen[j]; - } - lineMaxLength += separatorSpace; - maxLineLength[i] = lineMaxLength; - } - let order = "padStart"; - if (value !== undefined) { - for (let i = 0; i < entries.length; i++) { + tag = ""; + } + let base = ""; + let formatter = () => []; + let braces; + let noIterator = true; + let i = 0; + const filter = ctx.showHidden ? 0 : 2; + + let extrasType = kObjectType; + + if (proxyDetails != null && ctx.showProxy) { + return `Proxy ` + formatValue(ctx, proxyDetails, recurseTimes); + } else { + // Iterators and the rest are split to reduce checks. + // We have to check all values in case the constructor is set to null. + // Otherwise it would not possible to identify all types properly. + if (ReflectHas(value, SymbolIterator) || constructor === null) { + noIterator = false; + if (ArrayIsArray(value)) { + // Only set the constructor for non ordinary ("Array [...]") arrays. + const prefix = (constructor !== "Array" || tag !== "") + ? getPrefix(constructor, tag, "Array", `(${value.length})`) + : ""; + keys = core.ops.op_get_non_index_property_names(value, filter); + braces = [`${prefix}[`, "]"]; if ( - typeof value[i] !== "number" && - typeof value[i] !== "bigint" + value.length === 0 && keys.length === 0 && protoProps === undefined ) { - order = "padEnd"; - break; + return `${braces[0]}]`; + } + extrasType = kArrayExtrasType; + formatter = formatArray; + } else if (isSet(value)) { + const size = SetPrototypeGetSize(value); + const prefix = getPrefix(constructor, tag, "Set", `(${size})`); + keys = getKeys(value, ctx.showHidden); + formatter = constructor !== null + ? FunctionPrototypeBind(formatSet, null, value) + : FunctionPrototypeBind(formatSet, null, SetPrototypeValues(value)); + if (size === 0 && keys.length === 0 && protoProps === undefined) { + return `${prefix}{}`; } + braces = [`${prefix}{`, "}"]; + } else if (isMap(value)) { + const size = MapPrototypeGetSize(value); + const prefix = getPrefix(constructor, tag, "Map", `(${size})`); + keys = getKeys(value, ctx.showHidden); + formatter = constructor !== null + ? FunctionPrototypeBind(formatMap, null, value) + : FunctionPrototypeBind(formatMap, null, MapPrototypeEntries(value)); + if (size === 0 && keys.length === 0 && protoProps === undefined) { + return `${prefix}{}`; + } + braces = [`${prefix}{`, "}"]; + } else if (isTypedArray(value)) { + keys = core.ops.op_get_non_index_property_names(value, filter); + const bound = value; + const fallback = ""; + if (constructor === null) { + // TODO(wafuwafu13): Implement + // fallback = TypedArrayPrototypeGetSymbolToStringTag(value); + // // Reconstruct the array information. + // bound = new primordials[fallback](value); + } + const size = TypedArrayPrototypeGetLength(value); + const prefix = getPrefix(constructor, tag, fallback, `(${size})`); + braces = [`${prefix}[`, "]"]; + if (value.length === 0 && keys.length === 0 && !ctx.showHidden) { + return `${braces[0]}]`; + } + // Special handle the value. The original value is required below. The + // bound function is required to reconstruct missing information. + formatter = FunctionPrototypeBind(formatTypedArray, null, bound, size); + extrasType = kArrayExtrasType; + } else if (isMapIterator(value)) { + keys = getKeys(value, ctx.showHidden); + braces = getIteratorBraces("Map", tag); + // Add braces to the formatter parameters. + formatter = FunctionPrototypeBind(formatIterator, null, braces); + } else if (isSetIterator(value)) { + keys = getKeys(value, ctx.showHidden); + braces = getIteratorBraces("Set", tag); + // Add braces to the formatter parameters. + formatter = FunctionPrototypeBind(formatIterator, null, braces); + } else { + noIterator = true; } } - // Each iteration creates a single line of grouped entries. - for (let i = 0; i < entriesLength; i += columns) { - // The last lines may contain less entries than columns. - const max = MathMin(i + columns, entriesLength); - let str = ""; - let j = i; - for (; j < max - 1; j++) { - const lengthOfColorCodes = entries[j].length - dataLen[j]; - const padding = maxLineLength[j - i] + lengthOfColorCodes; - str += `${entries[j]}, `[order](padding, " "); - } - if (order === "padStart") { - const lengthOfColorCodes = entries[j].length - dataLen[j]; - const padding = maxLineLength[j - i] + - lengthOfColorCodes - - separatorSpace; - str += StringPrototypePadStart(entries[j], padding, " "); + if (noIterator) { + keys = getKeys(value, ctx.showHidden); + braces = ["{", "}"]; + if (constructor === "Object") { + if (isArgumentsObject(value)) { + braces[0] = "[Arguments] {"; + } else if (tag !== "") { + braces[0] = `${getPrefix(constructor, tag, "Object")}{`; + } + if (keys.length === 0 && protoProps === undefined) { + return `${braces[0]}}`; + } + } else if (typeof value === "function") { + base = getFunctionBase(value, constructor, tag); + if (keys.length === 0 && protoProps === undefined) { + return ctx.stylize(base, "special"); + } + } else if (isRegExp(value)) { + // Make RegExps say that they are RegExps + base = RegExpPrototypeToString( + constructor !== null ? value : new SafeRegExp(value), + ); + const prefix = getPrefix(constructor, tag, "RegExp"); + if (prefix !== "RegExp ") { + base = `${prefix}${base}`; + } + if ( + (keys.length === 0 && protoProps === undefined) || + (recurseTimes > ctx.depth && ctx.depth !== null) + ) { + return ctx.stylize(base, "regexp"); + } + } else if (ObjectPrototypeIsPrototypeOf(DatePrototype, value)) { + const date = proxyDetails ? proxyDetails[0] : value; + if (isNaN(DatePrototypeGetTime(date))) { + return ctx.stylize("Invalid Date", "date"); + } else { + base = DatePrototypeToISOString(date); + if (keys.length === 0 && protoProps === undefined) { + return ctx.stylize(base, "date"); + } + } + } else if (ObjectPrototypeIsPrototypeOf(ErrorPrototype, value)) { + base = inspectError(value, ctx); + if (keys.length === 0 && protoProps === undefined) { + return base; + } + } else if (isAnyArrayBuffer(value)) { + // Fast path for ArrayBuffer and SharedArrayBuffer. + // Can't do the same for DataView because it has a non-primitive + // .buffer property that we need to recurse for. + const arrayType = isArrayBuffer(value) + ? "ArrayBuffer" + : "SharedArrayBuffer"; + + const prefix = getPrefix(constructor, tag, arrayType); + if (typedArray === undefined) { + formatter = formatArrayBuffer; + } else if (keys.length === 0 && protoProps === undefined) { + return prefix + + `{ byteLength: ${ + formatNumber(ctx.stylize, TypedArrayPrototypeGetByteLength(value)) + } }`; + } + braces[0] = `${prefix}{`; + ArrayPrototypeUnshift(keys, "byteLength"); + } else if (isDataView(value)) { + braces[0] = `${getPrefix(constructor, tag, "DataView")}{`; + // .buffer goes last, it's not a primitive like the others. + ArrayPrototypeUnshift(keys, "byteLength", "byteOffset", "buffer"); + } else if (isPromise(value)) { + braces[0] = `${getPrefix(constructor, tag, "Promise")}{`; + formatter = formatPromise; + } else if (isWeakSet(value)) { + braces[0] = `${getPrefix(constructor, tag, "WeakSet")}{`; + formatter = ctx.showHidden ? formatWeakSet : formatWeakCollection; + } else if (isWeakMap(value)) { + braces[0] = `${getPrefix(constructor, tag, "WeakMap")}{`; + formatter = ctx.showHidden ? formatWeakMap : formatWeakCollection; + } else if (isModuleNamespaceObject(value)) { + braces[0] = `${getPrefix(constructor, tag, "Module")}{`; + // Special handle keys for namespace objects. + formatter = formatNamespaceObject.bind(null, keys); + } else if (isBoxedPrimitive(value)) { + base = getBoxedBase(value, ctx, keys, constructor, tag); + if (keys.length === 0 && protoProps === undefined) { + return base; + } } else { - str += entries[j]; + if (keys.length === 0 && protoProps === undefined) { + // TODO(wafuwafu13): Implement + // if (isExternal(value)) { + // const address = getExternalValue(value).toString(16); + // return ctx.stylize(`[External: ${address}]`, 'special'); + // } + return `${getCtxStyle(value, constructor, tag)}{}`; + } + braces[0] = `${getCtxStyle(value, constructor, tag)}{`; } - ArrayPrototypePush(tmp, str); } - if (iterableLimit < entries.length) { - ArrayPrototypePush(tmp, entries[entriesLength]); + } + + if (recurseTimes > ctx.depth && ctx.depth !== null) { + let constructorName = StringPrototypeSlice( + getCtxStyle(value, constructor, tag), + 0, + -1, + ); + if (constructor !== null) { + constructorName = `[${constructorName}]`; } - entries = tmp; + return ctx.stylize(constructorName, "special"); } - return entries; -} + recurseTimes += 1; -let circular; -function handleCircular(value, cyan) { - let index = 1; - if (circular === undefined) { - circular = new SafeMap(); - MapPrototypeSet(circular, value, index); - } else { - index = MapPrototypeGet(circular, value); - if (index === undefined) { - index = MapPrototypeGetSize(circular) + 1; - MapPrototypeSet(circular, value, index); + ctx.seen.push(value); + ctx.currentDepth = recurseTimes; + let output; + const indentationLvl = ctx.indentationLvl; + try { + output = formatter(ctx, value, recurseTimes); + for (i = 0; i < keys.length; i++) { + ArrayPrototypePush( + output, + formatProperty(ctx, value, recurseTimes, keys[i], extrasType), + ); + } + if (protoProps !== undefined) { + ArrayPrototypePushApply(output, protoProps); } + } catch (err) { + const constructorName = StringPrototypeSlice( + getCtxStyle(value, constructor, tag), + 0, + -1, + ); + return handleMaxCallStackSize(ctx, err, constructorName, indentationLvl); } - // Circular string is cyan - return cyan(`[Circular *${index}]`); -} -function _inspectValue( - value, - inspectOptions, -) { - const proxyDetails = core.getProxyDetails(value); - if (proxyDetails != null && inspectOptions.showProxy) { - return inspectProxy(proxyDetails, inspectOptions); + if (ctx.circular !== undefined) { + const index = ctx.circular.get(value); + if (index !== undefined) { + const reference = ctx.stylize(`<ref *${index}>`, "special"); + // Add reference always to the very beginning of the output. + if (ctx.compact !== true) { + base = base === "" ? reference : `${reference} ${base}`; + } else { + braces[0] = `${reference} ${braces[0]}`; + } + } + } + ctx.seen.pop(); + + if (ctx.sorted) { + const comparator = ctx.sorted === true ? undefined : ctx.sorted; + if (extrasType === kObjectType) { + output = ArrayPrototypeSort(output, comparator); + } else if (keys.length > 1) { + const sorted = output.slice(output.length - keys.length).sort(comparator); + output.splice( + output.length - keys.length, + keys.length, + ...new SafeArrayIterator(sorted), + ); + } } - const green = maybeColor(colors.green, inspectOptions); - const yellow = maybeColor(colors.yellow, inspectOptions); - const gray = maybeColor(colors.gray, inspectOptions); - const cyan = maybeColor(colors.cyan, inspectOptions); - const bold = maybeColor(colors.bold, inspectOptions); - const red = maybeColor(colors.red, inspectOptions); + const res = reduceToSingleString( + ctx, + output, + base, + braces, + extrasType, + recurseTimes, + value, + ); + const budget = ctx.budget[ctx.indentationLvl] || 0; + const newLength = budget + res.length; + ctx.budget[ctx.indentationLvl] = newLength; + // If any indentationLvl exceeds this limit, limit further inspecting to the + // minimum. Otherwise the recursive algorithm might continue inspecting the + // object even though the maximum string size (~2 ** 28 on 32 bit systems and + // ~2 ** 30 on 64 bit systems) exceeded. The actual output is not limited at + // exactly 2 ** 27 but a bit higher. This depends on the object shape. + // This limit also makes sure that huge objects don't block the event loop + // significantly. + if (newLength > 2 ** 27) { + ctx.depth = -1; + } + return res; +} - switch (typeof value) { - case "string": - return green(quoteString(value, inspectOptions)); - case "number": // Numbers are yellow - // Special handling of -0 - return yellow(ObjectIs(value, -0) ? "-0" : `${value}`); - case "boolean": // booleans are yellow - return yellow(String(value)); - case "undefined": // undefined is gray - return gray(String(value)); - case "symbol": // Symbols are green - return green(maybeQuoteSymbol(value, inspectOptions)); - case "bigint": // Bigints are yellow - return yellow(`${value}n`); - case "function": // Function string is cyan - if (ctxHas(value)) { - // Circular string is cyan - return handleCircular(value, cyan); - } +const builtInObjectsRegExp = new SafeRegExp("^[A-Z][a-zA-Z0-9]+$"); +const builtInObjects = new SafeSet( + ObjectGetOwnPropertyNames(globalThis).filter((e) => + builtInObjectsRegExp.test(e) + ), +); - return inspectFunction(value, inspectOptions); - case "object": // null is bold - if (value === null) { - return bold("null"); +function addPrototypeProperties( + ctx, + main, + obj, + recurseTimes, + output, +) { + let depth = 0; + let keys; + let keySet; + do { + if (depth !== 0 || main === obj) { + obj = ObjectGetPrototypeOf(obj); + // Stop as soon as a null prototype is encountered. + if (obj === null) { + return; } - - if (ctxHas(value)) { - return handleCircular(value, cyan); + // 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; } + } - return inspectObject( - value, - inspectOptions, - proxyDetails, + if (depth === 0) { + keySet = new SafeSet(); + } else { + Array.prototype.forEach.call(keys, (key) => keySet.add(key)); + } + // Get all own property names and symbols. + keys = ReflectOwnKeys(obj); + Array.prototype.push.call(ctx.seen, main); + for (const key of new SafeArrayIterator(keys)) { + // Ignore the `constructor` property and keys that exist on layers above. + if ( + key === "constructor" || + // deno-lint-ignore no-prototype-builtins + main.hasOwnProperty(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, + main, ); - default: - // Not implemented is red - return red("[Not Implemented]"); - } + if (ctx.colors) { + // Faint! + Array.prototype.push.call(output, `\u001b[2m${value}\u001b[22m`); + } else { + Array.prototype.push.call(output, value); + } + } + Array.prototype.pop.call(ctx.seen); + // 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 inspectValue( - value, - inspectOptions, -) { - ArrayPrototypePush(CTX_STACK, value); - let x; +function isInstanceof(proto, object) { try { - x = _inspectValue(value, inspectOptions); - } finally { - ArrayPrototypePop(CTX_STACK); + return ObjectPrototypeIsPrototypeOf(proto, object); + } catch { + return false; } - return x; -} - -/** Surround the string in quotes. - * - * The quote symbol is chosen by taking the first of the `QUOTES` array which - * does not occur in the string. If they all occur, settle with `QUOTES[0]`. - * - * Insert a backslash before any occurrence of the chosen quote symbol and - * before any backslash. - */ -function quoteString(string, inspectOptions = DEFAULT_INSPECT_OPTIONS) { - const quotes = inspectOptions.quotes; - const quote = - ArrayPrototypeFind(quotes, (c) => !StringPrototypeIncludes(string, c)) ?? - quotes[0]; - const escapePattern = new SafeRegExp(`(?=[${quote}\\\\])`, "g"); - string = StringPrototypeReplace(string, escapePattern, "\\"); - string = replaceEscapeSequences(string); - return `${quote}${string}${quote}`; } -const ESCAPE_PATTERN = new SafeRegExp(/([\b\f\n\r\t\v])/g); -const ESCAPE_MAP = ObjectFreeze({ - "\b": "\\b", - "\f": "\\f", - "\n": "\\n", - "\r": "\\r", - "\t": "\\t", - "\v": "\\v", -}); +function getConstructorName(obj, ctx, recurseTimes, protoProps) { + let firstProto; + const tmp = obj; + while (obj || isUndetectableObject(obj)) { + const descriptor = ObjectGetOwnPropertyDescriptor(obj, "constructor"); + if ( + descriptor !== undefined && + typeof descriptor.value === "function" && + descriptor.value.name !== "" && + isInstanceof(descriptor.value.prototype, tmp) + ) { + if ( + protoProps !== undefined && + (firstProto !== obj || + !builtInObjects.has(descriptor.value.name)) + ) { + addPrototypeProperties( + ctx, + tmp, + firstProto || tmp, + recurseTimes, + protoProps, + ); + } + return String(descriptor.value.name); + } -// deno-lint-ignore no-control-regex -const ESCAPE_PATTERN2 = new SafeRegExp(/[\x00-\x1f\x7f-\x9f]/g); + obj = ObjectGetPrototypeOf(obj); + if (firstProto === undefined) { + firstProto = obj; + } + } -// Replace escape sequences that can modify output. -function replaceEscapeSequences(string) { - return StringPrototypeReplace( - StringPrototypeReplace( - string, - ESCAPE_PATTERN, - (c) => ESCAPE_MAP[c], - ), - new SafeRegExp(ESCAPE_PATTERN2), - (c) => - "\\x" + - StringPrototypePadStart( - NumberPrototypeToString(StringPrototypeCharCodeAt(c, 0), 16), - 2, - "0", - ), - ); -} + if (firstProto === null) { + return null; + } -const QUOTE_STRING_PATTERN = new SafeRegExp(/^[a-zA-Z_][a-zA-Z_0-9]*$/); + const res = core.ops.op_get_constructor_name(tmp); -// Surround a string with quotes when it is required (e.g the string not a valid identifier). -function maybeQuoteString(string, inspectOptions) { - if ( - RegExpPrototypeTest(QUOTE_STRING_PATTERN, string) - ) { - return replaceEscapeSequences(string); + if (recurseTimes > ctx.depth && ctx.depth !== null) { + return `${res} <Complex prototype>`; } - return quoteString(string, inspectOptions); -} + const protoConstr = getConstructorName( + firstProto, + ctx, + recurseTimes + 1, + protoProps, + ); -const QUOTE_SYMBOL_REG = new SafeRegExp(/^[a-zA-Z_][a-zA-Z_.0-9]*$/); + if (protoConstr === null) { + return `${res} <${ + inspect(firstProto, { + ...ctx, + customInspect: false, + depth: -1, + }) + }>`; + } -// Surround a symbol's description in quotes when it is required (e.g the description has non printable characters). -function maybeQuoteSymbol(symbol, inspectOptions) { - const description = SymbolPrototypeGetDescription(symbol); + return `${res} <${protoConstr}>`; +} - if (description === undefined) { - return SymbolPrototypeToString(symbol); +const formatPrimitiveRegExp = new SafeRegExp("(?<=\n)"); +function formatPrimitive(fn, value, ctx) { + if (typeof value === "string") { + let trailer = ""; + if (value.length > ctx.maxStringLength) { + const remaining = value.length - ctx.maxStringLength; + value = value.slice(0, ctx.maxStringLength); + trailer = `... ${remaining} more character${remaining > 1 ? "s" : ""}`; + } + if ( + ctx.compact !== true && + // TODO(BridgeAR): Add unicode support. Use the readline getStringWidth + // function. + value.length > kMinLineLength && + value.length > ctx.breakLength - ctx.indentationLvl - 4 + ) { + return value + .split(formatPrimitiveRegExp) + .map((line) => fn(quoteString(line, ctx), "string")) + .join(` +\n${" ".repeat(ctx.indentationLvl + 2)}`) + trailer; + } + return fn(quoteString(value, ctx), "string") + trailer; + } + if (typeof value === "number") { + return formatNumber(fn, value); + } + if (typeof value === "bigint") { + return formatBigInt(fn, value); } + if (typeof value === "boolean") { + return fn(`${value}`, "boolean"); + } + if (typeof value === "undefined") { + return fn("undefined", "undefined"); + } + // es6 symbol primitive + return fn(maybeQuoteSymbol(value, ctx), "symbol"); +} - if (RegExpPrototypeTest(QUOTE_SYMBOL_REG, description)) { - return SymbolPrototypeToString(symbol); +function getPrefix(constructor, tag, fallback, size = "") { + if (constructor === null) { + if (tag !== "" && fallback !== tag) { + return `[${fallback}${size}: null prototype] [${tag}] `; + } + return `[${fallback}${size}: null prototype] `; } - return `Symbol(${quoteString(description, inspectOptions)})`; + if (tag !== "" && constructor !== tag) { + return `${constructor}${size} [${tag}] `; + } + return `${constructor}${size} `; } -const CTX_STACK = []; -function ctxHas(x) { - // Only check parent contexts - return ArrayPrototypeIncludes( - ArrayPrototypeSlice(CTX_STACK, 0, CTX_STACK.length - 1), - x, - ); +function formatArray(ctx, value, recurseTimes) { + const valLen = value.length; + const len = MathMin(MathMax(0, ctx.maxArrayLength), valLen); + + const remaining = valLen - len; + const output = []; + for (let i = 0; i < len; i++) { + // Special handle sparse arrays. + // deno-lint-ignore no-prototype-builtins + if (!value.hasOwnProperty(i)) { + return formatSpecialArray(ctx, value, recurseTimes, len, output, i); + } + output.push(formatProperty(ctx, value, recurseTimes, i, kArrayType)); + } + if (remaining > 0) { + output.push(`... ${remaining} more item${remaining > 1 ? "s" : ""}`); + } + return output; } -// Print strings when they are inside of arrays or objects with quotes -function inspectValueWithQuotes( - value, - inspectOptions, -) { - const abbreviateSize = typeof inspectOptions.strAbbreviateSize === "undefined" - ? STR_ABBREVIATE_SIZE - : inspectOptions.strAbbreviateSize; - const green = maybeColor(colors.green, inspectOptions); - switch (typeof value) { - case "string": { - const trunc = value.length > abbreviateSize - ? StringPrototypeSlice(value, 0, abbreviateSize) + "..." - : value; - return green(quoteString(trunc, inspectOptions)); // Quoted strings are green +function getCtxStyle(value, constructor, tag) { + let fallback = ""; + if (constructor === null) { + fallback = core.ops.op_get_constructor_name(value); + if (fallback === tag) { + fallback = "Object"; } - default: - return inspectValue(value, inspectOptions); } + return getPrefix(constructor, tag, fallback); } -function inspectArray( - value, - inspectOptions, -) { - const gray = maybeColor(colors.gray, inspectOptions); - let lastValidIndex = 0; +// Look up the keys of the object. +function getKeys(value, showHidden) { let keys; - const options = { - typeName: "Array", - displayName: "", - delims: ["[", "]"], - entryHandler: (entry, inspectOptions) => { - const { 0: index, 1: val } = entry; - let i = index; - lastValidIndex = index; - if (!ObjectPrototypeHasOwnProperty(value, i)) { - let skipTo; - keys = keys || ObjectKeys(value); - i = value.length; - if (keys.length === 0) { - // fast path, all items are empty - skipTo = i; - } else { - // Not all indexes are empty or there's a non-index property - // Find first non-empty array index - while (keys.length) { - const key = ArrayPrototypeShift(keys); - // check if it's a valid array index - if (key > lastValidIndex && key < 2 ** 32 - 1) { - i = Number(key); - break; - } - } - - skipTo = i - 1; - } - const emptyItems = i - index; - const ending = emptyItems > 1 ? "s" : ""; - return { - entry: gray(`<${emptyItems} empty item${ending}>`), - skipTo, - }; - } else { - return { entry: inspectValueWithQuotes(val, inspectOptions) }; - } - }, - group: inspectOptions.compact, - sort: false, - }; - return inspectIterable(value, options, inspectOptions); + const symbols = ObjectGetOwnPropertySymbols(value); + if (showHidden) { + keys = ObjectGetOwnPropertyNames(value); + if (symbols.length !== 0) { + ArrayPrototypePushApply(keys, symbols); + } + } else { + // This might throw if `value` is a Module Namespace Object from an + // unevaluated module, but we don't want to perform the actual type + // check because it's expensive. + // TODO(devsnek): track https://github.com/tc39/ecma262/issues/1209 + // and modify this logic as needed. + try { + keys = ObjectKeys(value); + } catch (err) { + assert( + isNativeError(err) && err.name === "ReferenceError" && + isModuleNamespaceObject(value), + ); + keys = ObjectGetOwnPropertyNames(value); + } + if (symbols.length !== 0) { + const filter = (key) => ObjectPrototypePropertyIsEnumerable(value, key); + ArrayPrototypePushApply(keys, ArrayPrototypeFilter(symbols, filter)); + } + } + return keys; } -function inspectTypedArray( - typedArrayName, - value, - inspectOptions, -) { - const valueLength = value.length; - const options = { - typeName: typedArrayName, - displayName: `${typedArrayName}(${valueLength})`, - delims: ["[", "]"], - entryHandler: (entry, inspectOptions) => { - const val = entry[1]; - inspectOptions.indentLevel++; - const inspectedValue = inspectValueWithQuotes(val, inspectOptions); - inspectOptions.indentLevel--; - return inspectedValue; - }, - group: inspectOptions.compact, - sort: false, - }; - return inspectIterable(value, options, inspectOptions); +function formatSet(value, ctx, _ignored, recurseTimes) { + ctx.indentationLvl += 2; + + const values = [...new SafeSetIterator(value)]; + const valLen = SetPrototypeGetSize(value); + const len = MathMin(MathMax(0, ctx.iterableLimit), valLen); + + const remaining = valLen - len; + const output = []; + for (let i = 0; i < len; i++) { + output.push(formatValue(ctx, values[i], recurseTimes)); + } + if (remaining > 0) { + output.push(`... ${remaining} more item${remaining > 1 ? "s" : ""}`); + } + + ctx.indentationLvl -= 2; + return output; } -function inspectSet( - value, - inspectOptions, -) { - const options = { - typeName: "Set", - displayName: "Set", - delims: ["{", "}"], - entryHandler: (entry, inspectOptions) => { - const val = entry[1]; - inspectOptions.indentLevel++; - const inspectedValue = inspectValueWithQuotes(val, inspectOptions); - inspectOptions.indentLevel--; - return inspectedValue; - }, - group: false, - sort: inspectOptions.sorted, - }; - return inspectIterable(value, options, inspectOptions); +function formatMap(value, ctx, _gnored, recurseTimes) { + ctx.indentationLvl += 2; + + const values = [...new SafeMapIterator(value)]; + const valLen = MapPrototypeGetSize(value); + const len = MathMin(MathMax(0, ctx.iterableLimit), valLen); + + const remaining = valLen - len; + const output = []; + for (let i = 0; i < len; i++) { + output.push( + `${formatValue(ctx, values[i][0], recurseTimes)} => ${ + formatValue(ctx, values[i][1], recurseTimes) + }`, + ); + } + if (remaining > 0) { + output.push(`... ${remaining} more item${remaining > 1 ? "s" : ""}`); + } + + ctx.indentationLvl -= 2; + return output; } -function inspectMap( +function formatTypedArray( value, - inspectOptions, + length, + ctx, + _ignored, + recurseTimes, ) { - const options = { - typeName: "Map", - displayName: "Map", - delims: ["{", "}"], - entryHandler: (entry, inspectOptions) => { - const { 0: key, 1: val } = entry; - inspectOptions.indentLevel++; - const inspectedValue = `${ - inspectValueWithQuotes(key, inspectOptions) - } => ${inspectValueWithQuotes(val, inspectOptions)}`; - inspectOptions.indentLevel--; - return inspectedValue; - }, - group: false, - sort: inspectOptions.sorted, - }; - return inspectIterable( - value, - options, - inspectOptions, - ); + const maxLength = MathMin(MathMax(0, ctx.maxArrayLength), length); + const remaining = value.length - maxLength; + const output = new Array(maxLength); + const elementFormatter = value.length > 0 && typeof value[0] === "number" + ? formatNumber + : formatBigInt; + for (let i = 0; i < maxLength; ++i) { + output[i] = elementFormatter(ctx.stylize, value[i]); + } + if (remaining > 0) { + output[maxLength] = `... ${remaining} more item${remaining > 1 ? "s" : ""}`; + } + 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 new SafeArrayIterator([ + "BYTES_PER_ELEMENT", + "length", + "byteLength", + "byteOffset", + "buffer", + ]) + ) { + const str = formatValue(ctx, value[key], recurseTimes, true); + Array.prototype.push.call(output, `[${key}]: ${str}`); + } + ctx.indentationLvl -= 2; + } + return output; } -function inspectWeakSet(inspectOptions) { - const cyan = maybeColor(colors.cyan, inspectOptions); - return `WeakSet { ${cyan("[items unknown]")} }`; // as seen in Node, with cyan color +function getIteratorBraces(type, tag) { + if (tag !== `${type} Iterator`) { + if (tag !== "") { + tag += "] ["; + } + tag += `${type} Iterator`; + } + return [`[${tag}] {`, "}"]; } -function inspectWeakMap(inspectOptions) { - const cyan = maybeColor(colors.cyan, inspectOptions); - return `WeakMap { ${cyan("[items unknown]")} }`; // as seen in Node, with cyan color -} +const iteratorRegExp = new SafeRegExp(" Iterator] {$"); +function formatIterator(braces, ctx, value, recurseTimes) { + // TODO(wafuwafu13): Implement + // const { 0: entries, 1: isKeyValue } = previewEntries(value, true); + const { 0: entries, 1: isKeyValue } = value; + if (isKeyValue) { + // Mark entry iterators as such. + braces[0] = braces[0].replace(iteratorRegExp, " Entries] {"); + return formatMapIterInner(ctx, recurseTimes, entries, kMapEntries); + } -function inspectDate(value, inspectOptions) { - // without quotes, ISO format, in magenta like before - const magenta = maybeColor(colors.magenta, inspectOptions); - return magenta( - isInvalidDate(value) ? "Invalid Date" : DatePrototypeToISOString(value), - ); + return formatSetIterInner(ctx, recurseTimes, entries, kIterator); } -function inspectRegExp(value, inspectOptions) { - const red = maybeColor(colors.red, inspectOptions); - return red(RegExpPrototypeToString(value)); // RegExps are red +function handleCircular(value, ctx) { + let index = 1; + if (ctx.circular === undefined) { + ctx.circular = new SafeMap(); + MapPrototypeSet(ctx.circular, value, index); + } else { + index = MapPrototypeGet(ctx.circular, value); + if (index === undefined) { + index = MapPrototypeGetSize(ctx.circular) + 1; + MapPrototypeSet(ctx.circular, value, index); + } + } + // Circular string is cyan + return ctx.stylize(`[Circular *${index}]`, "special"); } const AGGREGATE_ERROR_HAS_AT_PATTERN = new SafeRegExp(/\s+at/); const AGGREGATE_ERROR_NOT_EMPTY_LINE_PATTERN = new SafeRegExp(/^(?!\s*$)/gm); -function inspectError(value, cyan) { +function inspectError(value, ctx) { const causes = [value]; let err = value; while (err.cause) { if (ArrayPrototypeIncludes(causes, err.cause)) { - ArrayPrototypePush(causes, handleCircular(err.cause, cyan)); + ArrayPrototypePush(causes, handleCircular(err.cause, ctx)); break; } else { ArrayPrototypePush(causes, err.cause); @@ -1019,10 +1518,14 @@ function inspectError(value, cyan) { const refMap = new SafeMap(); for (let i = 0; i < causes.length; ++i) { const cause = causes[i]; - if (circular !== undefined) { - const index = MapPrototypeGet(circular, cause); + if (ctx.circular !== undefined) { + const index = MapPrototypeGet(ctx.circular, cause); if (index !== undefined) { - MapPrototypeSet(refMap, cause, cyan(`<ref *${index}> `)); + MapPrototypeSet( + refMap, + cause, + ctx.stylize(`<ref *${index}> `, "special"), + ); } } } @@ -1060,9 +1563,13 @@ function inspectError(value, cyan) { finalMessage += "\n"; finalMessage += ArrayPrototypeJoin(stackLines, "\n"); } else { - finalMessage += value.stack; + const stack = value.stack; + if (stack?.includes("\n at")) { + finalMessage += stack; + } else { + finalMessage += `[${stack || value.toString()}]`; + } } - finalMessage += ArrayPrototypeJoin( ArrayPrototypeMap( causes, @@ -1076,40 +1583,54 @@ function inspectError(value, cyan) { return finalMessage; } -function inspectStringObject(value, inspectOptions) { - const cyan = maybeColor(colors.cyan, inspectOptions); - return cyan(`[String: "${StringPrototypeToString(value)}"]`); // wrappers are in cyan -} +const hexSliceLookupTable = function () { + const alphabet = "0123456789abcdef"; + const table = new Array(256); + for (let i = 0; i < 16; ++i) { + const i16 = i * 16; + for (let j = 0; j < 16; ++j) { + table[i16 + j] = alphabet[i] + alphabet[j]; + } + } + return table; +}(); -function inspectBooleanObject(value, inspectOptions) { - const cyan = maybeColor(colors.cyan, inspectOptions); - return cyan(`[Boolean: ${BooleanPrototypeToString(value)}]`); // wrappers are in cyan +function hexSlice(buf, start, end) { + const len = buf.length; + if (!start || start < 0) { + start = 0; + } + if (!end || end < 0 || end > len) { + end = len; + } + let out = ""; + for (let i = start; i < end; ++i) { + out += hexSliceLookupTable[buf[i]]; + } + return out; } -function inspectNumberObject(value, inspectOptions) { - const cyan = maybeColor(colors.cyan, inspectOptions); - // Special handling of -0 - return cyan( - `[Number: ${ - ObjectIs(NumberPrototypeValueOf(value), -0) - ? "-0" - : NumberPrototypeToString(value) - }]`, - ); // wrappers are in cyan -} +const arrayBufferRegExp = new SafeRegExp("(.{2})", "g"); +function formatArrayBuffer(ctx, value) { + let buffer; + try { + buffer = new Uint8Array(value); + } catch { + return [ctx.stylize("(detached)", "special")]; + } + let str = hexSlice(buffer, 0, MathMin(ctx.maxArrayLength, buffer.length)) + .replace(arrayBufferRegExp, "$1 ").trim(); -function inspectBigIntObject(value, inspectOptions) { - const cyan = maybeColor(colors.cyan, inspectOptions); - return cyan(`[BigInt: ${BigIntPrototypeToString(value)}n]`); // wrappers are in cyan + const remaining = buffer.length - ctx.maxArrayLength; + if (remaining > 0) { + str += ` ... ${remaining} more byte${remaining > 1 ? "s" : ""}`; + } + return [`${ctx.stylize("[Uint8Contents]", "special")}: <${str}>`]; } -function inspectSymbolObject(value, inspectOptions) { - const cyan = maybeColor(colors.cyan, inspectOptions); - return cyan( - `[Symbol: ${ - maybeQuoteSymbol(SymbolPrototypeValueOf(value), inspectOptions) - }]`, - ); // wrappers are in cyan +function formatNumber(fn, value) { + // Format -0 as '-0'. Checking `value === -0` won't distinguish 0 from -0. + return fn(ObjectIs(value, -0) ? "-0" : `${value}`, "number"); } const PromiseState = { @@ -1118,290 +1639,896 @@ const PromiseState = { Rejected: 2, }; -function inspectPromise( +function formatPromise(ctx, value, recurseTimes) { + let output; + // TODO(wafuwafu13): Implement + const { 0: state, 1: result } = core.getPromiseDetails(value); + if (state === PromiseState.Pending) { + output = [ctx.stylize("<pending>", "special")]; + } else { + ctx.indentationLvl += 2; + const str = formatValue(ctx, result, recurseTimes); + ctx.indentationLvl -= 2; + output = [ + state === PromiseState.Rejected + ? `${ctx.stylize("<rejected>", "special")} ${str}` + : str, + ]; + } + return output; +} + +function formatWeakCollection(ctx) { + return [ctx.stylize("<items unknown>", "special")]; +} + +function formatWeakSet(ctx, value, recurseTimes) { + // TODO(wafuwafu13): Implement + // const entries = previewEntries(value); + const entries = value; + return formatSetIterInner(ctx, recurseTimes, entries, kWeak); +} + +function formatWeakMap(ctx, value, recurseTimes) { + // TODO(wafuwafu13): Implement + // const entries = previewEntries(value); + const entries = value; + return formatMapIterInner(ctx, recurseTimes, entries, kWeak); +} + +function formatProperty( + ctx, value, - inspectOptions, + recurseTimes, + key, + type, + desc, + original = value, ) { - const cyan = maybeColor(colors.cyan, inspectOptions); - const red = maybeColor(colors.red, inspectOptions); - - const { 0: state, 1: result } = core.getPromiseDetails(value); + let name, str; + let extra = " "; + desc = desc || ObjectGetOwnPropertyDescriptor(value, key) || + { value: value[key], enumerable: true }; + if (desc.value !== undefined) { + const diff = (ctx.compact !== true || type !== kObjectType) ? 2 : 3; + ctx.indentationLvl += diff; + str = formatValue(ctx, desc.value, recurseTimes); + if (diff === 3 && ctx.breakLength < getStringWidth(str, ctx.colors)) { + extra = `\n${" ".repeat(ctx.indentationLvl)}`; + } + ctx.indentationLvl -= diff; + } else if (desc.get !== undefined) { + const label = desc.set !== undefined ? "Getter/Setter" : "Getter"; + const s = ctx.stylize; + const sp = "special"; + if ( + ctx.getters && (ctx.getters === true || + (ctx.getters === "get" && desc.set === undefined) || + (ctx.getters === "set" && desc.set !== undefined)) + ) { + try { + const tmp = desc.get.call(original); + ctx.indentationLvl += 2; + if (tmp === null) { + str = `${s(`[${label}:`, sp)} ${s("null", "null")}${s("]", sp)}`; + } else if (typeof tmp === "object") { + str = `${s(`[${label}]`, sp)} ${formatValue(ctx, tmp, recurseTimes)}`; + } else { + const primitive = formatPrimitive(s, tmp, ctx); + str = `${s(`[${label}:`, sp)} ${primitive}${s("]", sp)}`; + } + ctx.indentationLvl -= 2; + } catch (err) { + const message = `<Inspection threw (${err.message})>`; + str = `${s(`[${label}:`, sp)} ${message}${s("]", sp)}`; + } + } else { + str = ctx.stylize(`[${label}]`, sp); + } + } else if (desc.set !== undefined) { + str = ctx.stylize("[Setter]", "special"); + } else { + str = ctx.stylize("undefined", "undefined"); + } + if (type === kArrayType) { + return str; + } + if (typeof key === "symbol") { + name = `[${ctx.stylize(maybeQuoteSymbol(key, ctx), "symbol")}]`; + } else if (key === "__proto__") { + name = "['__proto__']"; + } else if (desc.enumerable === false) { + const tmp = key.replace(strEscapeSequencesReplacer, escapeFn); - if (state === PromiseState.Pending) { - return `Promise { ${cyan("<pending>")} }`; + name = `[${tmp}]`; + } else if (keyStrRegExp.test(key)) { + name = ctx.stylize(key, "name"); + } else { + name = ctx.stylize(quoteString(key, ctx), "string"); } + return `${name}:${extra}${str}`; +} - const prefix = state === PromiseState.Fulfilled - ? "" - : `${red("<rejected>")} `; +function handleMaxCallStackSize( + _ctx, + _err, + _constructorName, + _indentationLvl, +) { + // TODO(wafuwafu13): Implement + // if (isStackOverflowError(err)) { + // ctx.seen.pop(); + // ctx.indentationLvl = indentationLvl; + // return ctx.stylize( + // `[${constructorName}: Inspection interrupted ` + + // 'prematurely. Maximum call stack size exceeded.]', + // 'special' + // ); + // } + // /* c8 ignore next */ + // assert.fail(err.stack); +} - inspectOptions.indentLevel++; - const str = `${prefix}${inspectValueWithQuotes(result, inspectOptions)}`; - inspectOptions.indentLevel--; +const colorRegExp = new SafeRegExp("\u001b\\[\\d\\d?m", "g"); +function removeColors(str) { + return str.replace(colorRegExp, ""); +} - if (str.length + PROMISE_STRING_BASE_LENGTH > LINE_BREAKING_LENGTH) { - return `Promise {\n${ - StringPrototypeRepeat(DEFAULT_INDENT, inspectOptions.indentLevel + 1) - }${str}\n}`; +function isBelowBreakLength(ctx, output, start, base) { + // Each entry is separated by at least a comma. Thus, we start with a total + // length of at least `output.length`. In addition, some cases have a + // whitespace in-between each other that is added to the total as well. + // TODO(BridgeAR): Add unicode support. Use the readline getStringWidth + // function. Check the performance overhead and make it an opt-in in case it's + // significant. + let totalLength = output.length + start; + if (totalLength + output.length > ctx.breakLength) { + return false; + } + for (let i = 0; i < output.length; i++) { + if (ctx.colors) { + totalLength += removeColors(output[i]).length; + } else { + totalLength += output[i].length; + } + if (totalLength > ctx.breakLength) { + return false; + } } + // Do not line up properties on the same line if `base` contains line breaks. + return base === "" || !StringPrototypeIncludes(base, "\n"); +} - return `Promise { ${str} }`; +function formatBigInt(fn, value) { + return fn(`${value}n`, "bigint"); } -function inspectProxy( - targetAndHandler, - inspectOptions, +function formatNamespaceObject( + keys, + ctx, + value, + recurseTimes, ) { - return `Proxy ${inspectArray(targetAndHandler, inspectOptions)}`; + const output = new Array(keys.length); + for (let i = 0; i < keys.length; i++) { + try { + output[i] = formatProperty( + ctx, + value, + recurseTimes, + keys[i], + kObjectType, + ); + } catch (_err) { + // TODO(wafuwfu13): Implement + // assert(isNativeError(err) && err.name === 'ReferenceError'); + // Use the existing functionality. This makes sure the indentation and + // line breaks are always correct. Otherwise it is very difficult to keep + // this aligned, even though this is a hacky way of dealing with this. + const tmp = { [keys[i]]: "" }; + output[i] = formatProperty(ctx, tmp, recurseTimes, keys[i], kObjectType); + const pos = output[i].lastIndexOf(" "); + // We have to find the last whitespace and have to replace that value as + // it will be visualized as a regular string. + output[i] = output[i].slice(0, pos + 1) + + ctx.stylize("<uninitialized>", "special"); + } + } + // Reset the keys to an empty array. This prevents duplicated inspection. + keys.length = 0; + return output; } -function inspectRawObject( +// The array is sparse and/or has extra keys +function formatSpecialArray( + ctx, value, - inspectOptions, + recurseTimes, + maxLength, + output, + i, ) { - const cyan = maybeColor(colors.cyan, inspectOptions); + const keys = ObjectKeys(value); + let index = i; + for (; i < keys.length && output.length < maxLength; i++) { + const key = keys[i]; + const tmp = +key; + // Arrays can only have up to 2^32 - 1 entries + if (tmp > 2 ** 32 - 2) { + break; + } + if (`${index}` !== key) { + if (!numberRegExp.test(key)) { + break; + } + const emptyItems = tmp - index; + const ending = emptyItems > 1 ? "s" : ""; + const message = `<${emptyItems} empty item${ending}>`; + output.push(ctx.stylize(message, "undefined")); + index = tmp; + if (output.length === maxLength) { + break; + } + } + output.push(formatProperty(ctx, value, recurseTimes, key, kArrayType)); + index++; + } + const remaining = value.length - index; + if (output.length !== maxLength) { + if (remaining > 0) { + const ending = remaining > 1 ? "s" : ""; + const message = `<${remaining} empty item${ending}>`; + output.push(ctx.stylize(message, "undefined")); + } + } else if (remaining > 0) { + output.push(`... ${remaining} more item${remaining > 1 ? "s" : ""}`); + } + return output; +} - if (inspectOptions.indentLevel >= inspectOptions.depth) { - return [cyan("[Object]"), ""]; // wrappers are in cyan +function getBoxedBase( + value, + ctx, + keys, + constructor, + tag, +) { + let type; + if (isNumberObject(value)) { + type = "Number"; + } else if (isStringObject(value)) { + type = "String"; + // For boxed Strings, we have to remove the 0-n indexed entries, + // since they just noisy up the output and are redundant + // Make boxed primitive Strings look like such + keys.splice(0, value.length); + } else if (isBooleanObject(value)) { + type = "Boolean"; + } else if (isBigIntObject(value)) { + type = "BigInt"; + } else { + type = "Symbol"; + } + let base = `[${type}`; + if (type !== constructor) { + if (constructor === null) { + base += " (null prototype)"; + } else { + base += ` (${constructor})`; + } } - let baseString; + base += `: ${formatPrimitive(stylizeNoColor, value.valueOf(), ctx)}]`; + if (tag !== "" && tag !== constructor) { + base += ` [${tag}]`; + } + if (keys.length !== 0 || ctx.stylize === stylizeNoColor) { + return base; + } + return ctx.stylize(base, type.toLowerCase()); +} + +function reduceToSingleString( + ctx, + output, + base, + braces, + extrasType, + recurseTimes, + value, +) { + if (ctx.compact !== true) { + if (typeof ctx.compact === "number" && ctx.compact >= 1) { + // Memorize the original output length. In case the output is grouped, + // prevent lining up the entries on a single line. + const entries = output.length; + // Group array elements together if the array contains at least six + // separate entries. + if (extrasType === kArrayExtrasType && entries > 6) { + output = groupArrayElements(ctx, output, value); + } + // `ctx.currentDepth` is set to the most inner depth of the currently + // inspected object part while `recurseTimes` is the actual current depth + // that is inspected. + // + // Example: + // + // const a = { first: [ 1, 2, 3 ], second: { inner: [ 1, 2, 3 ] } } + // + // The deepest depth of `a` is 2 (a.second.inner) and `a.first` has a max + // depth of 1. + // + // Consolidate all entries of the local most inner depth up to + // `ctx.compact`, as long as the properties are smaller than + // `ctx.breakLength`. + if ( + ctx.currentDepth - recurseTimes < ctx.compact && + entries === output.length + ) { + // Line up all entries on a single line in case the entries do not + // exceed `breakLength`. Add 10 as constant to start next to all other + // factors that may reduce `breakLength`. + const start = output.length + ctx.indentationLvl + + braces[0].length + base.length + 10; + if (isBelowBreakLength(ctx, output, start, base)) { + const joinedOutput = ArrayPrototypeJoin(output, ", "); + if (!StringPrototypeIncludes(joinedOutput, "\n")) { + return `${base ? `${base} ` : ""}${braces[0]} ${joinedOutput}` + + ` ${braces[1]}`; + } + } + } + } + // Line up each entry on an individual line. + const indentation = `\n${StringPrototypeRepeat(" ", ctx.indentationLvl)}`; + return `${base ? `${base} ` : ""}${braces[0]}${indentation} ` + + `${ArrayPrototypeJoin(output, `,${indentation} `)}${ + ctx.trailingComma ? "," : "" + }${indentation}${braces[1]}`; + } + // Line up all entries on a single line in case the entries do not exceed + // `breakLength`. + if (isBelowBreakLength(ctx, output, 0, base)) { + return `${braces[0]}${base ? ` ${base}` : ""} ${ + ArrayPrototypeJoin(output, ", ") + } ` + + braces[1]; + } + const indentation = StringPrototypeRepeat(" ", ctx.indentationLvl); + // If the opening "brace" is too large, like in the case of "Set {", + // we need to force the first item to be on the next line or the + // items will not line up correctly. + const ln = base === "" && braces[0].length === 1 + ? " " + : `${base ? ` ${base}` : ""}\n${indentation} `; + // Line up each entry on an individual line. + return `${braces[0]}${ln}${ + ArrayPrototypeJoin(output, `,\n${indentation} `) + } ${braces[1]}`; +} - let shouldShowDisplayName = false; - let displayName = value[ - SymbolToStringTag - ]; - if (!displayName) { - displayName = getClassInstanceName(value); +function groupArrayElements(ctx, output, value) { + let totalLength = 0; + let maxLength = 0; + let i = 0; + let outputLength = output.length; + if (ctx.maxArrayLength < output.length) { + // This makes sure the "... n more items" part is not taken into account. + outputLength--; } + const separatorSpace = 2; // Add 1 for the space and 1 for the separator. + const dataLen = new Array(outputLength); + // Calculate the total length of all output entries and the individual max + // entries length of all output entries. We have to remove colors first, + // otherwise the length would not be calculated properly. + for (; i < outputLength; i++) { + const len = getStringWidth(output[i], ctx.colors); + dataLen[i] = len; + totalLength += len + separatorSpace; + if (maxLength < len) { + maxLength = len; + } + } + // Add two to `maxLength` as we add a single whitespace character plus a comma + // in-between two entries. + const actualMax = maxLength + separatorSpace; + // Check if at least three entries fit next to each other and prevent grouping + // of arrays that contains entries of very different length (i.e., if a single + // entry is longer than 1/5 of all other entries combined). Otherwise the + // space in-between small entries would be enormous. if ( - displayName && displayName !== "Object" && displayName !== "anonymous" + actualMax * 3 + ctx.indentationLvl < ctx.breakLength && + (totalLength / actualMax > 5 || maxLength <= 6) ) { - shouldShowDisplayName = true; - } - - const entries = []; - const stringKeys = ObjectKeys(value); - const symbolKeys = ObjectGetOwnPropertySymbols(value); - if (inspectOptions.sorted) { - ArrayPrototypeSort(stringKeys); - ArrayPrototypeSort( - symbolKeys, - (s1, s2) => - StringPrototypeLocaleCompare( - SymbolPrototypeGetDescription(s1) ?? "", - SymbolPrototypeGetDescription(s2) ?? "", - ), + const approxCharHeights = 2.5; + const averageBias = MathSqrt(actualMax - totalLength / output.length); + const biasedMax = MathMax(actualMax - 3 - averageBias, 1); + // Dynamically check how many columns seem possible. + const columns = MathMin( + // Ideally a square should be drawn. We expect a character to be about 2.5 + // times as high as wide. This is the area formula to calculate a square + // which contains n rectangles of size `actualMax * approxCharHeights`. + // Divide that by `actualMax` to receive the correct number of columns. + // The added bias increases the columns for short entries. + MathRound( + MathSqrt( + approxCharHeights * biasedMax * outputLength, + ) / biasedMax, + ), + // Do not exceed the breakLength. + MathFloor((ctx.breakLength - ctx.indentationLvl) / actualMax), + // Limit array grouping for small `compact` modes as the user requested + // minimal grouping. + ctx.compact * 4, + // Limit the columns to a maximum of fifteen. + 15, ); - } - - const red = maybeColor(colors.red, inspectOptions); - - inspectOptions.indentLevel++; - - for (let i = 0; i < stringKeys.length; ++i) { - const key = stringKeys[i]; - if (inspectOptions.getters) { - let propertyValue; - let error = null; - try { - propertyValue = value[key]; - } catch (error_) { - error = error_; + // Return with the original output if no grouping should happen. + if (columns <= 1) { + return output; + } + const tmp = []; + const maxLineLength = []; + for (let i = 0; i < columns; i++) { + let lineMaxLength = 0; + for (let j = i; j < output.length; j += columns) { + if (dataLen[j] > lineMaxLength) { + lineMaxLength = dataLen[j]; + } } - const inspectedValue = error == null - ? inspectValueWithQuotes(propertyValue, inspectOptions) - : red(`[Thrown ${error.name}: ${error.message}]`); - ArrayPrototypePush( - entries, - `${maybeQuoteString(key, inspectOptions)}: ${inspectedValue}`, - ); - } else { - const descriptor = ObjectGetOwnPropertyDescriptor(value, key); - if (descriptor.get !== undefined && descriptor.set !== undefined) { - ArrayPrototypePush( - entries, - `${maybeQuoteString(key, inspectOptions)}: [Getter/Setter]`, - ); - } else if (descriptor.get !== undefined) { - ArrayPrototypePush( - entries, - `${maybeQuoteString(key, inspectOptions)}: [Getter]`, - ); + lineMaxLength += separatorSpace; + maxLineLength[i] = lineMaxLength; + } + let order = StringPrototypePadStart; + if (value !== undefined) { + for (let i = 0; i < output.length; i++) { + if (typeof value[i] !== "number" && typeof value[i] !== "bigint") { + order = StringPrototypePadEnd; + break; + } + } + } + // Each iteration creates a single line of grouped entries. + for (let i = 0; i < outputLength; i += columns) { + // The last lines may contain less entries than columns. + const max = MathMin(i + columns, outputLength); + let str = ""; + let j = i; + for (; j < max - 1; j++) { + // Calculate extra color padding in case it's active. This has to be + // done line by line as some lines might contain more colors than + // others. + const padding = maxLineLength[j - i] + output[j].length - dataLen[j]; + str += order(`${output[j]}, `, padding, " "); + } + if (order === StringPrototypePadStart) { + const padding = maxLineLength[j - i] + + output[j].length - + dataLen[j] - + separatorSpace; + str += StringPrototypePadStart(output[j], padding, " "); } else { - ArrayPrototypePush( - entries, - `${maybeQuoteString(key, inspectOptions)}: ${ - inspectValueWithQuotes(value[key], inspectOptions) - }`, - ); + str += output[j]; } + ArrayPrototypePush(tmp, str); + } + if (ctx.maxArrayLength < output.length) { + ArrayPrototypePush(tmp, output[outputLength]); } + output = tmp; } + return output; +} - for (let i = 0; i < symbolKeys.length; ++i) { - const key = symbolKeys[i]; - if ( - !inspectOptions.showHidden && - !propertyIsEnumerable(value, key) - ) { - continue; +function formatMapIterInner( + ctx, + recurseTimes, + entries, + state, +) { + const maxArrayLength = MathMax(ctx.maxArrayLength, 0); + // Entries exist as [key1, val1, key2, val2, ...] + const len = entries.length / 2; + const remaining = len - maxArrayLength; + const maxLength = MathMin(maxArrayLength, len); + let output = new Array(maxLength); + let i = 0; + ctx.indentationLvl += 2; + if (state === kWeak) { + for (; i < maxLength; i++) { + const pos = i * 2; + output[i] = `${formatValue(ctx, entries[pos], recurseTimes)} => ${ + formatValue(ctx, entries[pos + 1], recurseTimes) + }`; } - - if (inspectOptions.getters) { - let propertyValue; - let error; - try { - propertyValue = value[key]; - } catch (error_) { - error = error_; - } - const inspectedValue = error == null - ? inspectValueWithQuotes(propertyValue, inspectOptions) - : red(`Thrown ${error.name}: ${error.message}`); - ArrayPrototypePush( - entries, - `[${maybeQuoteSymbol(key, inspectOptions)}]: ${inspectedValue}`, + // Sort all entries to have a halfway reliable output (if more entries than + // retrieved ones exist, we can not reliably return the same output) if the + // output is not sorted anyway. + if (!ctx.sorted) { + output = output.sort(); + } + } else { + for (; i < maxLength; i++) { + const pos = i * 2; + const res = [ + formatValue(ctx, entries[pos], recurseTimes), + formatValue(ctx, entries[pos + 1], recurseTimes), + ]; + output[i] = reduceToSingleString( + ctx, + res, + "", + ["[", "]"], + kArrayExtrasType, + recurseTimes, ); - } else { - const descriptor = ObjectGetOwnPropertyDescriptor(value, key); - if (descriptor.get !== undefined && descriptor.set !== undefined) { - ArrayPrototypePush( - entries, - `[${maybeQuoteSymbol(key, inspectOptions)}]: [Getter/Setter]`, - ); - } else if (descriptor.get !== undefined) { - ArrayPrototypePush( - entries, - `[${maybeQuoteSymbol(key, inspectOptions)}]: [Getter]`, - ); - } else { - ArrayPrototypePush( - entries, - `[${maybeQuoteSymbol(key, inspectOptions)}]: ${ - inspectValueWithQuotes(value[key], inspectOptions) - }`, - ); - } } } + ctx.indentationLvl -= 2; + if (remaining > 0) { + output.push(`... ${remaining} more item${remaining > 1 ? "s" : ""}`); + } + return output; +} - inspectOptions.indentLevel--; +function formatSetIterInner( + ctx, + recurseTimes, + entries, + state, +) { + const maxArrayLength = MathMax(ctx.maxArrayLength, 0); + const maxLength = MathMin(maxArrayLength, entries.length); + const output = new Array(maxLength); + ctx.indentationLvl += 2; + for (let i = 0; i < maxLength; i++) { + output[i] = formatValue(ctx, entries[i], recurseTimes); + } + ctx.indentationLvl -= 2; + if (state === kWeak && !ctx.sorted) { + // Sort all entries to have a halfway reliable output (if more entries than + // retrieved ones exist, we can not reliably return the same output) if the + // output is not sorted anyway. + output.sort(); + } + const remaining = entries.length - maxLength; + if (remaining > 0) { + Array.prototype.push.call( + output, + `... ${remaining} more item${remaining > 1 ? "s" : ""}`, + ); + } + return output; +} - // Making sure color codes are ignored when calculating the total length - const entriesText = colors.stripColor(ArrayPrototypeJoin(entries, "")); - const totalLength = entries.length + inspectOptions.indentLevel + - entriesText.length; +// Regex used for ansi escape code splitting +// Adopted from https://github.com/chalk/ansi-regex/blob/HEAD/index.js +// License: MIT, authors: @sindresorhus, Qix-, arjunmehta and LitoMore +// Matches all ansi escape code sequences in a string +const ansiPattern = "[\\u001B\\u009B][[\\]()#;?]*" + + "(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*" + + "|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)" + + "|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"; +const ansi = new SafeRegExp(ansiPattern, "g"); + +/** + * Returns the number of columns required to display the given string. + */ +export function getStringWidth(str, removeControlChars = true) { + let width = 0; - if (entries.length === 0) { - baseString = "{}"; - } else if ( - totalLength > LINE_BREAKING_LENGTH || - !inspectOptions.compact || - StringPrototypeIncludes(entriesText, "\n") - ) { - const entryIndent = StringPrototypeRepeat( - DEFAULT_INDENT, - inspectOptions.indentLevel + 1, - ); - const closingIndent = StringPrototypeRepeat( - DEFAULT_INDENT, - inspectOptions.indentLevel, - ); - baseString = `{\n${entryIndent}${ - ArrayPrototypeJoin(entries, `,\n${entryIndent}`) - }${inspectOptions.trailingComma ? "," : ""}\n${closingIndent}}`; - } else { - baseString = `{ ${ArrayPrototypeJoin(entries, ", ")} }`; + if (removeControlChars) { + str = stripVTControlCharacters(str); + } + str = str.normalize("NFC"); + for (const char of new SafeStringIterator(str)) { + const code = char.codePointAt(0); + if (isFullWidthCodePoint(code)) { + width += 2; + } else if (!isZeroWidthCodePoint(code)) { + width++; + } } - if (shouldShowDisplayName) { - baseString = `${displayName} ${baseString}`; + return width; +} + +const isZeroWidthCodePoint = (code) => { + return code <= 0x1F || // C0 control codes + (code >= 0x7F && code <= 0x9F) || // C1 control codes + (code >= 0x300 && code <= 0x36F) || // Combining Diacritical Marks + (code >= 0x200B && code <= 0x200F) || // Modifying Invisible Characters + // Combining Diacritical Marks for Symbols + (code >= 0x20D0 && code <= 0x20FF) || + (code >= 0xFE00 && code <= 0xFE0F) || // Variation Selectors + (code >= 0xFE20 && code <= 0xFE2F) || // Combining Half Marks + (code >= 0xE0100 && code <= 0xE01EF); // Variation Selectors +}; + +/** + * Remove all VT control characters. Use to estimate displayed string width. + */ +export function stripVTControlCharacters(str) { + return str.replace(ansi, ""); +} + +function hasOwnProperty(obj, v) { + if (obj == null) { + return false; } + return ObjectPrototypeHasOwnProperty(obj, v); +} - let refIndex = ""; - if (circular !== undefined) { - const index = MapPrototypeGet(circular, value); - if (index !== undefined) { - refIndex = cyan(`<ref *${index}> `); +// Copyright Joyent, Inc. and other Node contributors. MIT license. +// Forked from Node's lib/internal/cli_table.js + +const tableChars = { + middleMiddle: "\u2500", + rowMiddle: "\u253c", + topRight: "\u2510", + topLeft: "\u250c", + leftMiddle: "\u251c", + topMiddle: "\u252c", + bottomRight: "\u2518", + bottomLeft: "\u2514", + bottomMiddle: "\u2534", + rightMiddle: "\u2524", + left: "\u2502 ", + right: " \u2502", + middle: " \u2502 ", +}; + +function isFullWidthCodePoint(code) { + // Code points are partially derived from: + // http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt + return ( + code >= 0x1100 && + (code <= 0x115f || // Hangul Jamo + code === 0x2329 || // LEFT-POINTING ANGLE BRACKET + code === 0x232a || // RIGHT-POINTING ANGLE BRACKET + // CJK Radicals Supplement .. Enclosed CJK Letters and Months + (code >= 0x2e80 && code <= 0x3247 && code !== 0x303f) || + // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A + (code >= 0x3250 && code <= 0x4dbf) || + // CJK Unified Ideographs .. Yi Radicals + (code >= 0x4e00 && code <= 0xa4c6) || + // Hangul Jamo Extended-A + (code >= 0xa960 && code <= 0xa97c) || + // Hangul Syllables + (code >= 0xac00 && code <= 0xd7a3) || + // CJK Compatibility Ideographs + (code >= 0xf900 && code <= 0xfaff) || + // Vertical Forms + (code >= 0xfe10 && code <= 0xfe19) || + // CJK Compatibility Forms .. Small Form Variants + (code >= 0xfe30 && code <= 0xfe6b) || + // Halfwidth and Fullwidth Forms + (code >= 0xff01 && code <= 0xff60) || + (code >= 0xffe0 && code <= 0xffe6) || + // Kana Supplement + (code >= 0x1b000 && code <= 0x1b001) || + // Enclosed Ideographic Supplement + (code >= 0x1f200 && code <= 0x1f251) || + // Miscellaneous Symbols and Pictographs 0x1f300 - 0x1f5ff + // Emoticons 0x1f600 - 0x1f64f + (code >= 0x1f300 && code <= 0x1f64f) || + // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane + (code >= 0x20000 && code <= 0x3fffd)) + ); +} + +function renderRow(row, columnWidths, columnRightAlign) { + let out = tableChars.left; + for (let i = 0; i < row.length; i++) { + const cell = row[i]; + const len = getStringWidth(cell); + const padding = StringPrototypeRepeat(" ", columnWidths[i] - len); + if (columnRightAlign?.[i]) { + out += `${padding}${cell}`; + } else { + out += `${cell}${padding}`; + } + if (i !== row.length - 1) { + out += tableChars.middle; } } + out += tableChars.right; + return out; +} - return [baseString, refIndex]; +function cliTable(head, columns) { + const rows = []; + const columnWidths = ArrayPrototypeMap(head, (h) => getStringWidth(h)); + const longestColumn = ArrayPrototypeReduce( + columns, + (n, a) => MathMax(n, a.length), + 0, + ); + const columnRightAlign = new Array(columnWidths.length).fill(true); + + for (let i = 0; i < head.length; i++) { + const column = columns[i]; + for (let j = 0; j < longestColumn; j++) { + if (rows[j] === undefined) { + rows[j] = []; + } + const value = (rows[j][i] = hasOwnProperty(column, j) ? column[j] : ""); + const width = columnWidths[i] || 0; + const counted = getStringWidth(value); + columnWidths[i] = MathMax(width, counted); + columnRightAlign[i] &= NumberIsInteger(+value); + } + } + + const divider = ArrayPrototypeMap( + columnWidths, + (i) => StringPrototypeRepeat(tableChars.middleMiddle, i + 2), + ); + + let result = + `${tableChars.topLeft}${ + ArrayPrototypeJoin(divider, tableChars.topMiddle) + }` + + `${tableChars.topRight}\n${renderRow(head, columnWidths)}\n` + + `${tableChars.leftMiddle}${ + ArrayPrototypeJoin(divider, tableChars.rowMiddle) + }` + + `${tableChars.rightMiddle}\n`; + + for (let i = 0; i < rows.length; ++i) { + const row = rows[i]; + result += `${renderRow(row, columnWidths, columnRightAlign)}\n`; + } + + result += + `${tableChars.bottomLeft}${ + ArrayPrototypeJoin(divider, tableChars.bottomMiddle) + }` + + tableChars.bottomRight; + + return result; } +/* End of forked part */ -function inspectObject(value, inspectOptions, proxyDetails) { - if ( - ReflectHas(value, customInspect) && - typeof value[customInspect] === "function" - ) { - return String(value[customInspect](inspect, inspectOptions)); +// We can match Node's quoting behavior exactly by swapping the double quote and +// single quote in this array. That would give preference to single quotes. +// However, we prefer double quotes as the default. + +const denoInspectDefaultOptions = { + indentationLvl: 0, + currentDepth: 0, + stylize: stylizeNoColor, + + showHidden: false, + depth: 4, + colors: false, + showProxy: false, + breakLength: 80, + compact: 3, + sorted: false, + getters: false, + + // node only + maxArrayLength: 100, + maxStringLength: 100, // deno: strAbbreviateSize: 100 + customInspect: true, + + // deno only + /** You can override the quotes preference in inspectString. + * Used by util.inspect() */ + // TODO(kt3k): Consider using symbol as a key to hide this from the public + // API. + quotes: ['"', "'", "`"], + iterableLimit: 100, // similar to node's maxArrayLength, but doesn't only apply to arrays + trailingComma: false, + + inspect, + + // TODO(@crowlKats): merge into indentationLvl + indentLevel: 0, +}; + +function getDefaultInspectOptions() { + return { + budget: {}, + seen: [], + ...denoInspectDefaultOptions, + }; +} + +const DEFAULT_INDENT = " "; // Default indent string + +const STR_ABBREVIATE_SIZE = 100; + +class CSI { + static kClear = "\x1b[1;1H"; + static kClearScreenDown = "\x1b[0J"; +} + +const QUOTE_SYMBOL_REG = new SafeRegExp(/^[a-zA-Z_][a-zA-Z_.0-9]*$/); + +function maybeQuoteSymbol(symbol, ctx) { + const description = SymbolPrototypeGetDescription(symbol); + + if (description === undefined) { + return SymbolPrototypeToString(symbol); } - // This non-unique symbol is used to support op_crates, ie. - // in extensions/web we don't want to depend on public - // Symbol.for("Deno.customInspect") symbol defined in the public API. - // Internal only, shouldn't be used by users. - const privateCustomInspect = SymbolFor("Deno.privateCustomInspect"); - if ( - ReflectHas(value, privateCustomInspect) && - typeof value[privateCustomInspect] === "function" - ) { - // TODO(nayeemrmn): `inspect` is passed as an argument because custom - // inspect implementations in `extensions` need it, but may not have access - // to the `Deno` namespace in web workers. Remove when the `Deno` - // namespace is always enabled. - return String( - value[privateCustomInspect](inspect, inspectOptions), - ); + + if (RegExpPrototypeTest(QUOTE_SYMBOL_REG, description)) { + return SymbolPrototypeToString(symbol); } - if (ObjectPrototypeIsPrototypeOf(ErrorPrototype, value)) { - return inspectError(value, maybeColor(colors.cyan, inspectOptions)); - } else if (ArrayIsArray(value)) { - return inspectArray(value, inspectOptions); - } else if (ObjectPrototypeIsPrototypeOf(NumberPrototype, value)) { - return inspectNumberObject(value, inspectOptions); - } else if (ObjectPrototypeIsPrototypeOf(BigIntPrototype, value)) { - return inspectBigIntObject(value, inspectOptions); - } else if (ObjectPrototypeIsPrototypeOf(BooleanPrototype, value)) { - return inspectBooleanObject(value, inspectOptions); - } else if (ObjectPrototypeIsPrototypeOf(StringPrototype, value)) { - return inspectStringObject(value, inspectOptions); - } else if (ObjectPrototypeIsPrototypeOf(SymbolPrototype, value)) { - return inspectSymbolObject(value, inspectOptions); - } else if (ObjectPrototypeIsPrototypeOf(PromisePrototype, value)) { - return inspectPromise(value, inspectOptions); - } else if (ObjectPrototypeIsPrototypeOf(RegExpPrototype, value)) { - return inspectRegExp(value, inspectOptions); - } else if (ObjectPrototypeIsPrototypeOf(DatePrototype, value)) { - return inspectDate( - proxyDetails ? proxyDetails[0] : value, - inspectOptions, - ); - } else if (ObjectPrototypeIsPrototypeOf(SetPrototype, value)) { - return inspectSet( - proxyDetails ? proxyDetails[0] : value, - inspectOptions, - ); - } else if (ObjectPrototypeIsPrototypeOf(MapPrototype, value)) { - return inspectMap( - proxyDetails ? proxyDetails[0] : value, - inspectOptions, - ); - } else if (ObjectPrototypeIsPrototypeOf(WeakSetPrototype, value)) { - return inspectWeakSet(inspectOptions); - } else if (ObjectPrototypeIsPrototypeOf(WeakMapPrototype, value)) { - return inspectWeakMap(inspectOptions); - } else if (isTypedArray(value)) { - return inspectTypedArray( - ObjectGetPrototypeOf(value).constructor.name, - value, - inspectOptions, - ); - } else { - // Otherwise, default object formatting - let { 0: insp, 1: refIndex } = inspectRawObject(value, inspectOptions); - insp = refIndex + insp; - return insp; + + return `Symbol(${quoteString(description, ctx)})`; +} + +/** Surround the string in quotes. + * + * The quote symbol is chosen by taking the first of the `QUOTES` array which + * does not occur in the string. If they all occur, settle with `QUOTES[0]`. + * + * Insert a backslash before any occurrence of the chosen quote symbol and + * before any backslash. + */ +function quoteString(string, ctx) { + const quote = ArrayPrototypeFind( + ctx.quotes, + (c) => !StringPrototypeIncludes(string, c), + ) ?? + ctx.quotes[0]; + const escapePattern = new SafeRegExp(`(?=[${quote}\\\\])`, "g"); + string = StringPrototypeReplace(string, escapePattern, "\\"); + string = replaceEscapeSequences(string); + return `${quote}${string}${quote}`; +} + +const ESCAPE_PATTERN = new SafeRegExp(/([\b\f\n\r\t\v])/g); +const ESCAPE_MAP = ObjectFreeze({ + "\b": "\\b", + "\f": "\\f", + "\n": "\\n", + "\r": "\\r", + "\t": "\\t", + "\v": "\\v", +}); + +const ESCAPE_PATTERN2 = new SafeRegExp("[\x00-\x1f\x7f-\x9f]", "g"); + +// Replace escape sequences that can modify output. +function replaceEscapeSequences(string) { + return StringPrototypeReplace( + StringPrototypeReplace( + string, + ESCAPE_PATTERN, + (c) => ESCAPE_MAP[c], + ), + new SafeRegExp(ESCAPE_PATTERN2), + (c) => + "\\x" + + StringPrototypePadStart( + NumberPrototypeToString(StringPrototypeCharCodeAt(c, 0), 16), + 2, + "0", + ), + ); +} + +// Print strings when they are inside of arrays or objects with quotes +function inspectValueWithQuotes( + value, + ctx, +) { + const abbreviateSize = typeof ctx.strAbbreviateSize === "undefined" + ? STR_ABBREVIATE_SIZE + : ctx.strAbbreviateSize; + switch (typeof value) { + case "string": { + const trunc = value.length > abbreviateSize + ? StringPrototypeSlice(value, 0, abbreviateSize) + "..." + : value; + return ctx.stylize(quoteString(trunc, ctx), "string"); // Quoted strings are green + } + default: + return formatValue(ctx, value, 0); } } @@ -1890,10 +3017,21 @@ function cssToAnsi(css, prevCss = null) { } function inspectArgs(args, inspectOptions = {}) { - circular = undefined; + const ctx = { + ...getDefaultInspectOptions(), + ...inspectOptions, + }; + if (inspectOptions.iterableLimit !== undefined) { + ctx.maxArrayLength = inspectOptions.iterableLimit; + } + if (inspectOptions.strAbbreviateSize !== undefined) { + ctx.maxStringLength = inspectOptions.strAbbreviateSize; + } + if (ctx.colors) ctx.stylize = createStylizeWithColor(styles, colors); + if (ctx.maxArrayLength === null) ctx.maxArrayLength = Infinity; + if (ctx.maxStringLength === null) ctx.maxStringLength = Infinity; - const noColor = colors.getNoColor(); - const rInspectOptions = { ...DEFAULT_INSPECT_OPTIONS, ...inspectOptions }; + const noColor = colors_.getNoColor(); const first = args[0]; let a = 0; let string = ""; @@ -1933,7 +3071,7 @@ function inspectArgs(args, inspectOptions = {}) { } } else if (ArrayPrototypeIncludes(["O", "o"], char)) { // Format as an object. - formattedArg = inspectValue(args[a++], rInspectOptions); + formattedArg = formatValue(ctx, args[a++], 0); } else if (char == "c") { const value = args[a++]; if (!noColor) { @@ -1974,14 +3112,14 @@ function inspectArgs(args, inspectOptions = {}) { string += args[a]; } else { // Use default maximum depth for null or undefined arguments. - string += inspectValue(args[a], rInspectOptions); + string += formatValue(ctx, args[a], 0); } } - if (rInspectOptions.indentLevel > 0) { + if (ctx.indentLevel > 0) { const groupIndent = StringPrototypeRepeat( DEFAULT_INDENT, - rInspectOptions.indentLevel, + ctx.indentLevel, ); string = groupIndent + StringPrototypeReplaceAll(string, "\n", `\n${groupIndent}`); @@ -1990,14 +3128,29 @@ function inspectArgs(args, inspectOptions = {}) { return string; } +function createStylizeWithColor(styles, colors) { + return function stylizeWithColor(str, styleType) { + const style = styles[styleType]; + if (style !== undefined) { + const color = colors[style]; + if (color !== undefined) { + return `\u001b[${color[0]}m${str}\u001b[${color[1]}m`; + } + } + return str; + }; +} + const countMap = new SafeMap(); const timerMap = new SafeMap(); const isConsoleInstance = Symbol("isConsoleInstance"); function getConsoleInspectOptions() { + const color = !colors_.getNoColor(); return { - ...DEFAULT_INSPECT_OPTIONS, - colors: !colors.getNoColor(), + ...getDefaultInspectOptions(), + colors: color, + stylize: color ? createStylizeWithColor(styles, colors) : stylizeNoColor, }; } @@ -2146,8 +3299,9 @@ class Console { const stringifyValue = (value) => inspectValueWithQuotes(value, { - ...DEFAULT_INSPECT_OPTIONS, + ...getDefaultInspectOptions(), depth: 1, + compact: true, }); const toTable = (header, body) => this.log(cliTable(header, body)); @@ -2318,18 +3472,34 @@ function inspect( value, inspectOptions = {}, ) { - circular = undefined; - return inspectValue(value, { - ...DEFAULT_INSPECT_OPTIONS, + // Default options + const ctx = { + ...getDefaultInspectOptions(), ...inspectOptions, - }); + }; + if (inspectOptions.iterableLimit !== undefined) { + ctx.maxArrayLength = inspectOptions.iterableLimit; + } + if (inspectOptions.strAbbreviateSize !== undefined) { + ctx.maxStringLength = inspectOptions.strAbbreviateSize; + } + + if (ctx.colors) ctx.stylize = createStylizeWithColor(styles, colors); + if (ctx.maxArrayLength === null) ctx.maxArrayLength = Infinity; + if (ctx.maxStringLength === null) ctx.maxStringLength = Infinity; + return formatValue(ctx, value, 0); } /** Creates a proxy that represents a subset of the properties * of the original object optionally without evaluating the properties * in order to get the values. */ function createFilteredInspectProxy({ object, keys, evaluate }) { - return new Proxy({}, { + const obj = class {}; + if (object.constructor?.name) { + ObjectDefineProperty(obj, "name", { value: object.constructor.name }); + } + + return new Proxy(new obj(), { get(_target, key) { if (key === SymbolToStringTag) { return object.constructor?.name; @@ -2417,12 +3587,19 @@ internals.parseCss = parseCss; internals.parseCssColor = parseCssColor; export { + colors, Console, createFilteredInspectProxy, + createStylizeWithColor, CSI, customInspect, + formatBigInt, + formatNumber, + formatValue, + getDefaultInspectOptions, inspect, inspectArgs, quoteString, + styles, wrapConsole, }; |