UNPKG

bmocha

Version:

Alternative implementation of mocha

840 lines (647 loc) 16 kB
/*! * inspect.js - inspect implementation * Copyright (c) 2018-2019, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bmocha */ 'use strict'; /* * Globals */ const { Array, ArrayBuffer, Buffer, Date, Error, Map, Object, RegExp, Set, SharedArrayBuffer, Uint8Array } = global; /* * Constants */ const HAS_SHARED_ARRAY_BUFFER = typeof SharedArrayBuffer === 'function'; const colors = { __proto__: null, bigint: 33, // yellow boolean: 33, // yellow date: 35, // magenta func: 36, // cyan nil: 1, // bold number: 33, // yellow regexp: 31, // red string: 32, // green symbol: 32, // green undef: 90, // bright black unknown: 90 // bright black }; /* * Inspector */ class Inspector { constructor(options) { this.seen = new Set(); this.single = false; this.maxArrayLength = 512; this.showHidden = false; this.sort = false; this.colors = false; this.init(options); } /* * Helpers */ init(options) { if (typeof options === 'boolean') options = { showHidden: options }; else if (typeof options === 'number') options = { maxArrayLength: options }; if (options == null || typeof options !== 'object') options = {}; if (options.showHidden != null) this.showHidden = Boolean(options.showHidden); if (options.maxArrayLength != null) this.maxArrayLength = options.maxArrayLength >>> 0; if (options.sort != null) this.sort = Boolean(options.sort); if (options.colors != null) this.colors = Boolean(options.colors); return this; } color(name, str) { if (!this.colors) return str; return `\x1b[${colors[name] >>> 0}m${str}\x1b[0m`; } /* * Primitives */ bigint(value, prefix) { return prefix + this.color('bigint', `${value}n`); } boolean(value, prefix) { return prefix + this.color('boolean', `${value}`); } nil(value, prefix) { return prefix + this.color('nil', 'null'); } number(value, prefix) { if (value === 0 && 1 / value === -Infinity) value = '-0'; return prefix + this.color('number', `${value}`); } string(value, prefix) { value = JSON.stringify(value).slice(1, -1); value = value.replace(/\\"/g, '"'); value = value.replace(/'/g, '\\\''); return prefix + this.color('string', `'${value}'`); } symbol(value, prefix) { return prefix + this.color('symbol', toString(value)); } undef(value, prefix) { return prefix + this.color('undef', 'undefined'); } unknown(value, prefix) { return prefix + this.color('unknown', `[${typeof value}]`); } /* * Objects */ *entries(obj, prefix) { let total = 0; let count = 0; if (isArrayLike(obj)) { if (!isBuffer(obj) && !isUint8Array(obj)) { const length = get(obj, 'length') >>> 0; total += length; for (let i = 0; i < length; i++) { if (count >= this.maxArrayLength) break; count += 1; yield [null, obj[i], false]; } } } else if (isSet(obj)) { total += get(obj, 'size') >>> 0; for (const key of iterate(obj)) { if (count >= this.maxArrayLength) break; count += 1; yield [null, key, false]; } } else if (isMap(obj)) { total += get(obj, 'size') >>> 0; for (const pair of iterate(obj)) { let key, value; try { // Can throw on child class. [key, value] = pair; } catch (e) { continue; } if (count >= this.maxArrayLength) break; count += 1; key = this.stringify(key, prefix + ' '); key = key.substring(prefix.length + 2); yield [key, value, false]; } } const keys = getKeys(obj, this.showHidden); const symbols = getSymbols(obj, this.showHidden); if (isError(obj) && !isSimpleError(obj)) { if (!keys.includes('name')) keys.push('name'); if (!keys.includes('message')) keys.push('message'); } if (this.sort) { keys.sort(); symbols.sort(symbolCompare); } for (let i = 0; i < keys.length; i++) yield this.property(obj, keys[i]); for (let i = 0; i < symbols.length; i++) yield this.property(obj, symbols[i]); if (count < total) yield [null, `... ${total - count} more items`, true]; } property(obj, key) { let desc = getOwnPropertyDescriptor(obj, key); if (desc == null) { desc = { value: get(obj, key), get: null, set: null }; } if (typeof key === 'symbol') { key = `[${this.symbol(key, '')}]`; } else { key = !isKey(key) ? this.string(key, '') : key; } // Might be an evil proxy. const get_ = get(desc, 'get'); const set_ = get(desc, 'set'); if (get_ && set_) return [key, this.color('func', '[Getter/Setter]'), true]; if (get_) return [key, this.color('func', '[Getter]'), true]; if (set_) return [key, this.color('func', '[Setter]'), true]; return [key, get(desc, 'value'), false]; } key(value, prefix) { this.single = true; try { return this.stringify(value, prefix); } finally { this.single = false; } } values(name, brackets, obj, prefix) { if (this.single) return name || `[${objectName(obj)}]`; const [open, close] = brackets; let str = prefix; let has = false; if (name) str += name + ' '; str += open; str += '\n'; this.seen.add(obj); for (const [key, value, raw] of this.entries(obj, prefix)) { let line = value; if (!raw) { line = this.stringify(value, prefix + ' '); line = line.substring(prefix.length + 2); } str += prefix + ' '; if (key != null) str += key + ': '; str += line; str += ','; str += '\n'; has = true; } this.seen.delete(obj); if (has) { str = str.slice(0, -2); str += '\n'; str += prefix; str += close; } else { if (name) { str = prefix + name; } else { str = str.slice(0, -1); str += close; } } return str; } args(obj, prefix) { return this.values('[Arguments]', '[]', obj, prefix); } array(obj, prefix) { return this.values(null, '[]', obj, prefix); } arrayBuffer(obj, prefix) { let buffer; try { buffer = Buffer.from(obj, 0, obj.byteLength); } catch (e) { return this.object(obj, prefix); } const name = `[${objectType(obj)}: ${toHex(buffer, this.maxArrayLength)}]`; return this.values(name, '{}', obj, prefix); } buffer(obj, prefix) { let name; try { name = `[Buffer: ${toHex(obj, this.maxArrayLength)}]`; } catch (e) { return this.object(obj, prefix); } return this.values(name, '{}', obj, prefix); } circular(obj, prefix) { return prefix + this.color('func', '[Circular]'); } date(obj, prefix) { let name; try { name = `[${obj.toISOString()}]`; } catch (e) { name = `[${toString(obj)}]`; } name = this.color('date', name); return this.values(name, '{}', obj, prefix); } error(obj, prefix) { let name; if (isSimpleError(obj)) { try { name = `[${obj.name}: ${obj.message}]`; } catch (e) { ; } } if (name == null) name = `[${objectName(obj)}]`; return this.values(name, '{}', obj, prefix); } func(obj, prefix) { let name = `[${funcName(obj)}]`; name = this.color('func', name); return this.values(name, '{}', obj, prefix); } map(obj, prefix) { return this.values('[Map]', '{}', obj, prefix); } object(obj, prefix) { let name = `[${objectName(obj)}]`; if (name === '[Object]') name = null; return this.values(name, '{}', obj, prefix); } regexp(obj, prefix) { const name = this.color('regexp', toString(obj)); return this.values(name, '{}', obj, prefix); } set(obj, prefix) { return this.values('[Set]', '[]', obj, prefix); } uint8array(obj, prefix) { let buffer; try { buffer = Buffer.from(obj.buffer, obj.byteOffset, obj.byteLength); } catch (e) { return this.object(obj, prefix); } const name = `[Uint8Array: ${toHex(buffer, this.maxArrayLength)}]`; return this.values(name, '{}', obj, prefix); } view(obj, prefix) { const name = `[${objectName(obj)}]`; return this.values(name, '[]', obj, prefix); } /* * Stringification */ stringify(value, prefix = '') { if (this.seen.has(value)) return this.circular(value, prefix); switch (typeof value) { case 'undefined': return this.undef(value, prefix); case 'object': if (value === null) return this.nil(value, prefix); if (isArguments(value)) return this.args(value, prefix); if (isArray(value)) return this.array(value, prefix); if (isMap(value)) return this.map(value, prefix); if (isSet(value)) return this.set(value, prefix); if (isBuffer(value)) return this.buffer(value, prefix); if (isDate(value)) return this.date(value, prefix); if (isRegExp(value)) return this.regexp(value, prefix); if (isError(value)) return this.error(value, prefix); if (isArrayBuffer(value)) return this.arrayBuffer(value, prefix); if (isUint8Array(value)) return this.uint8array(value, prefix); if (isView(value)) return this.view(value, prefix); return this.object(value, prefix); case 'boolean': return this.boolean(value, prefix); case 'number': return this.number(value, prefix); case 'string': return this.string(value, prefix); case 'symbol': return this.symbol(value, prefix); case 'function': return this.func(value, prefix); case 'bigint': return this.bigint(value, prefix); default: return this.unknown(value, prefix); } } } /* * API */ function inspect(value, options) { const inspector = new Inspector(options); try { return inspector.stringify(value, ''); } catch (e) { // Last line of defense. return `[${objectType(value)}]`; } } inspect.log = function log(value, options) { console.log(inspect(value, options)); }; inspect.single = function single(value, options) { const inspector = new Inspector(options); try { return inspector.key(value, ''); } catch (e) { // Last line of defense. return `[${objectType(value)}]`; } }; inspect.type = function type(value) { const type = typeof value; if (type === 'object') return objectType(value).toLowerCase(); return type; }; /* * Helpers */ function objectString(obj) { if (obj === undefined) return '[object Undefined]'; if (obj === null) return '[object Null]'; try { // Not sure if this can throw. return Object.prototype.toString.call(obj); } catch (e) { return '[object Object]'; } } function objectType(obj) { return objectString(obj).slice(8, -1); } function objectName(value) { const type = objectType(value); if (value == null) return type; if (type !== 'Object' && type !== 'Error') return type; const ctor = get(value, 'constructor'); if (ctor == null) return type; const name = get(ctor, 'name'); if (typeof name !== 'string' || name.length === 0) return type; return name; } function funcName(func) { const name = get(func, 'name'); if (typeof name !== 'string' || name.length === 0) return 'Function'; return `Function: ${name}`; } function isKey(key) { return key.length > 0 && !/[^\$\w]/.test(key) && !/^\d/.test(key); } function symbolCompare(a, b) { return String(a).localeCompare(String(b)); } function isArguments(obj) { return objectString(obj) === '[object Arguments]'; } function isArray(obj) { try { return Array.isArray(obj); } catch (e) { return false; } } function isArrayBuffer(obj) { if (HAS_SHARED_ARRAY_BUFFER) { if (instanceOf(obj, SharedArrayBuffer)) return true; } return instanceOf(obj, ArrayBuffer); } function isArrayLike(obj) { return isArray(obj) || isView(obj) || isArguments(obj); } function isBuffer(obj) { try { return Buffer.isBuffer(obj); } catch (e) { return false; } } function isDate(obj) { return instanceOf(obj, Date); } function isError(obj) { return instanceOf(obj, Error); } function isSimpleError(obj) { if (!isError(obj)) return false; const name = get(obj, 'name'); const message = get(obj, 'message'); return typeof name === 'string' && name.length > 0 && !name.includes('\n') && typeof message === 'string' && message.length > 0 && !message.includes('\n'); } function isMap(obj) { return instanceOf(obj, Map); } function isRegExp(obj) { return instanceOf(obj, RegExp); } function isSet(obj) { return instanceOf(obj, Set); } function isUint8Array(obj) { return instanceOf(obj, Uint8Array); } function isView(obj) { try { return ArrayBuffer.isView(obj); } catch (e) { return false; } } function isIndex(key, length) { if (key === '0') return length > 0; return /^[1-9]\d{0,14}$/.test(key) && Number(key) < length; } function getKeys(obj, showHidden) { if (isView(obj)) return []; const keys = showHidden ? getOwnPropertyNames(obj) : getOwnKeys(obj); // Defend against weird proxy. if (!isArray(keys)) return []; if (isArrayLike(obj)) { const length = get(obj, 'length') >>> 0; return keys.filter(key => !isIndex(key, length)); } return keys; } function getSymbols(obj, showHidden) { const symbols = getOwnPropertySymbols(obj); // Defend against weird proxy. if (!isArray(symbols)) return []; if (showHidden) return symbols; const out = []; for (const symbol of symbols) { if (isEnumerable(obj, symbol)) out.push(symbol); } return out; } function toHex(buf, max) { if (buf.length > max) { const hex = buf.toString('hex', 0, max); const left = buf.length - max; return `${hex} ... ${left} more bytes`; } return buf.toString('hex'); } /* * Safety */ function instanceOf(obj, ctor) { // Can throw on proxy. try { return obj instanceof ctor; } catch (e) { return false; } } function get(obj, prop) { // Can throw on proxy/getter. try { return obj[prop]; } catch (e) { return undefined; } } function getOwnKeys(obj) { // Can throw on proxy. try { return Object.keys(obj); } catch (e) { return []; } } function getOwnPropertyDescriptor(obj, prop) { // Can throw on proxy. try { return Object.getOwnPropertyDescriptor(obj, prop); } catch (e) { return undefined; } } function isEnumerable(obj, prop) { const desc = getOwnPropertyDescriptor(obj, prop); if (!desc) return false; return get(desc, 'enumerable') === true; } function getOwnPropertyNames(obj) { // Can throw on proxy. try { return Object.getOwnPropertyNames(obj); } catch (e) { return []; } } function getOwnPropertySymbols(obj) { // Can throw on proxy. try { return Object.getOwnPropertySymbols(obj); } catch (e) { return []; } } function* iterate(obj) { // Can throw on child class. try { for (const item of obj) yield item; } catch (e) { ; } } function toString(obj) { // Can throw on null proto // and who knows what else. try { return String(obj); } catch (e) { return 'Object'; } } /* * Expose */ module.exports = inspect;