From 7f19662607d0aae7a43a3e4de79d591889c9b659 Mon Sep 17 00:00:00 2001 From: Jan Vennemann Date: Wed, 21 Aug 2019 21:53:49 +0200 Subject: [PATCH] feat(node): node 12 compatible util module and improved console --- .../ti.internal/extensions/js/console.js | 29 + .../ti.internal/extensions/js/index.js | 1 + .../extensions/node/internal/assert.js | 26 + .../extensions/node/internal/errors.js | 205 ++ .../extensions/node/internal/util.js | 37 + .../extensions/node/internal/util/inspect.js | 1644 +++++++++++++++++ .../extensions/node/internal/util/types.js | 206 +++ .../ti.internal/extensions/node/util.js | 439 +---- tests/Resources/util.test.js | 1539 +++++++++++++++ 9 files changed, 3700 insertions(+), 426 deletions(-) create mode 100644 common/Resources/ti.internal/extensions/js/console.js create mode 100644 common/Resources/ti.internal/extensions/node/internal/assert.js create mode 100644 common/Resources/ti.internal/extensions/node/internal/errors.js create mode 100644 common/Resources/ti.internal/extensions/node/internal/util/inspect.js create mode 100644 common/Resources/ti.internal/extensions/node/internal/util/types.js create mode 100644 tests/Resources/util.test.js diff --git a/common/Resources/ti.internal/extensions/js/console.js b/common/Resources/ti.internal/extensions/js/console.js new file mode 100644 index 00000000000..c612473c235 --- /dev/null +++ b/common/Resources/ti.internal/extensions/js/console.js @@ -0,0 +1,29 @@ +import { formatWithOptions } from '../node/internal/util/inspect'; + +const nativeDebug = console.debug; +const nativeError = console.error; +const nativeInfo = console.info; +const nativeLog = console.log; +const nativeWarn = console.warn; + +const kNoColorInspectOptions = {}; + +console.debug = function (...args) { + nativeDebug.call(console, formatWithOptions(kNoColorInspectOptions, ...args)); +}; + +console.error = function (...args) { + nativeError.call(console, formatWithOptions(kNoColorInspectOptions, ...args)); +}; + +console.info = function (...args) { + nativeInfo.call(console, formatWithOptions(kNoColorInspectOptions, ...args)); +}; + +console.log = function (...args) { + nativeLog.call(console, formatWithOptions(kNoColorInspectOptions, ...args)); +}; + +console.warn = function (...args) { + nativeWarn.call(console, formatWithOptions(kNoColorInspectOptions, ...args)); +}; diff --git a/common/Resources/ti.internal/extensions/js/index.js b/common/Resources/ti.internal/extensions/js/index.js index ba1bbeeacbf..3f82d61e1e6 100644 --- a/common/Resources/ti.internal/extensions/js/index.js +++ b/common/Resources/ti.internal/extensions/js/index.js @@ -1,2 +1,3 @@ // Load all JavaScript extensions/polyfills +import './console'; import './Error'; diff --git a/common/Resources/ti.internal/extensions/node/internal/assert.js b/common/Resources/ti.internal/extensions/node/internal/assert.js new file mode 100644 index 00000000000..f829b83d810 --- /dev/null +++ b/common/Resources/ti.internal/extensions/node/internal/assert.js @@ -0,0 +1,26 @@ +import { codes } from './errors'; + +let error; +function lazyError() { + if (!error) { + // @fixme rollup cannot handle lazy loaded modules, maybe move to webpack? + // error = require('./errors').codes.ERR_INTERNAL_ASSERTION; + error = codes.ERR_INTERNAL_ASSERTION; + } + return error; +} +function assert(value, message) { + if (!value) { + const ERR_INTERNAL_ASSERTION = lazyError(); + throw new ERR_INTERNAL_ASSERTION(message); + } +} + +function fail(message) { + const ERR_INTERNAL_ASSERTION = lazyError(); + throw new ERR_INTERNAL_ASSERTION(message); +} + +assert.fail = fail; + +export default assert; diff --git a/common/Resources/ti.internal/extensions/node/internal/errors.js b/common/Resources/ti.internal/extensions/node/internal/errors.js new file mode 100644 index 00000000000..9121bda9bd4 --- /dev/null +++ b/common/Resources/ti.internal/extensions/node/internal/errors.js @@ -0,0 +1,205 @@ +/** + * Node's internal/errors module modified for Axway Titanium + * + * Only a few selected errors are exported manually here. Most of the functionality + * is still missing and may be added as we move forward with Node compatibility. + * + * @see https://github.com/nodejs/node/blob/master/lib/internal/errors.js + */ + +import assert from './assert'; +import { format } from './util/inspect'; + +const messages = new Map(); +export const codes = {}; + +// @todo implement this once needed +class SystemError extends Error { + +} + +// Utility function for registering the error codes. +function E(sym, val, def, ...otherClasses) { + // Special case for SystemError that formats the error message differently + // The SystemErrors only have SystemError as their base classes. + messages.set(sym, val); + + if (def === SystemError) { + throw new Error('Node compatible SystemError not yet implemented.'); + } else { + def = makeNodeErrorWithCode(def, sym); + } + + if (otherClasses.length !== 0) { + otherClasses.forEach((clazz) => { + def[clazz.name] = makeNodeErrorWithCode(clazz, sym); + }); + } + codes[sym] = def; +} + +function makeNodeErrorWithCode(Base, key) { + return class NodeError extends Base { + constructor(...args) { + super(); + const message = getMessage(key, args, this); + Object.defineProperty(this, 'message', { + value: message, + enumerable: false, + writable: true, + configurable: true + }); + addCodeToName(this, super.name, key); + } + + get code() { + return key; + } + + set code(value) { + Object.defineProperty(this, 'code', { + configurable: true, + enumerable: true, + value, + writable: true + }); + } + + toString() { + return `${this.name} [${key}]: ${this.message}`; + } + }; +} + +function getMessage(key, args, self) { + const msg = messages.get(key); + + /* + // @fixme rollup cannot handle lazy loaded modules, maybe move to webpack? + if (assert === undefined) { + assert = require('./internal/assert'); + } + */ + + if (typeof msg === 'function') { + assert( + msg.length <= args.length, // Default options do not count. + `Code: ${key}; The provided arguments length (${args.length}) does not ` + + `match the required ones (${msg.length}).` + ); + return msg.apply(self, args); + } + + const expectedLength = (msg.match(/%[dfijoOs]/g) || []).length; + assert( + expectedLength === args.length, + `Code: ${key}; The provided arguments length (${args.length}) does not ` + + `match the required ones (${expectedLength}).` + ); + if (args.length === 0) { + return msg; + } + + args.unshift(msg); + return format.apply(null, args); + // @fixme rollup cannot handle lazy loaded modules, maybe move to webpack? + // return lazyInternalUtilInspect().format.apply(null, args); +} + +function addCodeToName(err, name, code) { + // Add the error code to the name to include it in the stack trace. + err.name = `${name} [${code}]`; + // Access the stack to generate the error message including the error code + // from the name. + // @fixme: This only works on V8/Android, iOS/JSC has a different Error structure. + // should we try to make errors behave the same across platforms? + err.stack; + // Reset the name to the actual name. + if (name === 'SystemError') { + Object.defineProperty(err, 'name', { + value: name, + enumerable: false, + writable: true, + configurable: true + }); + } else { + delete err.name; + } +} + +E('ERR_INTERNAL_ASSERTION', (message) => { + const suffix = 'This is caused by either a bug in Titanium ' + + 'or incorrect usage of Titanium internals.\n' + + 'Please open an issue with this stack trace at ' + + 'https://jira.appcelerator.org\n'; + return message === undefined ? suffix : `${message}\n${suffix}`; +}, Error); +E('ERR_INVALID_ARG_TYPE', (name, expected, actual) => { + assert(typeof name === 'string', '\'name\' must be a string'); + + // determiner: 'must be' or 'must not be' + let determiner; + if (typeof expected === 'string' && expected.startsWith('not ')) { + determiner = 'must not be'; + expected = expected.replace(/^not /, ''); + } else { + determiner = 'must be'; + } + + let msg; + if (name.endsWith(' argument')) { + // For cases like 'first argument' + msg = `The ${name} ${determiner} ${oneOf(expected, 'type')}`; + } else { + const type = name.includes('.') ? 'property' : 'argument'; + msg = `The "${name}" ${type} ${determiner} ${oneOf(expected, 'type')}`; + } + + // TODO(BridgeAR): Improve the output by showing `null` and similar. + msg += `. Received type ${typeof actual}`; + return msg; +}, TypeError); + +let maxStack_ErrorName; +let maxStack_ErrorMessage; +/** + * Returns true if `err.name` and `err.message` are equal to engine-specific + * values indicating max call stack size has been exceeded. + * "Maximum call stack size exceeded" in V8. + * + * @param {Error} err + * @returns {boolean} + */ +export function isStackOverflowError(err) { + if (maxStack_ErrorMessage === undefined) { + try { + function overflowStack() { + overflowStack(); + } + overflowStack(); + } catch (e) { + maxStack_ErrorMessage = e.message; + maxStack_ErrorName = e.name; + } + } + + return err.name === maxStack_ErrorName && err.message === maxStack_ErrorMessage; +} + +function oneOf(expected, thing) { + assert(typeof thing === 'string', '`thing` has to be of type string'); + if (Array.isArray(expected)) { + const len = expected.length; + assert(len > 0, 'At least one expected value needs to be specified'); + expected = expected.map((i) => String(i)); + if (len > 2) { + return `one of ${thing} ${expected.slice(0, len - 1).join(', ')}, or ` + expected[len - 1]; + } else if (len === 2) { + return `one of ${thing} ${expected[0]} or ${expected[1]}`; + } else { + return `of ${thing} ${expected[0]}`; + } + } else { + return `of ${thing} ${String(expected)}`; + } +} diff --git a/common/Resources/ti.internal/extensions/node/internal/util.js b/common/Resources/ti.internal/extensions/node/internal/util.js index f26635a8680..e87cf5cd9a2 100644 --- a/common/Resources/ti.internal/extensions/node/internal/util.js +++ b/common/Resources/ti.internal/extensions/node/internal/util.js @@ -1,5 +1,22 @@ +import { isNativeError } from './util/types'; + const kNodeModulesRE = /^(.*)[\\/]node_modules[\\/]/; +export const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom'); + +const colorRegExp = /\u001b\[\d\d?m/g; // eslint-disable-line no-control-regex + +export function removeColors(str) { + return str.replace(colorRegExp, ''); +} + +export function isError(e) { + // An error could be an instance of Error while not being a native error + // or could be from a different realm and not be instance of Error but still + // be a native error. + return isNativeError(e) || e instanceof Error; +} + let getStructuredStack; class StackTraceError extends Error { } StackTraceError.prepareStackTrace = (err, trace) => trace; @@ -42,3 +59,23 @@ export function isInsideNodeModules() { return false; } + +export function join(output, separator) { + let str = ''; + if (output.length !== 0) { + const lastIndex = output.length - 1; + for (let i = 0; i < lastIndex; i++) { + // It is faster not to use a template string here + str += output[i]; + str += separator; + } + str += output[lastIndex]; + } + return str; +} + +export function uncurryThis(f) { + return function () { + return f.call.apply(f, arguments); + }; +} diff --git a/common/Resources/ti.internal/extensions/node/internal/util/inspect.js b/common/Resources/ti.internal/extensions/node/internal/util/inspect.js new file mode 100644 index 00000000000..c238d4cba16 --- /dev/null +++ b/common/Resources/ti.internal/extensions/node/internal/util/inspect.js @@ -0,0 +1,1644 @@ +/** + * Internal Node.js util/inspect module modified for Axway Titanium + * + * @see https://github.com/nodejs/node/blob/master/lib/internal/util/inspect.js + */ + +import { + isAsyncFunction, + isGeneratorFunction, + isAnyArrayBuffer, + isArrayBuffer, + isArgumentsObject, + isBoxedPrimitive, + isDataView, + isMap, + isMapIterator, + isPromise, + isSet, + isSetIterator, + isWeakMap, + isWeakSet, + isRegExp, + isDate, + isTypedArray, + isStringObject, + isNumberObject, + isBooleanObject, + isUint8Array, + isUint8ClampedArray, + isUint16Array, + isUint32Array, + isInt8Array, + isInt16Array, + isInt32Array, + isFloat32Array, + isFloat64Array +} from './types'; +import { codes, isStackOverflowError } from '../errors'; +import { + customInspectSymbol, + isError, + join, + removeColors, + uncurryThis +} from '../util'; + +const BooleanPrototype = Boolean.prototype; +const DatePrototype = Date.prototype; +const ErrorPrototype = Error.prototype; +const NumberPrototype = Number.prototype; +const MapPrototype = Map.prototype; +const RegExpPrototype = RegExp.prototype; +const StringPrototype = String.prototype; +const SetPrototype = Set.prototype; +const SymbolPrototype = Symbol.prototype; + +const isIos = [ 'ipad', 'iphone' ].includes(Ti.Platform.osname); + +const { ERR_INVALID_ARG_TYPE } = codes; + +const hasOwnProperty = uncurryThis(Object.prototype.hasOwnProperty); +const propertyIsEnumerable = uncurryThis(Object.prototype.propertyIsEnumerable); + +import Buffer from '../../buffer'; +let hexSlice = uncurryThis(Buffer.Buffer.prototype.hexSlice); + +const builtInObjects = new Set( + Object.getOwnPropertyNames(global).filter((e) => /^([A-Z][a-z]+)+$/.test(e)) +); + +export const inspectDefaultOptions = Object.seal({ + showHidden: false, + depth: 2, + colors: false, + customInspect: true, + showProxy: false, + maxArrayLength: 100, + breakLength: 80, + compact: 3, + sorted: false, + getters: false +}); + +const kObjectType = 0; +const kArrayType = 1; +const kArrayExtrasType = 2; + +/* eslint-disable no-control-regex */ +const strEscapeSequencesRegExp = /[\x00-\x1f\x27\x5c]/; +const strEscapeSequencesReplacer = /[\x00-\x1f\x27\x5c]/g; +const strEscapeSequencesRegExpSingle = /[\x00-\x1f\x5c]/; +const strEscapeSequencesReplacerSingle = /[\x00-\x1f\x5c]/g; +/* eslint-enable no-control-regex */ + +const keyStrRegExp = /^[a-zA-Z_][a-zA-Z_0-9]*$/; +const numberRegExp = /^(0|[1-9][0-9]*)$/; + +const nodeModulesRegExp = /[/\\]node_modules[/\\](.+?)(?=[/\\])/g; + +const kMinLineLength = 16; + +// Constants to map the iterator state. +const kWeak = 0; +const kIterator = 1; +const kMapEntries = 2; + +// Escaped special characters. Use empty strings to fill up unused entries. +/* eslint-disable quotes */ +const meta = [ + '\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004', + '\\u0005', '\\u0006', '\\u0007', '\\b', '\\t', + '\\n', '\\u000b', '\\f', '\\r', '\\u000e', + '\\u000f', '\\u0010', '\\u0011', '\\u0012', '\\u0013', + '\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018', + '\\u0019', '\\u001a', '\\u001b', '\\u001c', '\\u001d', + '\\u001e', '\\u001f', '', '', '', + '', '', '', '', "\\'", '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '\\\\' +]; +/* eslint-enable quotes */ + +function getUserOptions(ctx) { + const obj = { stylize: ctx.stylize }; + for (const key of Object.keys(inspectDefaultOptions)) { + obj[key] = ctx[key]; + } + if (ctx.userOptions === undefined) { + return obj; + } + return { ...obj, ...ctx.userOptions }; +} + +/** + * Echos the value of any input. Tries to print the value out + * in the best way possible given the different types. + * + * @param {any} value The value to print out. + * @param {Object} opts Optional options object that alters the output. + * @return {string} The string representation of `value` + */ +export function inspect(value, opts) { + // Default options + const ctx = { + budget: {}, + indentationLvl: 0, + seen: [], + currentDepth: 0, + stylize: stylizeNoColor, + showHidden: inspectDefaultOptions.showHidden, + depth: inspectDefaultOptions.depth, + colors: inspectDefaultOptions.colors, + customInspect: inspectDefaultOptions.customInspect, + showProxy: inspectDefaultOptions.showProxy, + maxArrayLength: inspectDefaultOptions.maxArrayLength, + breakLength: inspectDefaultOptions.breakLength, + compact: inspectDefaultOptions.compact, + sorted: inspectDefaultOptions.sorted, + getters: inspectDefaultOptions.getters + }; + if (arguments.length > 1) { + // Legacy... + if (arguments.length > 2) { + if (arguments[2] !== undefined) { + ctx.depth = arguments[2]; + } + if (arguments.length > 3 && arguments[3] !== undefined) { + ctx.colors = arguments[3]; + } + } + // Set user-specified options + if (typeof opts === 'boolean') { + ctx.showHidden = opts; + } else if (opts) { + const optKeys = Object.keys(opts); + for (const key of optKeys) { + // TODO(BridgeAR): Find a solution what to do about stylize. Either make + // this function public or add a new API with a similar or better + // functionality. + if (hasOwnProperty(inspectDefaultOptions, key) || key === 'stylize') { + ctx[key] = opts[key]; + } else if (ctx.userOptions === undefined) { + // This is required to pass through the actual user input. + ctx.userOptions = opts; + } + } + } + } + if (ctx.colors) { + console.warn('The "colors" option for util.inspect is not supported on Titanium.'); + } + if (ctx.maxArrayLength === null) { + ctx.maxArrayLength = Infinity; + } + return formatValue(ctx, value, 0); +} +inspect.custom = customInspectSymbol; + +Object.defineProperty(inspect, 'defaultOptions', { + get() { + return inspectDefaultOptions; + }, + set(options) { + if (options === null || typeof options !== 'object') { + throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); + } + return Object.assign(inspectDefaultOptions, options); + } +}); + +function addQuotes(str, quotes) { + if (quotes === -1) { + return `"${str}"`; + } + if (quotes === -2) { + return `\`${str}\``; + } + return `'${str}'`; +} + +const escapeFn = (str) => meta[str.charCodeAt(0)]; + +// Escape control characters, single quotes and the backslash. +// This is similar to JSON stringify escaping. +function strEscape(str) { + let escapeTest = strEscapeSequencesRegExp; + let escapeReplace = strEscapeSequencesReplacer; + let singleQuote = 39; + + // Check for double quotes. If not present, do not escape single quotes and + // instead wrap the text in double quotes. If double quotes exist, check for + // backticks. If they do not exist, use those as fallback instead of the + // double quotes. + // eslint-disable-next-line quotes + if (str.includes("'")) { + // This invalidates the charCode and therefore can not be matched for + // anymore. + if (!str.includes('"')) { + singleQuote = -1; + } else if (!str.includes('`') && !str.includes('${')) { + singleQuote = -2; + } + if (singleQuote !== 39) { + escapeTest = strEscapeSequencesRegExpSingle; + escapeReplace = strEscapeSequencesReplacerSingle; + } + } + + // Some magic numbers that worked out fine while benchmarking with v8 6.0 + if (str.length < 5000 && !escapeTest.test(str)) { + return addQuotes(str, singleQuote); + } + if (str.length > 100) { + str = str.replace(escapeReplace, escapeFn); + return addQuotes(str, singleQuote); + } + + let result = ''; + let last = 0; + const lastIndex = str.length; + for (let i = 0; i < lastIndex; i++) { + const point = str.charCodeAt(i); + if (point === singleQuote || point === 92 || point < 32) { + if (last === i) { + result += meta[point]; + } else { + result += `${str.slice(last, i)}${meta[point]}`; + } + last = i + 1; + } + } + + if (last !== lastIndex) { + result += str.slice(last); + } + return addQuotes(result, singleQuote); +} + +function stylizeNoColor(str) { + return str; +} + +// Return a new empty array to push in the results of the default formatter. +function getEmptyFormatArray() { + return []; +} + +function getConstructorName(obj, ctx) { + let firstProto; + const tmp = obj; + while (obj) { + const descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor'); + if (descriptor !== undefined + && typeof descriptor.value === 'function' + && descriptor.value.name !== '') { + return descriptor.value.name; + } + + obj = Object.getPrototypeOf(obj); + if (firstProto === undefined) { + firstProto = obj; + } + } + + if (firstProto === null) { + return null; + } + + /* + @todo this calls into native, can we replace this somehow? + return `${internalGetConstructorName(tmp)} <${inspect(firstProto, { + ...ctx, + customInspect: false + })}>`; + */ + + return null; +} + +function getPrefix(constructor, tag, fallback) { + if (constructor === null) { + if (tag !== '') { + return `[${fallback}: null prototype] [${tag}] `; + } + return `[${fallback}: null prototype] `; + } + + if (tag !== '' && constructor !== tag) { + return `${constructor} [${tag}] `; + } + return `${constructor} `; +} + +// Look up the keys of the object. +function getKeys(value, showHidden) { + let keys; + const symbols = Object.getOwnPropertySymbols(value); + if (showHidden) { + keys = Object.getOwnPropertyNames(value); + if (symbols.length !== 0) { + keys.push(...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 = Object.keys(value); + } catch (err) { + // @fixme how to du isModuleNamespaceObject? + /* + assert(isNativeError(err) && err.name === 'ReferenceError' && + isModuleNamespaceObject(value)); + */ + keys = Object.getOwnPropertyNames(value); + } + if (symbols.length !== 0) { + keys.push(...symbols.filter((key) => propertyIsEnumerable(value, key))); + } + } + return keys; +} + +function getCtxStyle(value, constructor, tag) { + let fallback = ''; + if (constructor === null) { + fallback = 'Object'; + } + return getPrefix(constructor, tag, fallback); +} + +function findTypedConstructor(value) { + for (const [ check, clazz ] of [ + [ isUint8Array, Uint8Array ], + [ isUint8ClampedArray, Uint8ClampedArray ], + [ isUint16Array, Uint16Array ], + [ isUint32Array, Uint32Array ], + [ isInt8Array, Int8Array ], + [ isInt16Array, Int16Array ], + [ isInt32Array, Int32Array ], + [ isFloat32Array, Float32Array ], + [ isFloat64Array, Float64Array ] + ]) { + if (check(value)) { + return clazz; + } + } +} + +let lazyNullPrototypeCache; +// Creates a subclass and name +// the constructor as `${clazz} : null prototype` +function clazzWithNullPrototype(clazz, name) { + if (lazyNullPrototypeCache === undefined) { + lazyNullPrototypeCache = new Map(); + } else { + const cachedClass = lazyNullPrototypeCache.get(clazz); + if (cachedClass !== undefined) { + return cachedClass; + } + } + class NullPrototype extends clazz { + get [Symbol.toStringTag]() { + return ''; + } + } + Object.defineProperty( + NullPrototype.prototype.constructor, + 'name', + { value: `[${name}: null prototype]` } + ); + lazyNullPrototypeCache.set(clazz, NullPrototype); + return NullPrototype; +} + +function noPrototypeIterator(ctx, value, recurseTimes) { + let newVal; + if (isSet(value)) { + const clazz = clazzWithNullPrototype(Set, 'Set'); + newVal = new clazz(SetPrototype.values(value)); + } else if (isMap(value)) { + const clazz = clazzWithNullPrototype(Map, 'Map'); + newVal = new clazz(MapPrototype.entries(value)); + } else if (Array.isArray(value)) { + const clazz = clazzWithNullPrototype(Array, 'Array'); + newVal = new clazz(value.length); + } else if (isTypedArray(value)) { + const constructor = findTypedConstructor(value); + const clazz = clazzWithNullPrototype(constructor, constructor.name); + newVal = new clazz(value); + } + if (newVal !== undefined) { + Object.defineProperties(newVal, Object.getOwnPropertyDescriptors(value)); + return formatRaw(ctx, newVal, recurseTimes); + } +} + +function isAllDigits(s) { + if (s.length === 0) { + return false; + } + for (var i = 0; i < s.length; ++i) { + const code = s.charCodeAt(i); + if (code < 48 || code > 57) { + return false; + } + } + return true; +} + +function getOwnNonIndexProperties(obj, filter) { + const props = []; + const keys = filter === ONLY_ENUMERABLE ? Object.keys(obj) : Object.getOwnPropertyNames(obj); + for (var i = 0; i < keys.length; ++i) { + const key = keys[i]; + if (!isAllDigits(key)) { + props.push(key); + } + } + return props; +} + +const ALL_PROPERTIES = 0; +const ONLY_ENUMERABLE = 2; + +function formatValue(ctx, value, recurseTimes, typedArray) { + // Primitive types cannot have properties. + if (typeof value !== 'object' && typeof value !== 'function') { + 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; + /* + @fixme check for proxies + // Always check for proxies to prevent side effects and to prevent triggering + // any proxy handlers. + const proxy = getProxyDetails(value); + if (proxy !== undefined) { + if (ctx.showProxy) { + return formatProxy(ctx, proxy, recurseTimes); + } + value = proxy[0]; + } + */ + // Provide a hook for user-specified inspect functions. + // Check that value is an object with an inspect function on it. + if (ctx.customInspect) { + const maybeCustom = value[customInspectSymbol]; + if (typeof maybeCustom === 'function' + // Filter out the util module, its inspect function is special. + && maybeCustom !== 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; + const ret = maybeCustom.call(context, depth, getUserOptions(ctx)); + // 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 ret.replace(/\n/g, `\n${' '.repeat(ctx.indentationLvl)}`); + } + } + } + // 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 Map([ [ 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'); + } + return formatRaw(ctx, value, recurseTimes, typedArray); +} + +function formatRaw(ctx, value, recurseTimes, typedArray) { + let keys; + + const constructor = getConstructorName(value, ctx); + let tag = value[Symbol.toStringTag]; + // Only list the tag in case it's non-enumerable / not an own property. + // Otherwise we'd print this twice. + if (typeof tag !== 'string' + || tag !== '' + && (ctx.showHidden ? hasOwnProperty : propertyIsEnumerable)( + value, Symbol.toStringTag + )) { + tag = ''; + } + let base = ''; + let formatter = getEmptyFormatArray; + let braces; + let noIterator = true; + let i = 0; + const filter = ctx.showHidden ? ALL_PROPERTIES : ONLY_ENUMERABLE; + + let extrasType = kObjectType; + + // Iterators and the rest are split to reduce checks. + if (value[Symbol.iterator]) { + noIterator = false; + if (Array.isArray(value)) { + keys = getOwnNonIndexProperties(value, filter); + // Only set the constructor for non ordinary ("Array [...]") arrays. + const prefix = getPrefix(constructor, tag, 'Array'); + braces = [ `${prefix === 'Array ' ? '' : prefix}[`, ']' ]; + if (value.length === 0 && keys.length === 0) { + return `${braces[0]}]`; + } + extrasType = kArrayExtrasType; + formatter = formatArray; + } else if (isSet(value)) { + keys = getKeys(value, ctx.showHidden); + const prefix = getPrefix(constructor, tag, 'Set'); + if (value.size === 0 && keys.length === 0) { + return `${prefix}{}`; + } + braces = [ `${prefix}{`, '}' ]; + formatter = formatSet; + } else if (isMap(value)) { + keys = getKeys(value, ctx.showHidden); + const prefix = getPrefix(constructor, tag, 'Map'); + if (value.size === 0 && keys.length === 0) { + return `${prefix}{}`; + } + braces = [ `${prefix}{`, '}' ]; + formatter = formatMap; + } else if (isTypedArray(value)) { + keys = getOwnNonIndexProperties(value, filter); + const prefix = constructor !== null + ? getPrefix(constructor, tag) + : getPrefix(constructor, tag, findTypedConstructor(value).name); + braces = [ `${prefix}[`, ']' ]; + if (value.length === 0 && keys.length === 0 && !ctx.showHidden) { + return `${braces[0]}]`; + } + formatter = formatTypedArray; + extrasType = kArrayExtrasType; + } else if (isMapIterator(value)) { + keys = getKeys(value, ctx.showHidden); + braces = getIteratorBraces('Map', tag); + formatter = formatIterator; + } else if (isSetIterator(value)) { + keys = getKeys(value, ctx.showHidden); + braces = getIteratorBraces('Set', tag); + formatter = formatIterator; + } else { + noIterator = true; + } + } + + 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) { + return `${braces[0]}}`; + } + } else if (typeof value === 'function') { + base = getFunctionBase(value, constructor, tag); + if (keys.length === 0) { + return ctx.stylize(base, 'special'); + } + } else if (isRegExp(value)) { + // Make RegExps say that they are RegExps + const regExp = constructor !== null ? value : new RegExp(value); + base = RegExpPrototype.toString.call(regExp); + const prefix = getPrefix(constructor, tag, 'RegExp'); + if (prefix !== 'RegExp ') { + base = `${prefix}${base}`; + } + if (keys.length === 0 || recurseTimes > ctx.depth && ctx.depth !== null) { + return ctx.stylize(base, 'regexp'); + } + } else if (isDate(value)) { + // Make dates with properties first say the date + base = Number.isNaN(DatePrototype.getTime.call(value)) + ? DatePrototype.toString.call(value) + : DatePrototype.toISOString.call(value); + const prefix = getPrefix(constructor, tag, 'Date'); + if (prefix !== 'Date ') { + base = `${prefix}${base}`; + } + if (keys.length === 0) { + return ctx.stylize(base, 'date'); + } + } else if (isError(value)) { + base = formatError(value, constructor, tag, ctx); + if (keys.length === 0) { + return base; + } else if (isIos) { + const nativeErrorProps = [ 'line', 'column', 'sourceURL' ]; + if (keys.every(key => nativeErrorProps.includes(key))) { + 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) { + return `${prefix}{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`; + } + braces[0] = `${prefix}{`; + keys.unshift('byteLength'); + } else if (isDataView(value)) { + braces[0] = `${getPrefix(constructor, tag, 'DataView')}{`; + // .buffer goes last, it's not a primitive like the others. + keys.unshift('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; + /* + * @fixme how to do isModuleNamespaceObject? + } else if (isModuleNamespaceObject(value)) { + braces[0] = `[${tag}] {`; + formatter = formatNamespaceObject; + */ + } else if (isBoxedPrimitive(value)) { + base = getBoxedBase(value, ctx, keys, constructor, tag); + if (keys.length === 0) { + return base; + } + } else { + // The input prototype got manipulated. Special handle these. We have to + // rebuild the information so we are able to display everything. + if (constructor === null) { + const specialIterator = noPrototypeIterator(ctx, value, recurseTimes); + if (specialIterator) { + return specialIterator; + } + } + if (isMapIterator(value)) { + braces = getIteratorBraces('Map', tag); + formatter = formatIterator; + } else if (isSetIterator(value)) { + braces = getIteratorBraces('Set', tag); + formatter = formatIterator; + // Handle other regular objects again. + } else { + if (keys.length === 0) { + return `${getCtxStyle(value, constructor, tag)}{}`; + } + braces[0] = `${getCtxStyle(value, constructor, tag)}{`; + } + } + } + if (recurseTimes > ctx.depth && ctx.depth !== null) { + let constructorName = getCtxStyle(value, constructor, tag).slice(0, -1); + if (constructor !== null) { + constructorName = `[${constructorName}]`; + } + return ctx.stylize(constructorName, 'special'); + } + recurseTimes += 1; + ctx.seen.push(value); + ctx.currentDepth = recurseTimes; + let output; + const indentationLvl = ctx.indentationLvl; + try { + output = formatter(ctx, value, recurseTimes, keys, braces); + for (i = 0; i < keys.length; i++) { + output.push( + formatProperty(ctx, value, recurseTimes, keys[i], extrasType)); + } + } catch (err) { + const constructorName = getCtxStyle(value, constructor, tag).slice(0, -1); + return handleMaxCallStackSize(ctx, err, constructorName, indentationLvl); + } + if (ctx.circular !== undefined) { + const index = ctx.circular.get(value); + if (index !== undefined) { + const reference = ctx.stylize(``, '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 = output.sort(comparator); + } else if (keys.length > 1) { + const sorted = output.slice(output.length - keys.length).sort(comparator); + output.splice(output.length - keys.length, keys.length, ...sorted); + } + } + 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; +} + +function getIteratorBraces(type, tag) { + if (tag !== `${type} Iterator`) { + if (tag !== '') { + tag += '] ['; + } + tag += `${type} Iterator`; + } + return [ `[${tag}] {`, '}' ]; +} + +function getBoxedBase(value, ctx, keys, constructor, tag) { + let fn; + let type; + if (isNumberObject(value)) { + fn = NumberPrototype; + type = 'Number'; + } else if (isStringObject(value)) { + fn = StringPrototype; + 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)) { + fn = BooleanPrototype; + type = 'Boolean'; + } else { + fn = SymbolPrototype; + type = 'Symbol'; + } + let base = `[${type}`; + if (type !== constructor) { + if (constructor === null) { + base += ' (null prototype)'; + } else { + base += ` (${constructor})`; + } + } + base += `: ${formatPrimitive(stylizeNoColor, fn.valueOf(value), ctx)}]`; + if (tag !== '' && tag !== constructor) { + base += ` [${tag}]`; + } + if (keys.length !== 0 || ctx.stylize === stylizeNoColor) { + return base; + } + return ctx.stylize(base, type.toLowerCase()); +} + +function getFunctionBase(value, constructor, tag) { + let type = 'Function'; + if (isGeneratorFunction(value)) { + type = `Generator${type}`; + } + if (isAsyncFunction(value)) { + type = `Async${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; +} + +function formatError(err, constructor, tag, ctx) { + let stack = err.stack || ErrorPrototype.toString.call(err); + // try to normalize JavaScriptCore stack to match v8 + if (isIos) { + const lines = stack.split('\n'); + stack = `${err.name}: ${err.message}`; + if (lines.length > 0) { + stack += lines.map(stackLine => { + const atSymbolIndex = stackLine.indexOf('@'); + const source = stackLine.slice(atSymbolIndex + 1); + const sourcePattern = /(.*):(\d+):(\d+)/; + let symbolName = 'unknown'; + if (atSymbolIndex !== -1) { + symbolName = stackLine.slice(0, atSymbolIndex); + } + + const sourceMatch = source.match(sourcePattern); + if (sourceMatch) { + let filePath = sourceMatch[1]; + const lineNumber = sourceMatch[2]; + const column = sourceMatch[3]; + if (filePath.startsWith('file:')) { + filePath = filePath.replace(`file://${Ti.Filesystem.resourcesDirectory}`, ''); + } + + return `\n at ${symbolName} (${filePath}:${lineNumber}:${column})`; + } else { + return `\n at ${symbolName} (${source})`; + } + }).join(''); + } + } + + // A stack trace may contain arbitrary data. Only manipulate the output + // for "regular errors" (errors that "look normal") for now. + const name = err.name || 'Error'; + let len = name.length; + if (constructor === null + || name.endsWith('Error') + && stack.startsWith(name) + && (stack.length === len || stack[len] === ':' || stack[len] === '\n')) { + let fallback = 'Error'; + if (constructor === null) { + const start = stack.match(/^([A-Z][a-z_ A-Z0-9[\]()-]+)(?::|\n {4}at)/) + || stack.match(/^([a-z_A-Z0-9-]*Error)$/); + fallback = start && start[1] || ''; + len = fallback.length; + fallback = fallback || 'Error'; + } + const prefix = getPrefix(constructor, tag, fallback).slice(0, -1); + if (name !== prefix) { + if (prefix.includes(name)) { + if (len === 0) { + stack = `${prefix}: ${stack}`; + } else { + stack = `${prefix}${stack.slice(len)}`; + } + } else { + stack = `${prefix} [${name}]${stack.slice(len)}`; + } + } + } + + // Ignore the error message if it's contained in the stack. + let pos = err.message && stack.indexOf(err.message) || -1; + if (pos !== -1) { + pos += err.message.length; + } + // Wrap the error in brackets in case it has no stack trace. + let stackStart = stack.indexOf('\n at', pos); + + if (stackStart === -1) { + stack = `[${stack}]`; + } else if (ctx.colors) { + // Highlight userland code and node modules. + let newStack = stack.slice(0, stackStart); + const lines = stack.slice(stackStart + 1).split('\n'); + for (const line of lines) { + // This adds underscores to all node_modules to quickly identify them. + let nodeModule; + newStack += '\n'; + let pos = 0; + while (nodeModule = nodeModulesRegExp.exec(line)) { + // '/node_modules/'.length === 14 + newStack += line.slice(pos, nodeModule.index + 14); + newStack += ctx.stylize(nodeModule[1], 'module'); + pos = nodeModule.index + nodeModule[0].length; + } + newStack += pos === 0 ? line : line.slice(pos); + } + stack = newStack; + } + // The message and the stack have to be indented as well! + if (ctx.indentationLvl !== 0) { + const indentation = ' '.repeat(ctx.indentationLvl); + stack = stack.replace(/\n/g, `\n${indentation}`); + } + return stack; +} + +function formatPromise(ctx, value, recurseTimes) { + // Node calls into native to get promise details which we can't do + return [ ctx.stylize('', 'special') ]; +} + +function formatProperty(ctx, value, recurseTimes, key, type) { + let name, str; + let extra = ' '; + const desc = Object.getOwnPropertyDescriptor(value, key) + || { value: value[key], enumerable: true }; + if (desc.value !== undefined) { + const diff = (type !== kObjectType || ctx.compact !== true) ? 2 : 3; + ctx.indentationLvl += diff; + str = formatValue(ctx, desc.value, recurseTimes); + if (diff === 3) { + const len = ctx.colors ? removeColors(str).length : str.length; + if (ctx.breakLength < len) { + 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 = value[key]; + 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 = ``; + 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') { + const tmp = key.toString().replace(strEscapeSequencesReplacer, escapeFn); + name = `[${ctx.stylize(tmp, 'symbol')}]`; + } else if (desc.enumerable === false) { + name = `[${key.replace(strEscapeSequencesReplacer, escapeFn)}]`; + } else if (keyStrRegExp.test(key)) { + name = ctx.stylize(key, 'name'); + } else { + name = ctx.stylize(strEscape(key), 'string'); + } + return `${name}:${extra}${str}`; +} + +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 = ctx.colors ? removeColors(output[i]).length : output[i].length; + 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 (actualMax * 3 + ctx.indentationLvl < ctx.breakLength + && (totalLength / actualMax > 5 || maxLength <= 6)) { + const approxCharHeights = 2.5; + const averageBias = Math.sqrt(actualMax - totalLength / output.length); + const biasedMax = Math.max(actualMax - 3 - averageBias, 1); + // Dynamically check how many columns seem possible. + const columns = Math.min( + // 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. + Math.round( + Math.sqrt( + approxCharHeights * biasedMax * outputLength + ) / biasedMax + ), + // Do not exceed the breakLength. + Math.floor((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 + ); + // 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]; + } + } + lineMaxLength += separatorSpace; + maxLineLength[i] = lineMaxLength; + } + let order = 'padStart'; + if (value !== undefined) { + for (let i = 0; i < output.length; i++) { + if (typeof value[i] !== 'number' && typeof value[i] !== 'bigint') { + order = 'padEnd'; + 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 = Math.min(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 += `${output[j]}, `[order](padding, ' '); + } + if (order === 'padStart') { + const padding = maxLineLength[j - i] + + output[j].length + - dataLen[j] + - separatorSpace; + str += output[j].padStart(padding, ' '); + } else { + str += output[j]; + } + tmp.push(str); + } + if (ctx.maxArrayLength < output.length) { + tmp.push(output[outputLength]); + } + output = tmp; + } + return output; +} + +function handleMaxCallStackSize(ctx, err, constructorName, indentationLvl) { + if (isStackOverflowError(err)) { + ctx.seen.pop(); + ctx.indentationLvl = indentationLvl; + return ctx.stylize( + `[${constructorName}: Inspection interrupted 'prematurely. Maximum call stack size exceeded.]`, + 'special' + ); + } + throw err; +} + +function formatNumber(fn, value) { + // Format -0 as '-0'. Checking `value === -0` won't distinguish 0 from -0. + return fn(Object.is(value, -0) ? '-0' : `${value}`, 'number'); +} +function formatBigInt(fn, value) { + return fn(`${value}n`, 'bigint'); +} + +function formatPrimitive(fn, value, ctx) { + if (typeof value === 'string') { + if (ctx.compact !== true + && value.length > kMinLineLength + && value.length > ctx.breakLength - ctx.indentationLvl - 4) { + return value.split(/\n/) + .map((line) => fn(strEscape(line), 'string')) + .join(` +\n${' '.repeat(ctx.indentationLvl + 2)}`); + } + return fn(strEscape(value), 'string'); + } + 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(SymbolPrototype.toString.call(value), 'symbol'); +} + +// The array is sparse and/or has extra keys +function formatSpecialArray(ctx, value, recurseTimes, maxLength, output, i) { + const keys = Object.keys(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; +} + +function formatArrayBuffer(ctx, value) { + const buffer = new Uint8Array(value); + /* + // @fixme rollup cannot handle lazy loaded modules, maybe move to webpack? + if (hexSlice === undefined) { + hexSlice = uncurryThis(require('../../buffer').default.Buffer.prototype.hexSlice); + } + */ + let str = hexSlice(buffer, 0, Math.min(ctx.maxArrayLength, buffer.length)) + .replace(/(.{2})/g, '$1 ').trim(); + const remaining = buffer.length - ctx.maxArrayLength; + if (remaining > 0) { + str += ` ... ${remaining} more byte${remaining > 1 ? 's' : ''}`; + } + return [ `${ctx.stylize('[Uint8Contents]', 'special')}: <${str}>` ]; +} + +function formatArray(ctx, value, recurseTimes) { + const valLen = value.length; + const len = Math.min(Math.max(0, ctx.maxArrayLength), valLen); + const remaining = valLen - len; + const output = []; + for (var i = 0; i < len; i++) { + // Special handle sparse arrays. + if (!hasOwnProperty(value, 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; +} + +function formatTypedArray(ctx, value, recurseTimes) { + const maxLength = Math.min(Math.max(0, ctx.maxArrayLength), value.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. + ctx.indentationLvl += 2; + for (const key of [ + 'BYTES_PER_ELEMENT', + 'length', + 'byteLength', + 'byteOffset', + 'buffer' + ]) { + const str = formatValue(ctx, value[key], recurseTimes, true); + output.push(`[${key}]: ${str}`); + } + ctx.indentationLvl -= 2; + } + return output; +} + +function formatSet(ctx, value, recurseTimes) { + const output = []; + ctx.indentationLvl += 2; + for (const v of value) { + output.push(formatValue(ctx, v, recurseTimes)); + } + ctx.indentationLvl -= 2; + // With `showHidden`, `length` will display as a hidden property for + // arrays. For consistency's sake, do the same for `size`, even though this + // property isn't selected by Object.getOwnPropertyNames(). + if (ctx.showHidden) { + output.push(`[size]: ${ctx.stylize(`${value.size}`, 'number')}`); + } + return output; +} + +function formatMap(ctx, value, recurseTimes) { + const output = []; + ctx.indentationLvl += 2; + for (const [ k, v ] of value) { + output.push(`${formatValue(ctx, k, recurseTimes)} => ${formatValue(ctx, v, recurseTimes)}`); + } + ctx.indentationLvl -= 2; + // See comment in formatSet + if (ctx.showHidden) { + output.push(`[size]: ${ctx.stylize(`${value.size}`, 'number')}`); + } + return output; +} + +function formatSetIterInner(ctx, recurseTimes, entries, state) { + const maxArrayLength = Math.max(ctx.maxArrayLength, 0); + const maxLength = Math.min(maxArrayLength, entries.length); + let output = new Array(maxLength); + ctx.indentationLvl += 2; + for (var 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 = output.sort(); + } + const remaining = entries.length - maxLength; + if (remaining > 0) { + output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); + } + return output; +} + +function formatMapIterInner(ctx, recurseTimes, entries, state) { + const maxArrayLength = Math.max(ctx.maxArrayLength, 0); + // Entries exist as [key1, val1, key2, val2, ...] + const len = entries.length / 2; + const remaining = len - maxArrayLength; + const maxLength = Math.min(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)}`; + } + // 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); + } + } + ctx.indentationLvl -= 2; + if (remaining > 0) { + output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); + } + return output; +} + +function formatWeakCollection(ctx) { + return [ ctx.stylize('', 'special') ]; +} + +function formatWeakSet(ctx, value, recurseTimes) { + // Node calls into native to get a preview of actual values which we can't do + return formatWeakCollection(ctx); +} + +function formatWeakMap(ctx, value, recurseTimes) { + // Node calls into native to get a preview of actual values which we can't do + return formatWeakCollection(ctx); +} + +function formatIterator(ctx, value, recurseTimes, keys, braces) { + const entries = []; + let isKeyValue = false; + let result = value.next(); + while (!result.done) { + const currentEntry = result.value; + entries.push(currentEntry); + if (currentEntry[0] !== currentEntry[1]) { + isKeyValue = true; + } + result = value.next(); + } + if (isKeyValue) { + // Mark entry iterators as such. + braces[0] = braces[0].replace(/ Iterator] {$/, ' Entries] {'); + return formatMapIterInner(ctx, recurseTimes, entries, kMapEntries); + } + return formatSetIterInner(ctx, recurseTimes, entries, kIterator); +} + +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. + let totalLength = output.length + start; + if (totalLength + output.length > ctx.breakLength) { + return false; + } + for (var 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 === '' || !base.includes('\n'); +} + +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 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)) { + return `${base ? `${base} ` : ''}${braces[0]} ${join(output, ', ')} ${braces[1]}`; + } + } + } + // Line up each entry on an individual line. + const indentation = `\n${' '.repeat(ctx.indentationLvl)}`; + return `${base ? `${base} ` : ''}${braces[0]}${indentation} ` + + `${join(output, `,${indentation} `)}${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}` : ''} ${join(output, ', ')} ` + braces[1]; + } + const indentation = ' '.repeat(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}${join(output, `,\n${indentation} `)} ${braces[1]}`; +} + +export function format(...args) { + return formatWithOptions(undefined, ...args); +} + +const firstErrorLine = (error) => error.message.split('\n')[0]; +let CIRCULAR_ERROR_MESSAGE; +function tryStringify(arg) { + try { + return JSON.stringify(arg); + } catch (err) { + // Populate the circular error message lazily + if (!CIRCULAR_ERROR_MESSAGE) { + try { + const a = {}; + a.a = a; + JSON.stringify(a); + } catch (e) { + CIRCULAR_ERROR_MESSAGE = firstErrorLine(e); + } + } + if (err.name === 'TypeError' + && firstErrorLine(err) === CIRCULAR_ERROR_MESSAGE) { + return '[Circular]'; + } + throw err; + } +} + +/* eslint-disable max-depth */ +export function formatWithOptions(inspectOptions, ...args) { + const first = args[0]; + let a = 0; + let str = ''; + let join = ''; + if (typeof first === 'string') { + if (args.length === 1) { + return first; + } + let tempStr; + let lastPos = 0; + for (var i = 0; i < first.length - 1; i++) { + if (first.charCodeAt(i) === 37) { // '%' + const nextChar = first.charCodeAt(++i); + if (a + 1 !== args.length) { + switch (nextChar) { + case 115: // 's' + const tempArg = args[++a]; + if (typeof tempArg === 'number') { + tempStr = formatNumber(stylizeNoColor, tempArg); + } else if (typeof tempArg === 'bigint') { + tempStr = `${tempArg}n`; + } else { + let constr; + if (typeof tempArg !== 'object' + || tempArg === null + || typeof tempArg.toString === 'function' + && (hasOwnProperty(tempArg, 'toString') + // A direct own property on the constructor prototype in + // case the constructor is not an built-in object. + || (constr = tempArg.constructor) + && !builtInObjects.has(constr.name) + && constr.prototype + && hasOwnProperty(constr.prototype, 'toString') + ) + ) { + tempStr = String(tempArg); + } else { + tempStr = inspect(tempArg, { + ...inspectOptions, + compact: 3, + colors: false, + depth: 0 + }); + } + } + break; + case 106: // 'j' + tempStr = tryStringify(args[++a]); + break; + case 100: // 'd' + const tempNum = args[++a]; + if (typeof tempNum === 'bigint') { + tempStr = `${tempNum}n`; + } else if (typeof tempNum === 'symbol') { + tempStr = 'NaN'; + } else { + tempStr = formatNumber(stylizeNoColor, Number(tempNum)); + } + break; + case 79: // 'O' + tempStr = inspect(args[++a], inspectOptions); + break; + case 111: // 'o' + { + tempStr = inspect(args[++a], { + ...inspectOptions, + showHidden: true, + showProxy: true, + depth: 4 + }); + break; + } + case 105: // 'i' + const tempInteger = args[++a]; + if (typeof tempInteger === 'bigint') { + tempStr = `${tempInteger}n`; + } else if (typeof tempInteger === 'symbol') { + tempStr = 'NaN'; + } else { + tempStr = formatNumber(stylizeNoColor, parseInt(tempInteger)); + } + break; + case 102: // 'f' + const tempFloat = args[++a]; + if (typeof tempFloat === 'symbol') { + tempStr = 'NaN'; + } else { + tempStr = formatNumber(stylizeNoColor, parseFloat(tempFloat)); + } + break; + case 37: // '%' + str += first.slice(lastPos, i); + lastPos = i + 1; + continue; + default: // Any other character is not a correct placeholder + continue; + } + if (lastPos !== i - 1) { + str += first.slice(lastPos, i - 1); + } + str += tempStr; + lastPos = i + 1; + } else if (nextChar === 37) { + str += first.slice(lastPos, i); + lastPos = i + 1; + } + } + } + if (lastPos !== 0) { + a++; + join = ' '; + if (lastPos < first.length) { + str += first.slice(lastPos); + } + } + } + while (a < args.length) { + const value = args[a]; + str += join; + str += typeof value !== 'string' ? inspect(value, inspectOptions) : value; + join = ' '; + a++; + } + return str; +} +/* eslint-enable max-depth */ diff --git a/common/Resources/ti.internal/extensions/node/internal/util/types.js b/common/Resources/ti.internal/extensions/node/internal/util/types.js new file mode 100644 index 00000000000..981720f0d4b --- /dev/null +++ b/common/Resources/ti.internal/extensions/node/internal/util/types.js @@ -0,0 +1,206 @@ +import { uncurryThis } from '../util'; + +const TypedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype); + +const TypedArrayProto_toStringTag = uncurryThis( + Object.getOwnPropertyDescriptor(TypedArrayPrototype, Symbol.toStringTag).get); + +function checkPrototype(value, name) { + if (typeof value !== 'object') { + return false; + } + + return Object.prototype.toString.call(value) === `[object ${name}]`; +} + +export function isAnyArrayBuffer(value) { + if (isArrayBuffer(value)) { + return true; + } + + return isSharedArrayBuffer(value); +} + +export function isArgumentsObject(value) { + return checkPrototype(value, 'Arguments'); +} + +export function isArrayBuffer(value) { + return checkPrototype(value, 'ArrayBuffer'); +} + +// Cached to make sure no userland code can tamper with it. +export const isArrayBufferView = ArrayBuffer.isView; + +export function isAsyncFunction(value) { + return checkPrototype(value, 'AsyncFunction'); +} + +export function isBigInt64Array(value) { + return TypedArrayProto_toStringTag(value) === 'BigInt64Array'; +} + +export function isBigUint64Array(value) { + return TypedArrayProto_toStringTag(value) === 'BigUint64Array'; +} + +export function isBooleanObject(value) { + return checkPrototype(value, 'Boolean'); +} + +export function isBoxedPrimitive(value) { + if (typeof value !== 'object') { + return false; + } + + return isNumberObject(value) + || isStringObject(value) + || isBooleanObject(value) + // || isBigIntObject(value) + || isSymbolObject(value); +} + +export function isDataView(value) { + return checkPrototype(value, 'DataView'); +} + +export function isDate(value) { + return checkPrototype(value, 'Date'); +} + +// @todo isExternal + +export function isFloat32Array(value) { + return TypedArrayProto_toStringTag(value) === 'Float32Array'; +} + +export function isFloat64Array(value) { + return TypedArrayProto_toStringTag(value) === 'Float64Array'; +} + +export function isGeneratorFunction(value) { + return checkPrototype(value, 'GeneratorFunction'); +} + +export function isGeneratorObject(value) { + return checkPrototype(value, 'GeneratorObject'); +} + +export function isInt8Array(value) { + return TypedArrayProto_toStringTag(value) === 'Int8Array'; +} + +export function isInt16Array(value) { + return TypedArrayProto_toStringTag(value) === 'Int16Array'; +} + +export function isInt32Array(value) { + return TypedArrayProto_toStringTag(value) === 'Int32Array'; +} + +export function isMap(value) { + return checkPrototype(value, 'Map'); +} + +export function isMapIterator(value) { + if (typeof value !== 'object') { + return false; + } + + const prototype = Object.getPrototypeOf(value); + return prototype && prototype[Symbol.toStringTag] === 'Map Iterator'; +} + +// @todo isModuleNamespaceObject + +export function isNativeError(value) { + // if not an instance of an Error, definitely not a native error + if (!(value instanceof Error)) { + return false; + } + if (!value || !value.constructor) { + return false; + } + return [ + 'Error', + 'EvalError', + 'RangeError', + 'ReferenceError', + 'SyntaxError', + 'TypeError', + 'URIError' + ].includes(value.constructor.name); +} + +export function isNumberObject(value) { + return checkPrototype(value, 'Number'); +} + +export function isPromise(value) { + return checkPrototype(value, 'Promise'); +} + +// @todo isProxy + +export function isRegExp(value) { + return checkPrototype(value, 'RegExp'); +} + +export function isSet(value) { + return checkPrototype(value, 'Set'); +} + +export function isSetIterator(value) { + if (typeof value !== 'object') { + return false; + } + + const prototype = Object.getPrototypeOf(value); + return prototype && prototype[Symbol.toStringTag] === 'Set Iterator'; +} + +export function isSharedArrayBuffer(value) { + if (!global.SharedArrayBuffer) { + return false; + } + + return checkPrototype(value, 'SharedArrayBuffer'); +} + +export function isStringObject(value) { + return checkPrototype(value, 'String'); +} + +export function isSymbolObject(value) { + return checkPrototype(value, 'Symbol'); +} + +export function isTypedArray(value) { + return TypedArrayProto_toStringTag(value) !== undefined; +} + +export function isUint8Array(value) { + return TypedArrayProto_toStringTag(value) === 'Uint8Array'; +} + +export function isUint8ClampedArray(value) { + return TypedArrayProto_toStringTag(value) === 'Uint8ClampedArray'; +} + +export function isUint16Array(value) { + return TypedArrayProto_toStringTag(value) === 'Uint16Array'; +} + +export function isUint32Array(value) { + return TypedArrayProto_toStringTag(value) === 'Uint32Array'; +} + +export function isWeakMap(value) { + return checkPrototype(value, 'WekMap'); +} + +export function isWeakSet(value) { + return checkPrototype(value, 'WekSet'); +} + +// @todo isWebAssemblyCompiledModule diff --git a/common/Resources/ti.internal/extensions/node/util.js b/common/Resources/ti.internal/extensions/node/util.js index 98e0e675d8c..a908fc5cd26 100644 --- a/common/Resources/ti.internal/extensions/node/util.js +++ b/common/Resources/ti.internal/extensions/node/util.js @@ -1,61 +1,19 @@ import assertArgumentType from './_errors'; +import * as types from './internal/util/types'; +import { format, formatWithOptions, inspect } from './internal/util/inspect'; +import Buffer from './buffer'; const MONTHS = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]; const util = { - // So node actually calls into native code for these checks, but I think for shim compatability this is good enough - // There's overhead for doing the native checks, and it'd require a native module to achieve. - types: { - // TODO: We're missing a lot of the methods hanging off this namespace! - isNumberObject: value => { - return typeof value === 'object' && Object.prototype.toString.call(value) === '[object Number]'; - }, - isStringObject: value => { - return typeof value === 'object' && Object.prototype.toString.call(value) === '[object String]'; - }, - isBooleanObject: value => { - return typeof value === 'object' && Object.prototype.toString.call(value) === '[object Boolean]'; - }, - // isBigIntObject: value => { - // return Object.prototype.toString.call(value) === '[object BigInt]'; - // }, - isSymbolObject: value => { - return typeof value === 'object' && Object.prototype.toString.call(value) === '[object Symbol]'; - }, - isBoxedPrimitive: function (value) { - if (typeof value !== 'object') { - return false; - } - return this.isNumberObject(value) - || this.isStringObject(value) - || this.isBooleanObject(value) - // || this.isBigIntObject(value) - || this.isSymbolObject(value); - }, - isNativeError: value => { - // if not an instance of an Error, definitely not a native error - if (!(value instanceof Error)) { - return false; - } - if (!value || !value.constructor) { - return false; - } - return [ 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', 'TypeError', 'URIError' ].includes(value.constructor.name); - - }, - isPromise: value => { - const valueType = typeof value; - return (valueType === 'object' || valueType === 'function') && value.then && typeof value.then === 'function'; - }, - isSet: value => value instanceof Set, - isMap: value => value instanceof Map, - isDate: value => value instanceof Date, - isRegexp: value => value instanceof RegExp || Object.prototype.toString.call(value) === '[object RegExp]' - }, - isArray: value => Array.isArray(value), + format, + formatWithOptions, + inspect, + isArray: Array.isArray, isBoolean: value => typeof value === 'boolean', + isBuffer: Buffer.Buffer.isBuffer, isFunction: value => typeof value === 'function', isNull: value => value === null, isNullOrUndefined: value => value === undefined || value === null, @@ -65,6 +23,9 @@ const util = { isString: value => typeof value === 'string', isSymbol: value => typeof value === 'symbol', isUndefined: value => value === undefined, + isRegExp: types.isRegExp, + isDate: types.isDate, + isError: (e) => Object.prototype.toString.call(e) === '[object Error]' || e instanceof Error, log: string => { const date = new Date(); const time = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`; @@ -74,382 +35,8 @@ const util = { print: (...args) => console.log(args.join('')), // FIXME: Shouldn't add trailing newline like console.log does! puts: (...args) => console.log(args.join('\n')), error: (...args) => console.error(args.join('\n')), - debug: string => console.error(`DEBUG: ${string}`) -}; - -util.isBuffer = () => false; // FIXME: Check for Ti.Buffer? for node/browserify buffer? -util.isDate = value => util.types.isDate(value); -util.isError = value => util.types.isNativeError(value); -util.isRegexp = value => util.types.isRegexp(value); - -function getConstructor(obj) { - if (obj.constructor) { - return obj.constructor.name; - } - return 'Object'; -} - -const defaultInspectOptions = { - showHidden: false, - depth: 2, - colors: false, - customInspect: true, - showProxy: false, - maxArrayLength: 100, - breakLength: 60, - compact: true, - sorted: false, - getters: false -}; - -function formatArray(array, options) { - const maxLength = Math.max(0, options.maxArrayLength); - const arrayLength = array.length; - const values = []; - let consecutiveEmpties = 0; - let i = 0; - // for sparse arrays, consecutive empties count as a "single item" in terms of maxArrayLength - for (; i < arrayLength; i++) { // don't go past end of array... - const value = array[i]; - if (value === undefined) { // sparse array! - consecutiveEmpties++; - continue; - } - - // non-empty index currently... - if (consecutiveEmpties > 0) { // were we collecting consecutive empty indices as a single gap? - values.push(`<${consecutiveEmpties} empty item${consecutiveEmpties > 1 ? 's' : ''}>`); - consecutiveEmpties = 0; // reset our count - if (values.length >= maxLength) { // don't show more than options.maxArrayLength "values" - break; - } - } - // push the current index value - values.push(util.inspect(value, options)); - if (values.length >= maxLength) { // don't show more than options.maxArrayLength "values" - i++; // so our "remaining" count is correct - break; - } - } - - const remaining = arrayLength - i; - if (remaining > 0) { // did we stop before the end of the array (due to options.maxArrayLength)? - values.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); - } else if (consecutiveEmpties > 0) { // did the sparse array gaps run to the end of the array? - values.push(`<${consecutiveEmpties} empty item${consecutiveEmpties > 1 ? 's' : ''}>`); - } - - return values; -} - -/** - * @param {*} obj JS value or object to inspect - * @param {object} [options] options for output - * @param {Integer} [options.breakLength=60] length at which to break properties into individual lines - * @param {boolean} [options.showHidden=false] whether to include hidden properties (non-enumerable) - * @param {boolean} [options.sorted=false] whether to sort the property listings per-object - * @param {boolean} [options.compact=true] if set to `false`, uses luxurious amount of spacing and newlines - * @param {Integer} [options.depth=2] depth to recurse into objects - * @returns {string} - */ -util.inspect = (obj, options = {}) => { - const mergedOptions = Object.assign({}, defaultInspectOptions, options); - // increase our recursion counter to avoid going past depth - if (mergedOptions.recursionCount === undefined) { - mergedOptions.recursionCount = -1; - } - mergedOptions.recursionCount++; - if (mergedOptions.indentLevel === undefined) { - mergedOptions.indentLevel = 0; - } - try { - const objType = typeof obj; - if (objType === 'object' || objType === 'function') { - if (obj === null) { - return 'null'; - } - - // Guard against circular references - mergedOptions.memo = mergedOptions.memo || []; - if (mergedOptions.memo.includes(obj)) { - return '[Circular]'; - } - try { - mergedOptions.memo.push(obj); // popped off in a finally block, so we only worry about circular references, not sibling references - - const constructorName = getConstructor(obj); - // if the constructor name is not 'Object', pre-pend it! - let prefix = ''; - if (constructorName !== 'Object') { - prefix = `${constructorName} `; - } - // now grab the type tag if it has one! - const tag = obj[Symbol.toStringTag]; - if (tag && tag !== constructorName) { - prefix = `${prefix}[${tag}] `; - } - - // what braces do we use to enclose the values/properties? - let open = '{'; - let close = '}'; - let header = ''; // for special cases like Function where we pre-pend header info - const values = []; // collect the values/properties we list! - const isArray = Array.isArray(obj); - if (isArray) { - if (prefix === 'Array ') { - prefix = ''; // wipe "normal" Array prefixes - } - [ open, close ] = [ '[', ']' ]; // use array braces - values.push(...formatArray(obj, mergedOptions)); - } else if (util.types.isMap(obj)) { - if (obj.size > 0) { - values.push(...Array.from(obj).map(entry => `${util.inspect(entry[0], mergedOptions)} => ${util.inspect(entry[1], mergedOptions)}`)); - } - } else if (util.types.isSet(obj)) { - if (obj.size > 0) { - values.push(...Array.from(obj).map(o => util.inspect(o, mergedOptions))); - } - } else if (util.types.isRegexp(obj)) { - // don't do prefix or any of that crap! TODO: Can we just call Regexp.prototype.toString.call()? - return `/${obj.source}/${obj.flags}`; - } else if (util.isFunction(obj)) { - if (prefix === 'Function ') { - prefix = ''; // wipe "normal" Function prefixes - } - - // Functions are special and we must use a "header" - // if no values/properties, just print the "header" - // if any, stick "header" inside braces before property/value listing - if (obj.name) { - header = `[Function: ${obj.name}]`; - } else { - header = '[Function]'; - } - } - - // If we've gone past our depth, just do a quickie result here, like '[Object]' - if (mergedOptions.recursionCount > mergedOptions.depth) { - return header || `[${constructorName || tag || 'Object'}]`; - } - - // handle properties - const properties = []; - // if showing hidden, get all own properties, otherwise just enumerable - const ownProperties = (mergedOptions.showHidden) ? Object.getOwnPropertyNames(obj) : Object.keys(obj); - // FIXME: On V8/Android we are not getting 'arguments' and 'caller' properties! - // This may be because in newer specs/strict mode they shouldn't be accessible? - for (const propName of ownProperties) { - if (isArray && propName.match(/^\d+$/)) { // skip Array's index properties - continue; - } - const propDesc = Object.getOwnPropertyDescriptor(obj, propName) - || { value: obj[propName], enumerable: true }; // fall back to faking a descriptor - const key = propDesc.enumerable ? propName : `[${propName}]`; // If not enumerable, wrap name in []! - if (propDesc.value !== undefined) { - mergedOptions.indentLevel += 3; // Node uses 3 spaces for arrays/Objects? - let space = ' '; - const value = util.inspect(propDesc.value, mergedOptions); - // if value is breaking, break between key and top-level value - if (value.length > mergedOptions.breakLength) { - space = `\n${' '.repeat(mergedOptions.indentLevel)}`; - } - mergedOptions.indentLevel -= 3; - properties.push(`${key}:${space}${value}`); - } else if (propDesc.get !== undefined) { - // TODO: Handle when options.getters === true, need to actually attempt to get and show value! - if (propDesc.set !== undefined) { - properties.push(`${key}: [Getter/Setter]`); - } else { - properties.push(`${key}: [Getter]`); - } - } else if (propDesc.set !== undefined) { - properties.push(`${key}: [Setter]`); - } else { // weird case of a property defined with an explicit undefined value - properties.push(`${key}: undefined`); - } - } - if (properties.length !== 0) { - // TODO: Handle custom sorting option! - if (mergedOptions.sorted) { - properties.sort(); - } - values.push(...properties); - } - - let value = ''; - if (values.length === 0) { - if (header.length > 0) { - value = header; // i.e. '[Function: name]' - } else { - value = `${open}${close}`; // no spaces, i.e. '{}' or '[]' - } - } else { - let str = ''; - if (header.length > 0) { // i.e. '{ [Function] a: 1, b: 2 }' - str = `${header} `; - } - // Handle breaking them by breakLength here! - let length = 0; - for (const value of values) { - length += value.length + 1; // Node seems to add one for comma, but not more for spaces? - if (length > mergedOptions.breakLength) { // break early if length > breakLength! - break; - } - } - if (length > mergedOptions.breakLength) { - const indent = ' '.repeat(mergedOptions.indentLevel); - // break them up! - str += values.join(`,\n${indent} `); - } else { - str += values.join(', '); - } - - value = `${open} ${str} ${close}`; // spaces between braces and values/properties - } - - return `${prefix}${value}`; - } finally { - mergedOptions.memo.pop(obj); - } - } - // only special case is -0 - if (objType === 'string') { - return `'${obj}'`; - } else if (objType === 'number' && Object.is(obj, -0)) { // can't check for -0 using === - return '-0'; - } else if (util.isSymbol(obj)) { - return obj.toString(); - } - // TODO: Handle BigInt? - return `${obj}`; - } finally { - mergedOptions.recursionCount--; - } -}; - -/** - * Retruns result of `JSON.stringify()` if possible, falling back to `'[Circular]'` if that throws. - * @param {*} value The value/object to stringify - * @returns {string} - */ -function stringify(value) { - try { - return JSON.stringify(value); - } catch (e) { - if (e instanceof TypeError - && (e.message.includes('circular') || e.message.includes('cyclic'))) { - // "Converting circular structure to JSON" - // JSC gives: "JSON.stringify cannot serialize cyclic structures." - // TODO: Maybe force a circular reference object through and sniff the JS engine's message generated to match against? - return '[Circular]'; - } - throw e; - } -} - -util.format = (...args) => { - const firstArg = args[0]; - if (typeof firstArg === 'string') { - // normal usage! - if (args.length === 1) { - return firstArg; - } - - // TODO: ok, we really do have to look at the string to find the % specifiers - // Do we loop over the args.length and find next index of '%', match what type it is and replace? - let lastIndex = 0; - let str = ''; - let i = 1; // start at second argument - for (i; i < args.length;) { - const curArg = args[i]; - const foundIndex = firstArg.indexOf('%', lastIndex); - if (foundIndex === -1) { - // No more placeholders left, so break and at bottom we'll append rest of string - break; - } - // grab segment of string and append to str - str += firstArg.slice(lastIndex, foundIndex); - // now look at next char to see how to replace - const nextChar = firstArg.charAt(foundIndex + 1); - switch (nextChar) { - case 's': // string - str += String(curArg); - i++; // consume argument - break; - - case 'd': // Number - if (util.isSymbol(curArg) || util.types.isSymbolObject(curArg)) { - str += 'NaN'; - } else { - str += Number(curArg); - } - i++; // consume argument - break; - - case 'i': // Integer - if (util.isSymbol(curArg) || util.types.isSymbolObject(curArg)) { - str += 'NaN'; - } else { - str += parseInt(curArg); - } - i++; // consume argument - break; - - case 'f': // Float - if (util.isSymbol(curArg) || util.types.isSymbolObject(curArg)) { - str += 'NaN'; - } else { - str += parseFloat(curArg); - } - i++; // consume argument - break; - - case 'j': // JSON - str += stringify(curArg); - i++; // consume argument - break; - - case 'o': // Object w/showHidden and showProxy - str += util.inspect(curArg, { showHidden: true, showProxy: true, depth: 4 }); - i++; // consume argument - break; - - case 'O': // Object w/o options - str += util.inspect(curArg, {}); - i++; // consume argument - break; - - case '%': // escaped % - str += '%'; - // Don't consume argument here! - break; - } - lastIndex = foundIndex + 2; - } - - // If we haven't reached end of string, append rest of it with no replacements! - str += firstArg.slice(lastIndex, firstArg.length); - - // If we have args remaining, need to... - // loop over rest of args and coerce to Strings and concat joined by spaces. - // Unless typeof === 'object' or 'symbol', then do util.inspect() on them - if (i < args.length) { - str += ` ${args.slice(i).map(a => { - const aType = typeof a; - switch (aType) { - case 'object': - case 'symbol': - return util.inspect(a); - default: - return String(a); - } - }).join(' ')}`; - } - return str; - } - - // first arg wasn't string, so we loop over args and call util.inspect on each - return args.map(a => util.inspect(a)).join(' '); + debug: string => console.error(`DEBUG: ${string}`), + types }; /** diff --git a/tests/Resources/util.test.js b/tests/Resources/util.test.js new file mode 100644 index 00000000000..0a8b2745010 --- /dev/null +++ b/tests/Resources/util.test.js @@ -0,0 +1,1539 @@ +/* + * Appcelerator Titanium Mobile + * Copyright (c) 2011-Present by Appcelerator, Inc. All Rights Reserved. + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ +/* eslint-env mocha */ +/* eslint no-unused-expressions: "off" */ +/* eslint no-array-constructor: "off" */ +/* eslint no-new-wrappers: "off" */ +'use strict'; + +const should = require('./utilities/assertions'); // eslint-disable-line no-unused-vars +const utilities = require('./utilities/utilities'); + +let util; + +/** + * Compares two version strings and returns true if the actual version is + * greater than or equal to the expected version + * @param {string} version actual version we're checking + * @param {string} expected version to match against + * @returns {boolean} + */ +function greaterOrEqualTo(version, expected) { + const expectedParts = expected.split('.'); + const actualParts = version.split('.'); + // Pad shorter array with 0s + while (expectedParts.length > actualParts.length) { + actualParts.push(0); + } + while (actualParts.length > expectedParts.length) { + expectedParts.push(0); + } + // shoudl be the same length now + const length = expectedParts.length; + for (let i = 0; i < length; i++) { + const actualNum = Number.parseInt(actualParts[i]); + const expectedNum = Number.parseInt(expectedParts[i]); + if (actualNum > expectedNum) { + return true; + } + if (actualNum < expectedNum) { + return false; + } + // else, continue to next segment + } + return true; // everything matched +} + +describe.only('util', () => { + it('should be required as core module', () => { + util = require('util'); + util.should.be.an.Object; + }); + + // For copious tests, see https://github.com/nodejs/node/blob/master/test/parallel/test-util-format.js + describe('#format()', () => { + it('is a function', () => { + util.format.should.be.a.Function; + }); + + it('if placeholder has no corresponding argument, don\'t replace placeholder', () => { + util.format('%s:%s', 'foo').should.eql('foo:%s'); + }); + + it('extra arguments are coerced into strings and concatenated delimited by space', () => { + util.format('%s:%s', 'foo', 'bar', 'baz').should.eql('foo:bar baz'); + }); + + it('if first arg is not string, concat all args separated by spaces', () => { + util.format(1, 2, 3).should.eql('1 2 3'); + }); + + it('if only one arg, returned as-is', () => { + util.format('%% %s').should.eql('%% %s'); + }); + + describe('String placeholder', () => { + it('with int', () => { + util.format('%s', 1).should.eql('1'); + util.format('%s', 42).should.eql('42'); + util.format('%s %s', 42, 43).should.eql('42 43'); + util.format('%s %s', 42).should.eql('42 %s'); + }); + + it('with undefined', () => { + util.format('%s', undefined).should.eql('undefined'); + }); + + it('with null', () => { + util.format('%s', null).should.eql('null'); + }); + + it('with string', () => { + util.format('%s', 'foo').should.eql('foo'); + }); + + it('with string holding int value', () => { + util.format('%s', '42').should.eql('42'); + }); + + it('with floats', () => { + util.format('%s', 42.0).should.eql('42'); + util.format('%s', 1.5).should.eql('1.5'); + util.format('%s', -0.5).should.eql('-0.5'); + }); + + it('with Symbol', () => { + util.format('%s', Symbol()).should.eql('Symbol()'); + util.format('%s', Symbol('foo')).should.eql('Symbol(foo)'); + }); + // TODO: BigInt + }); + + describe('Number placeholder', () => { + it('with floats', () => { + util.format('%d', 42.0).should.eql('42'); + util.format('%d', 1.5).should.eql('1.5'); + util.format('%d', -0.5).should.eql('-0.5'); + }); + + it('with ints', () => { + util.format('%d', 42).should.eql('42'); + util.format('%d %d', 42, 43).should.eql('42 43'); + util.format('%d %d', 42).should.eql('42 %d'); + }); + + it('with string holding int value', () => { + util.format('%d', '42').should.eql('42'); + }); + + it('with string holding float value', () => { + util.format('%d', '42.0').should.eql('42'); + }); + + it('with empty string', () => { + util.format('%d', '').should.eql('0'); + }); + + it('with Symbol', () => { + util.format('%d', Symbol()).should.eql('NaN'); + }); + + it('with null', () => { + util.format('%d', null).should.eql('0'); + }); + + it('with undefined', () => { + util.format('%d', undefined).should.eql('NaN'); + }); + + // TODO: BigInt + }); + + describe('Float placeholder', () => { + it('with floats', () => { + util.format('%f', 42.0).should.eql('42'); + util.format('%f', 1.5).should.eql('1.5'); + util.format('%f', -0.5).should.eql('-0.5'); + }); + + it('with ints', () => { + util.format('%f', 42).should.eql('42'); + util.format('%f %f', 42, 43).should.eql('42 43'); + util.format('%f %f', 42).should.eql('42 %f'); + }); + + it('with string holding int value', () => { + util.format('%f', '42').should.eql('42'); + }); + + it('with string holding float value', () => { + util.format('%f', '42.0').should.eql('42'); + }); + + it('with empty string', () => { + util.format('%f', '').should.eql('NaN'); + }); + + it('with Symbol', () => { + util.format('%f', Symbol()).should.eql('NaN'); + }); + + it('with null', () => { + util.format('%f', null).should.eql('NaN'); + }); + + it('with undefined', () => { + util.format('%f', undefined).should.eql('NaN'); + }); + + // TODO: BigInt + }); + + describe('Integer placeholder', () => { + it('with ints', () => { + util.format('%i', 42).should.eql('42'); + util.format('%i %i', 42, 43).should.eql('42 43'); + util.format('%i %i', 42).should.eql('42 %i'); + }); + + it('with floats', () => { + util.format('%i', 42.0).should.eql('42'); + util.format('%i', 1.5).should.eql('1'); + util.format('%i', -0.5).should.eql('-0'); + }); + + it('with string holding int value', () => { + util.format('%i', '42').should.eql('42'); + }); + + it('with string holding float value', () => { + util.format('%i', '42.0').should.eql('42'); + }); + + it('with empty string', () => { + util.format('%i', '').should.eql('NaN'); + }); + + it('with Symbol', () => { + util.format('%i', Symbol()).should.eql('NaN'); + }); + + it('with null', () => { + util.format('%i', null).should.eql('NaN'); + }); + + it('with undefined', () => { + util.format('%i', undefined).should.eql('NaN'); + }); + + // TODO: BigInt + }); + + describe('JSON placeholder', () => { + it('with floats', () => { + util.format('%j', 42.0).should.eql('42'); + util.format('%j', 1.5).should.eql('1.5'); + util.format('%j', -0.5).should.eql('-0.5'); + }); + + it('with ints', () => { + util.format('%j', 42).should.eql('42'); + util.format('%j %j', 42, 43).should.eql('42 43'); + util.format('%j %j', 42).should.eql('42 %j'); + }); + + it('with string holding int value', () => { + util.format('%j', '42').should.eql('"42"'); + }); + + it('with string holding float value', () => { + util.format('%j', '42.0').should.eql('"42.0"'); + }); + + it('with empty string', () => { + util.format('%j', '').should.eql('""'); + }); + + it('with Symbol', () => { + util.format('%j', Symbol()).should.eql('undefined'); + }); + + it('with null', () => { + util.format('%j', null).should.eql('null'); + }); + + it('with undefined', () => { + util.format('%j', undefined).should.eql('undefined'); + }); + + it('with object having circular reference', () => { + const o = {}; + o.o = o; + util.format('%j', o).should.eql('[Circular]'); + }); + + it('with object throwing Error in toJSON() re-throws Error', () => { + const o = { + toJSON: () => { + throw new Error('Failed!'); + } + }; + (() => util.format('%j', o)).should.throw('Failed!'); + }); + + // TODO: BigInt + }); + + describe('%O - object placeholder', () => { + it('with int', () => { + util.format('%O', 42).should.eql('42'); + }); + + it('with undefined', () => { + util.format('%O', undefined).should.eql('undefined'); + }); + + it('with null', () => { + util.format('%O', null).should.eql('null'); + }); + + it('with string', () => { + util.format('%O', 'foo').should.eql('\'foo\''); + }); + + it('with string holding int value', () => { + util.format('%O', '42').should.eql('\'42\''); + }); + + it('with floats', () => { + util.format('%O', 42.0).should.eql('42'); + util.format('%O', 1.5).should.eql('1.5'); + util.format('%O', -0.5).should.eql('-0.5'); + }); + + it('with Symbol', () => { + util.format('%O', Symbol()).should.eql('Symbol()'); + util.format('%O', Symbol('foo')).should.eql('Symbol(foo)'); + }); + + it('with simple object', () => { + const obj = { + foo: 'bar' + }; + util.format('%O', obj).should.eql('{ foo: \'bar\' }'); + }); + + it('with object', () => { + const obj = { + foo: 'bar', + foobar: 1, + func: function () {} + }; + util.format('%O', obj).should.eql('{ foo: \'bar\', foobar: 1, func: [Function: func] }'); + }); + + it('with nested object', () => { + const nestedObj2 = { + foo: 'bar', + foobar: 1, + func: [ { a: function () {} } ] + }; + // FIXME: There's a weird edge case we fail here: when function is at cutoff depth and showHidden is true, we report '[Function: a]', while node reports '[Function]' + // I don't know why. + util.format('%O', nestedObj2).should.eql( + '{ foo: \'bar\', foobar: 1, func: [ { a: [Function: a] } ] }'); + }); + + it('with same object twice', () => { + const obj = { + foo: 'bar', + foobar: 1, + func: function () {} + }; + util.format('%O %O', obj, obj).should.eql( + '{ foo: \'bar\', foobar: 1, func: [Function: func] } ' + + '{ foo: \'bar\', foobar: 1, func: [Function: func] }'); + }); + }); + + describe('%o - object placeholder', () => { + it('with int', () => { + util.format('%o', 42).should.eql('42'); + }); + + it('with undefined', () => { + util.format('%o', undefined).should.eql('undefined'); + }); + + it('with null', () => { + util.format('%o', null).should.eql('null'); + }); + + it('with string', () => { + util.format('%o', 'foo').should.eql('\'foo\''); + }); + + it('with string holding int value', () => { + util.format('%o', '42').should.eql('\'42\''); + }); + + it('with floats', () => { + util.format('%o', 42.0).should.eql('42'); + util.format('%o', 1.5).should.eql('1.5'); + util.format('%o', -0.5).should.eql('-0.5'); + }); + + it('with Symbol', () => { + util.format('%o', Symbol()).should.eql('Symbol()'); + util.format('%o', Symbol('foo')).should.eql('Symbol(foo)'); + }); + + it('with simple object', () => { + const obj = { + foo: 'bar' + }; + util.format('%o', obj).should.eql('{ foo: \'bar\' }'); + }); + + it('with object', () => { + const obj = { + foo: 'bar', + foobar: 1, + func: function () {} + }; + util.format('%o', obj).should.eql( + '{\n' + + ' foo: \'bar\',\n' + + ' foobar: 1,\n' + + ' func: [Function: func] {\n' + + ' [arguments]: null,\n' + + ' [caller]: null,\n' + + ' [length]: 0,\n' + + ' [name]: \'func\',\n' + + ' [prototype]: func { [constructor]: [Circular *1] }\n' + + ' }\n' + + '}' + ); + }); + + it('with nested object', () => { + const nestedObj2 = { + foo: 'bar', + foobar: 1, + func: [ { a: function () {} } ] + }; + util.format('%o', nestedObj2).should.eql( + '{\n' + + ' foo: \'bar\',\n' + + ' foobar: 1,\n' + + ' func: [\n' + + ' {\n' + + ' a: [Function: a] {\n' + + ' [arguments]: null,\n' + + ' [caller]: null,\n' + + ' [length]: 0,\n' + + ' [name]: \'a\',\n' + + ' [prototype]: a { [constructor]: [Circular *1] }\n' + + ' }\n' + + ' },\n' + + ' [length]: 1\n' + + ' ]\n' + + '}' + ); + }); + + // The property order is not consistent on iOS, which is kind of expected + // since internally Object.getOwnPropertyNames is used, which does not + // guarantee a specific order of returned property names. + // On Android the order seems to be consistent + it.iosBroken('with same object twice', () => { + const obj = { + foo: 'bar', + foobar: 1, + func: function () {} + }; + util.format('%o %o', obj, obj).should.eql( + '{\n' + + ' foo: \'bar\',\n' + + ' foobar: 1,\n' + + ' func: [Function: func] {\n' + + ' [arguments]: null,\n' + + ' [caller]: null,\n' + + ' [length]: 0,\n' + + ' [name]: \'func\',\n' + + ' [prototype]: func { [constructor]: [Circular *1] }\n' + + ' }\n' + + '} {\n' + + ' foo: \'bar\',\n' + + ' foobar: 1,\n' + + ' func: [Function: func] {\n' + + ' [arguments]: null,\n' + + ' [caller]: null,\n' + + ' [length]: 0,\n' + + ' [name]: \'func\',\n' + + ' [prototype]: func { [constructor]: [Circular *1] }\n' + + ' }\n' + + '}' + ); + }); + }); + }); + + describe('#inspect()', () => { + it('is a function', () => { + util.inspect.should.be.a.Function; + }); + + it('handles string literal', () => { + util.inspect('a').should.eql('\'a\''); + }); + + it('handles number literal', () => { + util.inspect(1).should.eql('1'); + }); + + it('handles empty array', () => { + util.inspect([]).should.eql('[]'); + }); + + it('handles array with number values', () => { + util.inspect([ 1, 2, 3 ]).should.eql('[ 1, 2, 3 ]'); + }); + + it('handles array with mixed values', () => { + util.inspect([ 'a', 2 ]).should.eql('[ \'a\', 2 ]'); + }); + + it('handles sparse array', () => { + // eslint-disable-next-line no-sparse-arrays + util.inspect([ 1, , 3 ]).should.eql('[ 1, <1 empty item>, 3 ]'); + }); + + it('handles sparse array with multiple items missing in a row', () => { + // eslint-disable-next-line no-sparse-arrays + util.inspect([ 1,,,, 3 ]).should.eql('[ 1, <3 empty items>, 3 ]'); + }); + + it('handles sparse array with multiple separate gaps', () => { + // eslint-disable-next-line no-sparse-arrays + util.inspect([ 1,,,, 3, ,, 4 ]).should.eql('[ 1, <3 empty items>, 3, <2 empty items>, 4 ]'); + }); + + it('handles array with length > options.maxArrayLength', () => { + util.inspect([ 1, 2, 3 ], { maxArrayLength: 1 }).should.eql('[ 1, ... 2 more items ]'); + }); + + it('handles array with length > options.maxArrayLength and is sparse', () => { + // eslint-disable-next-line no-sparse-arrays + util.inspect([ 1,,,, 3, ,, 4 ], { maxArrayLength: 1 }).should.eql('[ 1, ... 7 more items ]'); + }); + + it('handles sparse array with length > options.maxArrayLength counting gaps as one item for length', () => { + // eslint-disable-next-line no-sparse-arrays + util.inspect([ 1,,,, ], { maxArrayLength: 2 }).should.eql('[ 1, <3 empty items> ]'); + // eslint-disable-next-line no-sparse-arrays + util.inspect([ 1,,,, 3, ,, 4 ], { maxArrayLength: 2 }).should.eql('[ 1, <3 empty items>, ... 4 more items ]'); + // eslint-disable-next-line no-sparse-arrays + util.inspect([ 1,,,, 3, ,, 4 ], { maxArrayLength: 3 }).should.eql('[ 1, <3 empty items>, 3, ... 3 more items ]'); + // eslint-disable-next-line no-sparse-arrays + util.inspect([ 1,,,, 3, ,, 4 ], { maxArrayLength: 4 }).should.eql('[ 1, <3 empty items>, 3, <2 empty items>, ... 1 more item ]'); + }); + + it('handles Regexp literal', () => { + util.inspect(/123/).should.eql('/123/'); + }); + + it('handles Regexp literal with flags', () => { + util.inspect(/123/ig).should.eql('/123/gi'); + }); + + it('handles new Regexp instance', () => { + util.inspect(new RegExp()).should.eql('/(?:)/'); + }); + + it('handles object primitive literal', () => { + util.inspect({}).should.eql('{}'); + }); + + it('handles new Object', () => { + // eslint-disable-next-line no-new-object + util.inspect(new Object()).should.eql('{}'); + }); + + it('handles Map instance', () => { + util.inspect(new Map()).should.eql('Map {}'); + }); + + it('handles Map instance with key/value pair', () => { + util.inspect(new Map([ [ 'a', 1 ] ])).should.eql('Map { \'a\' => 1 }'); + }); + + it('handles empty Set instance', () => { + util.inspect(new Set()).should.eql('Set {}'); + }); + + it('handles Set instance with number values', () => { + util.inspect(new Set([ 1, 2, 3 ])).should.eql('Set { 1, 2, 3 }'); + }); + + it('handles object with custom type tag', () => { + const baz = Object.create({}, { [Symbol.toStringTag]: { value: 'foo' } }); + util.inspect(baz).should.eql('Object [foo] {}'); + }); + + it('handles object with null prototype', () => { + const baz = Object.create(null, {}); + util.inspect(baz).should.eql('[Object: null prototype] {}'); + }); + + it('handles class instance', () => { + class Bar {} + util.inspect(new Bar()).should.eql('Bar {}'); + }); + + it('handles class instance with custom type tag', () => { + class Foo { + get [Symbol.toStringTag]() { + return 'bar'; + } + } + util.inspect(new Foo()).should.eql('Foo [bar] {}'); + }); + + it('handles empty function', () => { + util.inspect(function () {}).should.eql('[Function (anonymous)]'); + }); + + it('handles named function', () => { + util.inspect(function bar() {}).should.eql('[Function: bar]'); + }); + + it('handles arrow function', () => { + util.inspect(() => {}).should.eql('[Function (anonymous)]'); + }); + + it('handles function with custom property', () => { + const myFunc = () => {}; + myFunc.a = 1; + util.inspect(myFunc).should.eql('[Function: myFunc] { a: 1 }'); + }); + + it('handles object with getter property', () => { + const obj = {}; + // eslint-disable-next-line accessor-pairs + Object.defineProperty(obj, 'whatever', { get: () => 1, enumerable: true }); + util.inspect(obj).should.eql('{ whatever: [Getter] }'); + }); + + it('handles object with setter property', () => { + const obj = {}; + // eslint-disable-next-line accessor-pairs + Object.defineProperty(obj, 'whatever2', { set: () => {}, enumerable: true }); + util.inspect(obj).should.eql('{ whatever2: [Setter] }'); + }); + + it('handles object with getter/setter property', () => { + const obj = {}; + Object.defineProperty(obj, 'whatever3', { get: () => 1, set: () => {}, enumerable: true }); + util.inspect(obj).should.eql('{ whatever3: [Getter/Setter] }'); + }); + + it('handles object with property holding explicit undefined value', () => { + const obj = {}; + Object.defineProperty(obj, 'whatever4', { value: undefined, enumerable: true }); + util.inspect(obj).should.eql('{ whatever4: undefined }'); + }); + + it('with simple object', () => { + const obj = { + foo: 'bar' + }; + util.inspect(obj).should.eql('{ foo: \'bar\' }'); + }); + + it('with same object repeated in an array', () => { + const a = { id: 1 }; + util.inspect([ a, a ]).should.eql('[ { id: 1 }, { id: 1 } ]'); + }); + + it('with object', () => { + const obj = { + foo: 'bar', + foobar: 1, + func: function () {} + }; + // In Node 10+, we can sort the properties to ensure order to match, otherwise JSC/V8 return arguments/caller in different order on Functions + util.inspect(obj, { + showHidden: true, + breakLength: Infinity, + sorted: true + }).should.eql( + '{ foo: \'bar\', foobar: 1, func: [Function: func] { [arguments]: null, [caller]: null, [length]: 0, [name]: \'func\', [prototype]: func { [constructor]: [Circular *1] } } }' + ); + }); + + it('with nested object and infinite depth', () => { + const nestedObj2 = { + foo: 'bar', + foobar: 1, + func: [ { a: function () {} } ] + }; + + // In Node 10+, we can sort the properties to ensure order to match, otheerwise JSC/V8 return arguments/caller in different order on Functions + util.inspect(nestedObj2, { + showHidden: true, + breakLength: Infinity, + depth: Infinity, + sorted: true + }).should.eql( + '{\n' + + ' foo: \'bar\',\n' + + ' foobar: 1,\n' + + ' func: [\n' + + ' { a: [Function: a] { [arguments]: null, [caller]: null, [length]: 0, [name]: \'a\', [prototype]: a { [constructor]: [Circular *1] } } },\n' + + ' [length]: 1\n' + + ' ]\n' + + '}' + ); + }); + + it('with nested object and default depth', () => { + const nestedObj2 = { + foo: 'bar', + foobar: 1, + func: [ { a: function () {} } ] + }; + util.inspect(nestedObj2, { showHidden: true, breakLength: Infinity }).should.eql( + '{ foo: \'bar\', foobar: 1, func: [ { a: [Function] }, [length]: 1 ] }'); + }); + + it('with toplevel object that breaks and nested object that doesn\'t break', () => { + const nestedObj2 = { + foo: 'bar', + foobar: 1, + func: { + other: true, + yeah: 'man' + }, + something: 'else' + }; + util.inspect(nestedObj2).should.eql( + '{\n' + + ' foo: \'bar\',\n' + + ' foobar: 1,\n' + + ' func: { other: true, yeah: \'man\' },\n' + + ' something: \'else\'\n' + + '}'); + }); + + it('with toplevel and nested objects that break', () => { + const nestedObj2 = { + foo: 'bar', + foobar: 1, + func: { + other: true, + yeah: 'man', + whatever: '123456789', + whatever2: '123456789' + } + }; + util.inspect(nestedObj2).should.eql( + '{\n' + + ' foo: \'bar\',\n' + + ' foobar: 1,\n' + + ' func: {\n' + + ' other: true,\n' + + ' yeah: \'man\',\n' + + ' whatever: \'123456789\',\n' + + ' whatever2: \'123456789\'\n' + + ' }\n' + + '}' + ); + }); + + it('with nested object and empty options', () => { + const nestedObj2 = { + foo: 'bar', + foobar: 1, + func: [ { a: function () {} } ] + }; + util.inspect(nestedObj2, {}).should.eql( + '{ foo: \'bar\', foobar: 1, func: [ { a: [Function: a] } ] }'); + }); + + it('with default breakLength at exact break point', () => { + const obj = { + foo: '', + foobar: 1, + something: '1', + whatever: '', + whatever2: '', + whatever3: '' + }; + util.inspect(obj).should.eql('{\n foo: \'\',\n foobar: 1,\n something: \'1\',\n whatever: \'\',\n whatever2: \'\',\n whatever3: \'\'\n}'); + }); + + it('with default breakLength just below break point', () => { + const obj = { + foo: '', + foobar: 1, + something: '1', + whatever: '', + whatever2: '' + }; + util.inspect(obj).should.eql('{ foo: \'\', foobar: 1, something: \'1\', whatever: \'\', whatever2: \'\' }'); + }); + }); + + describe('#inherits()', () => { + it('is a function', () => { + util.inherits.should.be.a.Function; + }); + + it('hooks subclass to super constructor', (finished) => { + function BaseClass() { + this.listeners = {}; + } + + BaseClass.prototype.on = function (eventName, listener) { + const eventListeners = this.listeners[eventName] || []; + eventListeners.push(listener); + this.listeners[eventName] = eventListeners; + }; + + BaseClass.prototype.emit = function (eventName, data) { + const eventListeners = this.listeners[eventName] || []; + for (const listener of eventListeners) { + listener.call(this, data); + } + }; + + function MyStream() { + BaseClass.call(this); + } + + util.inherits(MyStream, BaseClass); + + MyStream.prototype.write = function (data) { + this.emit('data', data); + }; + + const stream = new MyStream(); + + should(stream instanceof BaseClass).eql(true); + should(MyStream.super_).eql(BaseClass); + + stream.on('data', data => { + data.should.eql('It works!'); + finished(); + }); + stream.write('It works!'); // Received data: "It works!" + }); + + it('throws TypeError if super constructor is null', () => { + function BaseClass() { + } + + function MyStream() { + BaseClass.call(this); + } + + should.throws(() => util.inherits(MyStream, null), + TypeError + ); + }); + + it('throws TypeError if constructor is null', () => { + function BaseClass() { + } + + should.throws(() => util.inherits(null, BaseClass), + TypeError + ); + }); + + it('throws TypeError if super constructor has no prototype', () => { + const BaseClass = Object.create(null, {}); + + function MyStream() { + BaseClass.call(this); + } + + should.throws(() => util.inherits(MyStream, BaseClass), + TypeError + ); + }); + }); + + describe('#isArray', () => { + it('should return true only if the given object is an Array', () => { + should.strictEqual(util.isArray([]), true); + should.strictEqual(util.isArray(Array()), true); + should.strictEqual(util.isArray(new Array()), true); + should.strictEqual(util.isArray(new Array(5)), true); + should.strictEqual(util.isArray(new Array('with', 'some', 'entries')), true); + should.strictEqual(util.isArray({}), false); + should.strictEqual(util.isArray({ push: function () {} }), false); + should.strictEqual(util.isArray(/regexp/), false); + should.strictEqual(util.isArray(new Error()), false); + should.strictEqual(util.isArray(Object.create(Array.prototype)), false); + }); + }); + + describe('#isRegExp', () => { + it('should return true only if the given object is a RegExp', () => { + should.strictEqual(util.isRegExp(/regexp/), true); + should.strictEqual(util.isRegExp(RegExp(), 'foo'), true); + should.strictEqual(util.isRegExp(new RegExp()), true); + should.strictEqual(util.isRegExp({}), false); + should.strictEqual(util.isRegExp([]), false); + should.strictEqual(util.isRegExp(new Date()), false); + should.strictEqual(util.isRegExp(Object.create(RegExp.prototype)), false); + }); + }); + + describe('#isDate', () => { + it('should return true only if the given object is a Date', () => { + should.strictEqual(util.isDate(new Date()), true); + should.strictEqual(util.isDate(new Date(0), 'foo'), true); + should.strictEqual(util.isDate(Date()), false); + should.strictEqual(util.isDate({}), false); + should.strictEqual(util.isDate([]), false); + should.strictEqual(util.isDate(new Error()), false); + should.strictEqual(util.isDate(Object.create(Date.prototype)), false); + }); + }); + + describe('#isError', () => { + it('should return true only if the given object is an Error', () => { + should.strictEqual(util.isError(new Error()), true); + should.strictEqual(util.isError(new TypeError()), true); + should.strictEqual(util.isError(new SyntaxError()), true); + should.strictEqual(util.isError({}), false); + should.strictEqual(util.isError({ name: 'Error', message: '' }), false); + should.strictEqual(util.isError([]), false); + should.strictEqual(util.isError(Object.create(Error.prototype)), true); + }); + }); + + describe('#isObject', () => { + it('should return true only if the given object is an Object', () => { + should.strictEqual(util.isObject({}), true); + should.strictEqual(util.isObject([]), true); + should.strictEqual(util.isObject(new Number(3)), true); + should.strictEqual(util.isObject(Number(4)), false); + should.strictEqual(util.isObject(1), false); + }); + }); + + describe('#isPrimitive', () => { + it('should return true only if the given object is a primitve', () => { + should.strictEqual(util.isPrimitive({}), false); + should.strictEqual(util.isPrimitive(new Error()), false); + should.strictEqual(util.isPrimitive(new Date()), false); + should.strictEqual(util.isPrimitive([]), false); + should.strictEqual(util.isPrimitive(/regexp/), false); + should.strictEqual(util.isPrimitive(function () {}), false); + should.strictEqual(util.isPrimitive(new Number(1)), false); + should.strictEqual(util.isPrimitive(new String('bla')), false); + should.strictEqual(util.isPrimitive(new Boolean(true)), false); + should.strictEqual(util.isPrimitive(1), true); + should.strictEqual(util.isPrimitive('bla'), true); + should.strictEqual(util.isPrimitive(true), true); + should.strictEqual(util.isPrimitive(undefined), true); + should.strictEqual(util.isPrimitive(null), true); + should.strictEqual(util.isPrimitive(Infinity), true); + should.strictEqual(util.isPrimitive(NaN), true); + should.strictEqual(util.isPrimitive(Symbol('symbol')), true); + }); + }); + + describe('#isBuffer', () => { + it('should return true only if the given object is a Buffer', () => { + should.strictEqual(util.isBuffer('foo'), false); + should.strictEqual(util.isBuffer(Buffer.from('foo')), true); + }); + }); + + describe('#promisify()', () => { + it('is a function', () => { + util.promisify.should.be.a.Function; + }); + + it('wraps callback function to return promise with resolve', (finished) => { + function callbackOriginal(argOne, argTwo, next) { + next(argOne, argTwo); + } + const promisified = util.promisify(callbackOriginal); + const result = promisified(null, 123); + should(result instanceof Promise).eql(true); + result.then(value => { // eslint-disable-line promise/always-return + should(value).eql(123); + finished(); + }).catch(err => finished(err)); + }); + + it('wraps callback function to return promise with rejection', (finished) => { + function callbackOriginal(argOne, argTwo, next) { + next(argOne, argTwo); + } + const promisified = util.promisify(callbackOriginal); + const result = promisified(new Error('example'), 123); + should(result instanceof Promise).eql(true); + result.then(value => { // eslint-disable-line promise/always-return + should(value).eql(123); + finished(new Error('Expected promise to get rejected!')); + }).catch(err => { + err.message.should.eql('example'); + finished(); + }); + }); + + it('throws TypeError if original argument is not a function', () => { + should.throws(() => util.promisify({}), + TypeError + ); + }); + }); + + describe('#callbackify()', () => { + it('is a function', () => { + util.callbackify.should.be.a.Function; + }); + + it('wraps function returning Promise to return function accepting callback (with success)', (finished) => { + function original(argOne) { + return Promise.resolve(argOne); + } + const callbackified = util.callbackify(original); + callbackified(23, (err, result) => { + try { + should(err).not.be.ok; + should(result).eql(23); + finished(); + } catch (e) { + finished(e); + } + }); + }); + + it('wraps function returning Promise to return function accepting callback (with error)', (finished) => { + function original(argOne) { + return Promise.reject(argOne); + } + const callbackified = util.callbackify(original); + callbackified(new Error('expected this'), (err, result) => { + try { + should(err).be.ok; + should(result).not.be.ok; + finished(); + } catch (e) { + finished(e); + } + }); + }); + + it('handles special case of falsy rejection', (finished) => { + function original() { + return Promise.reject(null); + } + const callbackified = util.callbackify(original); + callbackified((err, result) => { + try { + should(err).be.ok; + should(err instanceof Error).eql(true); + should(err.reason).eql(null); + finished(); + } catch (e) { + finished(e); + } + }); + }); + + it('throws TypeError if original argument is not a function', () => { + should.throws(() => util.callbackify({}), + TypeError + ); + }); + }); + + describe('#deprecate()', () => { + it('is a function', () => { + util.deprecate.should.be.a.Function; + }); + + it('wraps function to emit warning', () => { + function original(...args) { + return args; + } + const deprecated = util.deprecate(original, 'dont call me Al'); + // this should get called synchronously, so I don't think we need to do any setTimeout/async finished stuff + process.on('warning', warning => { + warning.name.should.eql('DeprecationWarning'); + warning.message.should.eql('dont call me Al'); + }); + const result = deprecated(null, 123); + should(result).eql([ null, 123 ]); + }); + + // TODO: Test that we return original function if process.noDeprecation is true! + }); + + describe('#log()', () => { + it('is a function', () => { + util.log.should.be.a.Function; + }); + + it('prepends timestamp to message', () => { + // Hijack console.log! NOTE: This doesn't work on iOS until we move to obj-c API! + const original = console.log; + try { + console.log = string => { + string.should.match(/^\d{1,2} \w{3} \d{2}:\d{2}:\d{2} - message$/); + }; + util.log('message'); + } finally { + console.log = original; + } + }); + }); + + describe('#print()', () => { + it('is a function', () => { + util.print.should.be.a.Function; + }); + + it('concatenates with no join', () => { + // Hijack console.log! NOTE: This doesn't work on iOS until we move to obj-c API! + const original = console.log; + try { + console.log = string => { + string.should.eql('123'); + }; + util.print(1, 2, 3); + } finally { + console.log = original; + } + }); + }); + + describe('#puts()', () => { + it('is a function', () => { + util.puts.should.be.a.Function; + }); + + it('concatenates with newline join', () => { + // Hijack console.log! NOTE: This doesn't work on iOS until we move to obj-c API! + const original = console.log; + try { + console.log = string => { + string.should.eql('1\n2\n3'); + }; + util.puts(1, 2, 3); + } finally { + console.log = original; + } + }); + }); + + describe('#debug()', () => { + it('is a function', () => { + util.debug.should.be.a.Function; + }); + + it('concatenates with newline join', () => { + // Hijack console.error! NOTE: This doesn't work on iOS until we move to obj-c API! + const original = console.error; + try { + console.error = string => { + string.should.eql('DEBUG: message'); + }; + util.debug('message'); + } finally { + console.error = original; + } + }); + }); + + describe('#error()', () => { + it('is a function', () => { + util.error.should.be.a.Function; + }); + + it('concatenates with newline join', () => { + // Hijack console.error! NOTE: This doesn't work on iOS until we move to obj-c API! + const original = console.error; + try { + console.error = string => { + string.should.eql('1\n2\n3'); + }; + util.error(1, 2, 3); + } finally { + console.error = original; + } + }); + }); + + describe('.types', () => { + describe('#isAnyArrayBuffer()', () => { + it('should return true for built-in ArrayBuffer', () => { + const ab = new ArrayBuffer(); + util.types.isAnyArrayBuffer(ab).should.be.true; + }); + + it.skip('should return true for built-in SharedArrayBuffer', () => { + // SharedArrayBuffer is disabled in all major JS engines due to Spectre & Meltrdown vulnerabilities + }); + + it('should return false for other values', () => { + util.types.isAnyArrayBuffer({}).should.be.false; + util.types.isAnyArrayBuffer(new Float32Array()).should.be.false; + }); + }); + + describe('#isArgumentsObject()', () => { + it('should return true for function arguments object', () => { + (function () { + util.types.isArgumentsObject(arguments).should.be.true; + }()); + }); + + it('should return false for other values', () => { + util.types.isArgumentsObject([]).should.be.false + util.types.isArgumentsObject({ [Symbol.toStringTag]: 'Arguments' }).should.be.false; + }); + }); + + describe('#isArrayBuffer()', () => { + it('should return true for built-in ArrayBuffer instance', () => { + const ab = new ArrayBuffer(); + util.types.isArrayBuffer(ab).should.be.true; + }); + + it('should return false for other values', () => { + util.types.isArrayBuffer([]).should.be.false; + util.types.isArrayBuffer(new Float32Array()).should.be.false; + }); + }); + + describe('#isAsyncFunction()', () => { + it('should return true for async functions', () => { + util.types.isAsyncFunction(async () => {}).should.be.true; + }) + + it('should return false for normal functions', () => { + util.types.isAsyncFunction(() => {}).should.be.true; + }) + }); + + describe('#isNativeError()', () => { + it('is a function', () => { + util.types.isNativeError.should.be.a.Function; + }); + + it('returns true for Error instance', () => { + util.types.isNativeError(new Error()).should.eql(true); + }); + + it('returns true for EvalError instance', () => { + util.types.isNativeError(new EvalError()).should.eql(true); + }); + + it('returns true for RangeError instance', () => { + util.types.isNativeError(new RangeError()).should.eql(true); + }); + + it('returns true for ReferenceError instance', () => { + util.types.isNativeError(new ReferenceError()).should.eql(true); + }); + + it('returns true for SyntaxError instance', () => { + util.types.isNativeError(new SyntaxError()).should.eql(true); + }); + + it('returns true for TypeError instance', () => { + util.types.isNativeError(new TypeError()).should.eql(true); + }); + + it('returns true for URIError instance', () => { + util.types.isNativeError(new URIError()).should.eql(true); + }); + + it('returns false for custom Error subclass', () => { + class SubError extends Error {} + util.types.isNativeError(new SubError()).should.eql(false); + }); + }); + + describe('#isNumberObject()', () => { + it('is a function', () => { + util.types.isNumberObject.should.be.a.Function; + }); + + it('returns true for boxed Number', () => { + // eslint-disable-next-line no-new-wrappers + util.types.isNumberObject(new Number()).should.eql(true); + }); + + it('returns false for primitive Number', () => { + util.types.isNumberObject(0).should.eql(false); + }); + }); + + describe('#isStringObject()', () => { + it('is a function', () => { + util.types.isStringObject.should.be.a.Function; + }); + + it('returns true for boxed String', () => { + // eslint-disable-next-line no-new-wrappers + util.types.isStringObject(new String('foo')).should.eql(true); + }); + + it('returns false for primitive String', () => { + util.types.isStringObject('foo').should.eql(false); + }); + }); + + describe('#isBooleanObject()', () => { + it('is a function', () => { + util.types.isBooleanObject.should.be.a.Function; + }); + + it('returns true for boxed Boolean', () => { + // eslint-disable-next-line no-new-wrappers + util.types.isBooleanObject(new Boolean(false)).should.eql(true); + }); + + it('returns false for primitive Boolean', () => { + util.types.isBooleanObject(true).should.eql(false); + }); + }); + + // TODO: Re-enable when we have BigInt support + // describe('#isBigIntObject()', () => { + // it('is a function', () => { + // util.types.isBigIntObject.should.be.a.Function; + // }); + + // it('returns true for boxed BigInt', () => { + // // eslint-disable-next-line no-new-wrappers,no-undef + // util.types.isSymbolObject(Object(BigInt(9007199254740991))).should.eql(true); + // }); + + // it('returns false for BigInt instance', () => { + // // eslint-disable-next-line no-undef + // util.types.isSymbolObject(BigInt(9007199254740991)).should.eql(false); + // }); + + // it('returns false for primitive BigInt', () => { + // util.types.isSymbolObject(9007199254740991n).should.eql(false); + // }); + // }); + + describe('#isSymbolObject()', () => { + it('is a function', () => { + util.types.isSymbolObject.should.be.a.Function; + }); + + it('returns true for boxed Symbol', () => { + // eslint-disable-next-line no-new-wrappers + util.types.isSymbolObject(Object(Symbol('foo'))).should.eql(true); + }); + + it('returns false for primitive Symbol', () => { + util.types.isSymbolObject(Symbol('foo')).should.eql(false); + }); + }); + + describe('#isBoxedPrimitive()', () => { + it('is a function', () => { + util.types.isBoxedPrimitive.should.be.a.Function; + }); + + it('returns false for primitive Boolean', () => { + util.types.isBoxedPrimitive(false).should.eql(false); + }); + + it('returns true for boxed Boolean', () => { + // eslint-disable-next-line no-new-wrappers + util.types.isBoxedPrimitive(new Boolean(false)).should.eql(true); + }); + + it('returns false for primitive Symbol', () => { + util.types.isBoxedPrimitive(Symbol('foo')).should.eql(false); + }); + + it('returns true for boxed Symbol', () => { + util.types.isBoxedPrimitive(Object(Symbol('foo'))).should.eql(true); + }); + + // it('returns true for boxed BigInt', () => { + // // eslint-disable-next-line no-undef + // util.types.isBoxedPrimitive(Object(BigInt(5))).should.eql(true); + // }); + }); + + describe('#isSet()', () => { + it('is a function', () => { + util.types.isSet.should.be.a.Function; + }); + + it('returns true for Set instance', () => { + util.types.isSet(new Set()).should.eql(true); + }); + }); + + describe('#isSetIterator()', () => { + it('should return true if the value is an iterator returned for a built-in Set instance', () => { + const set = new Set(); + util.types.isSetIterator(set.keys()).should.be.true; + util.types.isSetIterator(set.values()).should.be.true; + util.types.isSetIterator(set.entries()).should.be.true; + util.types.isSetIterator(set[Symbol.iterator]()).should.be.true; + }); + + it('should return false for other iterators', () => { + const map = new Map(); + util.types.isSetIterator(map.values()).should.be.false; + }); + }); + + describe('#isMap()', () => { + it('is a function', () => { + util.types.isMap.should.be.a.Function; + }); + + it('returns true for Map instance', () => { + util.types.isMap(new Map()).should.eql(true); + }); + }); + + describe('#isMapIterator()', () => { + it('should return true if the value is an iterator retunred for a built-in Map instance', () => { + const map = new Map(); + util.types.isMapIterator(map.keys()).should.be.true; + util.types.isMapIterator(map.values()).should.be.true; + util.types.isMapIterator(map.entries()).should.be.true; + util.types.isMapIterator(map[Symbol.iterator]()).should.be.true; + }) + + it('should return false for other iterators', () => { + const set = new Set(); + util.types.isMapIterator(set.values()).should.be.false; + }); + }); + + describe('#isDataView()', () => { + const ab = new ArrayBuffer(20); + + it('should return true for built-in DataView instance', () => { + util.types.isDataView(new DataView(ab)).should.be.true; + }) + + it('should return false for typed array instance', () => { + util.types.isDataView(new Float64Array()).should.be.false + }); + }); + + describe('#isDate()', () => { + it('is a function', () => { + util.types.isDate.should.be.a.Function; + }); + + it('returns true for built-in Date instance', () => { + util.types.isDate(new Date()).should.eql(true); + }); + }); + + describe('#isPromise()', () => { + it('should return true for built-in Promise', () => { + util.types.isPromise(Promise.resolve(42)).should.be.true; + }); + + it('should return false for Promise like objects', () => { + util.types.isPromise({ then: () => {}, catch: () => {} }).should.be.false; + }) + }); + + describe('#isRegExp()', () => { + it('is a function', () => { + util.types.isRegExp.should.be.a.Function; + }); + + it('returns true for RegExp instance', () => { + util.types.isRegExp(/abc/).should.eql(true); + }); + + it('returns true for RegExp primitive', () => { + util.types.isRegExp(new RegExp('abc')).should.eql(true); + }); + }); + + describe('#isGeneratorFunction()', () => { + it('should return true for generator function', () => { + util.types.isGeneratorFunction(function* foo() {}).should.be.true; + }); + + it('should return false for normal function', () => { + util.types.isGeneratorFunction(function foo() {}).should.be.false; + }) + }); + + describe('#isGeneratorObject()', () => { + it('should return true for generator object', () => { + function* foo() {} + const generator = foo(); + util.types.isGeneratorObject(generator).should.be.true; + }) + + it('should return false for any other object', () => { + util.types.isGeneratorObject({}).should.be.false; + }) + }); + + describe('Typed Arrays', () => { + it('should correctly check typed arrays', () => { + should(!util.types.isUint8Array({ [Symbol.toStringTag]: 'Uint8Array' })).be.true; + should(util.types.isUint8Array(new Uint8Array())).be.true; + + should(!util.types.isUint8ClampedArray({ [Symbol.toStringTag]: 'Uint8ClampedArray' })).be.true; + should(util.types.isUint8ClampedArray(new Uint8ClampedArray())).be.true; + + should(!util.types.isUint16Array({ [Symbol.toStringTag]: 'Uint16Array' })).be.true; + should(util.types.isUint16Array(new Uint16Array())).be.true; + + should(!util.types.isUint32Array({ [Symbol.toStringTag]: 'Uint32Array' })).be.true; + should(util.types.isUint32Array(new Uint32Array())).be.true; + + should(!util.types.isInt8Array({ [Symbol.toStringTag]: 'Int8Array' })).be.true; + should(util.types.isInt8Array(new Int8Array())).be.true; + + should(!util.types.isInt16Array({ [Symbol.toStringTag]: 'Int16Array' })).be.true; + should(util.types.isInt16Array(new Int16Array())).be.true; + + should(!util.types.isInt32Array({ [Symbol.toStringTag]: 'Int32Array' })).be.true; + should(util.types.isInt32Array(new Int32Array())).be.true; + + should(!util.types.isFloat32Array({ [Symbol.toStringTag]: 'Float32Array' })).be.true; + should(util.types.isFloat32Array(new Float32Array())).be.true; + + should(!util.types.isFloat64Array({ [Symbol.toStringTag]: 'Float64Array' })).be.true; + should(util.types.isFloat64Array(new Float64Array())).be.true; + + /* + @todo enable when we have BigInt64 support + should(!util.types.isBigInt64Array({ [Symbol.toStringTag]: 'BigInt64Array' })).be.true; + should(util.types.isBigInt64Array(new BigInt64Array)).be.true; + + should(!util.types.isBigUint64Array({ [Symbol.toStringTag]: 'BigUint64Array' })).be.true; + should(util.types.isBigUint64Array(new BigUint64Array)).be.true; + */ + }); + }); + }); +});