diff options
author | Florian Dold <florian.dold@gmail.com> | 2017-08-14 05:01:11 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2017-08-14 05:02:09 +0200 |
commit | 363723fc84f7b8477592e0105aeb331ec9a017af (patch) | |
tree | 29f92724f34131bac64d6a318dd7e30612e631c7 /node_modules/concordance/lib | |
parent | 5634e77ad96bfe1818f6b6ee70b7379652e5487f (diff) |
node_modules
Diffstat (limited to 'node_modules/concordance/lib')
47 files changed, 5398 insertions, 0 deletions
diff --git a/node_modules/concordance/lib/Circular.js b/node_modules/concordance/lib/Circular.js new file mode 100644 index 000000000..c5c020267 --- /dev/null +++ b/node_modules/concordance/lib/Circular.js @@ -0,0 +1,35 @@ +'use strict' + +class Circular { + constructor () { + this.stack = new Map() + } + + add (descriptor) { + if (this.stack.has(descriptor)) throw new Error('Already in stack') + + if (descriptor.isItem !== true && descriptor.isMapEntry !== true && descriptor.isProperty !== true) { + this.stack.set(descriptor, this.stack.size + 1) + } + return this + } + + delete (descriptor) { + if (this.stack.has(descriptor)) { + if (this.stack.get(descriptor) !== this.stack.size) throw new Error('Not on top of stack') + this.stack.delete(descriptor) + } + return this + } + + has (descriptor) { + return this.stack.has(descriptor) + } + + get (descriptor) { + return this.stack.has(descriptor) + ? this.stack.get(descriptor) + : 0 + } +} +module.exports = Circular diff --git a/node_modules/concordance/lib/Indenter.js b/node_modules/concordance/lib/Indenter.js new file mode 100644 index 000000000..deeca842e --- /dev/null +++ b/node_modules/concordance/lib/Indenter.js @@ -0,0 +1,22 @@ +'use strict' + +class Indenter { + constructor (level, step) { + this.level = level + this.step = step + this.value = step.repeat(level) + } + + increase () { + return new Indenter(this.level + 1, this.step) + } + + decrease () { + return new Indenter(this.level - 1, this.step) + } + + toString () { + return this.value + } +} +module.exports = Indenter diff --git a/node_modules/concordance/lib/Registry.js b/node_modules/concordance/lib/Registry.js new file mode 100644 index 000000000..f4a4b6808 --- /dev/null +++ b/node_modules/concordance/lib/Registry.js @@ -0,0 +1,24 @@ +'use strict' + +class Registry { + constructor () { + this.counter = 0 + this.map = new WeakMap() + } + + has (value) { + return this.map.has(value) + } + + get (value) { + return this.map.get(value).descriptor + } + + alloc (value) { + const index = ++this.counter + const pointer = {descriptor: null, index} + this.map.set(value, pointer) + return pointer + } +} +module.exports = Registry diff --git a/node_modules/concordance/lib/compare.js b/node_modules/concordance/lib/compare.js new file mode 100644 index 000000000..7e24228e6 --- /dev/null +++ b/node_modules/concordance/lib/compare.js @@ -0,0 +1,103 @@ +'use strict' + +const constants = require('./constants') +const describe = require('./describe') +const recursorUtils = require('./recursorUtils') +const shouldCompareDeep = require('./shouldCompareDeep') +const symbolProperties = require('./symbolProperties') +const Circular = require('./Circular') + +const AMBIGUOUS = constants.AMBIGUOUS +const DEEP_EQUAL = constants.DEEP_EQUAL +const UNEQUAL = constants.UNEQUAL + +function shortcircuitPrimitive (value) { + if (value === null || value === undefined || value === true || value === false) return true + + const type = typeof value + if (type === 'string' || type === 'symbol') return true + // Don't shortcircuit NaN values + if (type === 'number') return !isNaN(value) + + return false +} + +function compareDescriptors (lhs, rhs) { + const lhsCircular = new Circular() + const rhsCircular = new Circular() + + const lhsStack = [] + const rhsStack = [] + let topIndex = -1 + + do { + let result + if (lhsCircular.has(lhs)) { + result = lhsCircular.get(lhs) === rhsCircular.get(rhs) + ? DEEP_EQUAL + : UNEQUAL + } else if (rhsCircular.has(rhs)) { + result = UNEQUAL + } else { + result = lhs.compare(rhs) + } + + if (result === UNEQUAL) return false + if (result !== DEEP_EQUAL) { + if (!shouldCompareDeep(result, lhs, rhs)) return false + + if (result === AMBIGUOUS && lhs.isProperty === true) { + // Replace both sides by a pseudo-descriptor which collects symbol + // properties instead. + lhs = new symbolProperties.Collector(lhs, lhsStack[topIndex].recursor) + rhs = new symbolProperties.Collector(rhs, rhsStack[topIndex].recursor) + // Replace the current recursors so they can continue correctly after + // the collectors have been "compared". This is necessary since the + // collectors eat the first value after the last symbol property. + lhsStack[topIndex].recursor = recursorUtils.unshift(lhsStack[topIndex].recursor, lhs.collectAll()) + rhsStack[topIndex].recursor = recursorUtils.unshift(rhsStack[topIndex].recursor, rhs.collectAll()) + } + + lhsCircular.add(lhs) + rhsCircular.add(rhs) + + lhsStack.push({ subject: lhs, recursor: lhs.createRecursor() }) + rhsStack.push({ subject: rhs, recursor: rhs.createRecursor() }) + topIndex++ + } + + while (topIndex >= 0) { + lhs = lhsStack[topIndex].recursor() + rhs = rhsStack[topIndex].recursor() + if (lhs !== null && rhs !== null) { + break + } + + if (lhs === null && rhs === null) { + const lhsRecord = lhsStack.pop() + const rhsRecord = rhsStack.pop() + lhsCircular.delete(lhsRecord.subject) + rhsCircular.delete(rhsRecord.subject) + topIndex-- + } else { + return false + } + } + } while (topIndex >= 0) + + return true +} +exports.compareDescriptors = compareDescriptors + +function compare (actual, expected, options) { + if (Object.is(actual, expected)) return { pass: true } + // Primitive values should be the same, so if actual or expected is primitive + // then the values will never compare. + if (shortcircuitPrimitive(actual) || shortcircuitPrimitive(expected)) return { pass: false } + + actual = describe(actual, options) + expected = describe(expected, options) + const pass = compareDescriptors(actual, expected) + return { actual, expected, pass } +} +exports.compare = compare diff --git a/node_modules/concordance/lib/complexValues/arguments.js b/node_modules/concordance/lib/complexValues/arguments.js new file mode 100644 index 000000000..446407222 --- /dev/null +++ b/node_modules/concordance/lib/complexValues/arguments.js @@ -0,0 +1,48 @@ +'use strict' + +const constants = require('../constants') +const object = require('./object') + +const AMBIGUOUS = constants.AMBIGUOUS +const UNEQUAL = constants.UNEQUAL + +function describe (props) { + return new DescribedArgumentsValue(Object.assign({ + // Treat as an array, to allow comparisons with arrays + isArray: true, + isList: true + }, props, { ctor: 'Arguments' })) +} +exports.describe = describe + +function deserialize (state, recursor) { + return new DeserializedArgumentsValue(state, recursor) +} +exports.deserialize = deserialize + +const tag = Symbol('ArgumentsValue') +exports.tag = tag + +class ArgumentsValue extends object.ObjectValue { + compare (expected) { + if (expected.isComplex !== true) return UNEQUAL + + // When used on the left-hand side of a comparison, argument values may be + // compared to arrays. + if (expected.stringTag === 'Array') return AMBIGUOUS + + return super.compare(expected) + } +} +Object.defineProperty(ArgumentsValue.prototype, 'tag', { value: tag }) + +const DescribedArgumentsValue = object.DescribedMixin(ArgumentsValue) + +class DeserializedArgumentsValue extends object.DeserializedMixin(ArgumentsValue) { + compare (expected) { + // Deserialized argument values may only be compared to argument values. + return expected.isComplex === true && expected.stringTag === 'Array' + ? UNEQUAL + : super.compare(expected) + } +} diff --git a/node_modules/concordance/lib/complexValues/arrayBuffer.js b/node_modules/concordance/lib/complexValues/arrayBuffer.js new file mode 100644 index 000000000..1053b893e --- /dev/null +++ b/node_modules/concordance/lib/complexValues/arrayBuffer.js @@ -0,0 +1,30 @@ +'use strict' + +const typedArray = require('./typedArray') + +function describe (props) { + return new DescribedArrayBufferValue(Object.assign({ + // Assume at least Node.js 4.5.0, which introduces Buffer.from() + buffer: Buffer.from(props.value), + // Set isArray and isList so the property recursor excludes the byte accessors + isArray: true, + isList: true + }, props)) +} +exports.describe = describe + +function deserialize (state, recursor) { + return new DeserializedArrayBufferValue(state, recursor) +} +exports.deserialize = deserialize + +const tag = Symbol('ArrayBufferValue') +exports.tag = tag + +// ArrayBuffers can be represented as regular Buffers, allowing them to be +// treated as TypedArrays for the purposes of this package. +class ArrayBufferValue extends typedArray.TypedArrayValue {} +Object.defineProperty(ArrayBufferValue.prototype, 'tag', { value: tag }) + +const DescribedArrayBufferValue = typedArray.DescribedMixin(ArrayBufferValue) +const DeserializedArrayBufferValue = typedArray.DeserializedMixin(ArrayBufferValue) diff --git a/node_modules/concordance/lib/complexValues/boxed.js b/node_modules/concordance/lib/complexValues/boxed.js new file mode 100644 index 000000000..3736ad137 --- /dev/null +++ b/node_modules/concordance/lib/complexValues/boxed.js @@ -0,0 +1,51 @@ +'use strict' + +const recursorUtils = require('../recursorUtils') +const stringPrimitive = require('../primitiveValues/string').tag +const object = require('./object') + +function describe (props) { + return new DescribedBoxedValue(props) +} +exports.describe = describe + +function deserialize (state, recursor) { + return new DeserializedBoxedValue(state, recursor) +} +exports.deserialize = deserialize + +const tag = Symbol('BoxedValue') +exports.tag = tag + +class BoxedValue extends object.ObjectValue {} +Object.defineProperty(BoxedValue.prototype, 'tag', {value: tag}) + +class DescribedBoxedValue extends object.DescribedMixin(BoxedValue) { + constructor (props) { + super(props) + this.unboxed = props.unboxed + } + + createListRecursor () { + return recursorUtils.NOOP_RECURSOR + } + + createPropertyRecursor () { + if (this.unboxed.tag !== stringPrimitive) return super.createPropertyRecursor() + + // Just so that createPropertyRecursor() skips the index-based character + // properties. + try { + this.isList = true + return super.createPropertyRecursor() + } finally { + this.isList = false + } + } + + createRecursor () { + return recursorUtils.unshift(super.createRecursor(), this.unboxed) + } +} + +const DeserializedBoxedValue = object.DeserializedMixin(BoxedValue) diff --git a/node_modules/concordance/lib/complexValues/dataView.js b/node_modules/concordance/lib/complexValues/dataView.js new file mode 100644 index 000000000..0dd6199f6 --- /dev/null +++ b/node_modules/concordance/lib/complexValues/dataView.js @@ -0,0 +1,29 @@ +'use strict' + +const typedArray = require('./typedArray') + +function describe (props) { + return new DescribedDataViewValue(Object.assign({ + buffer: typedArray.getBuffer(props.value), + // Set isArray and isList so the property recursor excludes the byte accessors + isArray: true, + isList: true + }, props)) +} +exports.describe = describe + +function deserialize (state, recursor) { + return new DeserializedDataViewValue(state, recursor) +} +exports.deserialize = deserialize + +const tag = Symbol('DataViewValue') +exports.tag = tag + +// DataViews can be represented as regular Buffers, allowing them to be treated +// as TypedArrays for the purposes of this package. +class DataViewValue extends typedArray.TypedArrayValue {} +Object.defineProperty(DataViewValue.prototype, 'tag', { value: tag }) + +const DescribedDataViewValue = typedArray.DescribedMixin(DataViewValue) +const DeserializedDataViewValue = typedArray.DeserializedMixin(DataViewValue) diff --git a/node_modules/concordance/lib/complexValues/date.js b/node_modules/concordance/lib/complexValues/date.js new file mode 100644 index 000000000..b437456b8 --- /dev/null +++ b/node_modules/concordance/lib/complexValues/date.js @@ -0,0 +1,89 @@ +'use strict' + +const dateTime = require('date-time') + +const constants = require('../constants') +const formatUtils = require('../formatUtils') +const lineBuilder = require('../lineBuilder') +const object = require('./object') + +const SHALLOW_EQUAL = constants.SHALLOW_EQUAL +const UNEQUAL = constants.UNEQUAL + +function describe (props) { + const date = props.value + const invalid = isNaN(date.valueOf()) + return new DescribedDateValue(Object.assign({}, props, {invalid})) +} +exports.describe = describe + +function deserialize (state, recursor) { + return new DeserializedDateValue(state, recursor) +} +exports.deserialize = deserialize + +const tag = Symbol('DateValue') +exports.tag = tag + +function formatDate (date) { + // Always format in UTC. The local timezone shouldn't be used since it's most + // likely different from that of CI servers. + return dateTime({ + date, + local: false, + showTimeZone: true, + showMilliseconds: true + }) +} + +class DateValue extends object.ObjectValue { + constructor (props) { + super(props) + this.invalid = props.invalid + } + + compare (expected) { + const result = super.compare(expected) + if (result !== SHALLOW_EQUAL) return result + + return (this.invalid && expected.invalid) || Object.is(this.value.getTime(), expected.value.getTime()) + ? SHALLOW_EQUAL + : UNEQUAL + } + + formatShallow (theme, indent) { + const string = formatUtils.formatCtorAndStringTag(theme, this) + ' ' + + (this.invalid ? theme.date.invalid : formatUtils.wrap(theme.date.value, formatDate(this.value))) + ' ' + + theme.object.openBracket + + return super.formatShallow(theme, indent).customize({ + finalize (innerLines) { + return innerLines.isEmpty + ? lineBuilder.single(string + theme.object.closeBracket) + : lineBuilder.first(string) + .concat(innerLines.withFirstPrefixed(indent.increase()).stripFlags()) + .append(lineBuilder.last(indent + theme.object.closeBracket)) + }, + + maxDepth () { + return lineBuilder.single(string + ' ' + theme.maxDepth + ' ' + theme.object.closeBracket) + } + }) + } + + serialize () { + const iso = this.invalid ? null : this.value.toISOString() + return [this.invalid, iso, super.serialize()] + } +} +Object.defineProperty(DateValue.prototype, 'tag', { value: tag }) + +const DescribedDateValue = object.DescribedMixin(DateValue) + +class DeserializedDateValue extends object.DeserializedMixin(DateValue) { + constructor (state, recursor) { + super(state[2], recursor) + this.invalid = state[0] + this.value = new Date(this.invalid ? NaN : state[1]) + } +} diff --git a/node_modules/concordance/lib/complexValues/error.js b/node_modules/concordance/lib/complexValues/error.js new file mode 100644 index 000000000..781dd7e87 --- /dev/null +++ b/node_modules/concordance/lib/complexValues/error.js @@ -0,0 +1,133 @@ +'use strict' + +const constants = require('../constants') +const isEnumerable = require('../isEnumerable') +const formatUtils = require('../formatUtils') +const lineBuilder = require('../lineBuilder') +const NOOP_RECURSOR = require('../recursorUtils').NOOP_RECURSOR +const object = require('./object') + +const UNEQUAL = constants.UNEQUAL + +function describe (props) { + const error = props.value + return new DescribedErrorValue(Object.assign({ + nameIsEnumerable: isEnumerable(error, 'name'), + name: error.name, + messageIsEnumerable: isEnumerable(error, 'message'), + message: error.message + }, props)) +} +exports.describe = describe + +function deserialize (state, recursor) { + return new DeserializedErrorValue(state, recursor) +} +exports.deserialize = deserialize + +const tag = Symbol('ErrorValue') +exports.tag = tag + +class ErrorValue extends object.ObjectValue { + constructor (props) { + super(props) + this.name = props.name + } + + compare (expected) { + return this.tag === expected.tag && this.name === expected.name + ? super.compare(expected) + : UNEQUAL + } + + formatShallow (theme, indent) { + const name = this.name || this.ctor + + let string = name + ? formatUtils.wrap(theme.error.name, name) + : formatUtils.wrap(theme.object.stringTag, this.stringTag) + if (this.ctor && this.ctor !== name) { + string += ' ' + formatUtils.wrap(theme.error.ctor, this.ctor) + } + if (this.stringTag && this.stringTag !== this.ctor && this.name && !this.name.includes(this.stringTag)) { + string += ' ' + formatUtils.wrap(theme.object.secondaryStringTag, this.stringTag) + } + string += ' ' + theme.object.openBracket + + return super.formatShallow(theme, indent).customize({ + finalize (innerLines) { + return innerLines.isEmpty + ? lineBuilder.single(string + theme.object.closeBracket) + : lineBuilder.first(string) + .concat(innerLines.withFirstPrefixed(indent.increase()).stripFlags()) + .append(lineBuilder.last(indent + theme.object.closeBracket)) + }, + + maxDepth () { + return lineBuilder.single(string + ' ' + theme.maxDepth + ' ' + theme.object.closeBracket) + } + }) + } + + serialize () { + return [this.name, super.serialize()] + } +} +Object.defineProperty(ErrorValue.prototype, 'tag', { value: tag }) + +class DescribedErrorValue extends object.DescribedMixin(ErrorValue) { + constructor (props) { + super(props) + this.nameIsEnumerable = props.nameIsEnumerable + this.messageIsEnumerable = props.messageIsEnumerable + this.message = props.message + } + + createPropertyRecursor () { + const recursor = super.createPropertyRecursor() + + let skipName = this.nameIsEnumerable + let emitMessage = !this.messageIsEnumerable + + let size = recursor.size + if (skipName && size > 0) { + size -= 1 + } + if (emitMessage) { + size += 1 + } + + if (size === 0) return NOOP_RECURSOR + + let done = false + const next = () => { + if (done) return null + + const property = recursor.next() + if (property) { + if (skipName && property.key.value === 'name') { + skipName = false + return next() + } + return property + } + + if (emitMessage) { + emitMessage = false + return this.describeProperty('message', this.describeAny(this.message)) + } + + done = true + return null + } + + return { size, next } + } +} + +class DeserializedErrorValue extends object.DeserializedMixin(ErrorValue) { + constructor (state, recursor) { + super(state[1], recursor) + this.name = state[0] + } +} diff --git a/node_modules/concordance/lib/complexValues/function.js b/node_modules/concordance/lib/complexValues/function.js new file mode 100644 index 000000000..4ddd38f39 --- /dev/null +++ b/node_modules/concordance/lib/complexValues/function.js @@ -0,0 +1,159 @@ +'use strict' + +const functionNameSupport = require('function-name-support') + +const constants = require('../constants') +const getStringTag = require('../getStringTag') +const isEnumerable = require('../isEnumerable') +const formatUtils = require('../formatUtils') +const lineBuilder = require('../lineBuilder') +const NOOP_RECURSOR = require('../recursorUtils').NOOP_RECURSOR +const object = require('./object') + +const UNEQUAL = constants.UNEQUAL +const SHALLOW_EQUAL = constants.SHALLOW_EQUAL + +// Node.js 4 provides Function, more recent versions use GeneratorFunction +const generatorsHaveGeneratorTag = getStringTag(function * () {}) === 'GeneratorFunction' + +function describe (props) { + const fn = props.value + return new DescribedFunctionValue(Object.assign({ + nameIsEnumerable: isEnumerable(fn, 'name'), + name: typeof fn.name === 'string' ? fn.name : null + }, props)) +} +exports.describe = describe + +function deserialize (state, recursor) { + return new DeserializedFunctionValue(state, recursor) +} +exports.deserialize = deserialize + +const tag = Symbol('FunctionValue') +exports.tag = tag + +class FunctionValue extends object.ObjectValue { + constructor (props) { + super(props) + this.name = props.name + } + + formatShallow (theme, indent) { + const string = formatUtils.wrap(theme.function.stringTag, this.stringTag) + + (this.name ? ' ' + formatUtils.wrap(theme.function.name, this.name) : '') + + ' ' + theme.object.openBracket + + return super.formatShallow(theme, indent).customize({ + finalize (innerLines) { + return innerLines.isEmpty + ? lineBuilder.single(string + theme.object.closeBracket) + : lineBuilder.first(string) + .concat(innerLines.withFirstPrefixed(indent.increase()).stripFlags()) + .append(lineBuilder.last(indent + theme.object.closeBracket)) + }, + + maxDepth () { + return lineBuilder.single(string + ' ' + theme.maxDepth + ' ' + theme.object.closeBracket) + } + }) + } +} +Object.defineProperty(FunctionValue.prototype, 'tag', { value: tag }) + +class DescribedFunctionValue extends object.DescribedMixin(FunctionValue) { + constructor (props) { + super(props) + this.nameIsEnumerable = props.nameIsEnumerable + } + + compare (expected) { + if (this.tag !== expected.tag) return UNEQUAL + if (this.name !== expected.name) return UNEQUAL + if (this.value && expected.value && this.value !== expected.value) return UNEQUAL + + return super.compare(expected) + } + + createPropertyRecursor () { + const recursor = super.createPropertyRecursor() + + const skipName = this.nameIsEnumerable + if (!skipName) return recursor + + let size = recursor.size + if (skipName) { + size -= 1 + } + + if (size === 0) return NOOP_RECURSOR + + const next = () => { + const property = recursor.next() + if (property) { + if (skipName && property.key.value === 'name') { + return next() + } + return property + } + + return null + } + + return { size, next } + } + + serialize () { + return [this.name, generatorsHaveGeneratorTag, super.serialize()] + } +} + +class DeserializedFunctionValue extends object.DeserializedMixin(FunctionValue) { + constructor (state, recursor) { + super(state[2], recursor) + this.name = state[0] + this.trustStringTag = state[1] + } + + compare (expected) { + if (this.tag !== expected.tag) return UNEQUAL + + if (this.name !== expected.name) { + if (this.functionNameSupportFlags === functionNameSupport.bitFlags) { + // The engine used to create the serialization supports the same + // function name inference as the current engine. That said, unless + // the engine has full support for name inference, it's possible that + // names were lost simply due to refactoring. Names are unequal if + // the engine has full support, or if names were inferred. + if (functionNameSupport.hasFullSupport === true || (this.name !== '' && expected.name !== '')) return UNEQUAL + } else if (functionNameSupport.isSubsetOf(this.functionNameSupportFlags)) { + // The engine used to create the serialization could infer more function + // names than the current engine. Assume `expected.name` comes from the + // current engine and treat the names as unequal only if the current + // engine could infer a name. + if (expected.name !== '') return UNEQUAL + } else { + /* istanbul ignore else */ + if (functionNameSupport.isSupersetOf(this.functionNameSupportFlags)) { + // The engine used to create the serialization could infer fewer + // function names than the current engine. Treat the names as unequal + // only if a name was in the serialization. + if (this.name !== '') return UNEQUAL + } + } + } + + // Assume `stringTag` is either 'Function' or 'GeneratorFunction', and that + // it always equals `ctor`. Since Node.js 4 only provides 'Function', even + // for generator functions, only compare `stringTag` if the serialized value + // legitimately would have been `Function`, and the expected `stringTag` can + // reliably be inferred. + if (this.trustStringTag && generatorsHaveGeneratorTag && this.stringTag !== expected.stringTag) return UNEQUAL + + return SHALLOW_EQUAL + } + + serialize () { + return [this.name, this.trustStringTag, super.serialize()] + } +} diff --git a/node_modules/concordance/lib/complexValues/global.js b/node_modules/concordance/lib/complexValues/global.js new file mode 100644 index 000000000..6acd66d7a --- /dev/null +++ b/node_modules/concordance/lib/complexValues/global.js @@ -0,0 +1,33 @@ +'use strict' + +const constants = require('../constants') +const formatUtils = require('../formatUtils') +const lineBuilder = require('../lineBuilder') + +const DEEP_EQUAL = constants.DEEP_EQUAL +const UNEQUAL = constants.UNEQUAL + +function describe () { + return new GlobalValue() +} +exports.describe = describe + +exports.deserialize = describe + +const tag = Symbol('GlobalValue') +exports.tag = tag + +class GlobalValue { + compare (expected) { + return this.tag === expected.tag + ? DEEP_EQUAL + : UNEQUAL + } + + formatDeep (theme) { + return lineBuilder.single( + formatUtils.wrap(theme.global, 'Global') + ' ' + theme.object.openBracket + theme.object.closeBracket) + } +} +Object.defineProperty(GlobalValue.prototype, 'isComplex', { value: true }) +Object.defineProperty(GlobalValue.prototype, 'tag', { value: tag }) diff --git a/node_modules/concordance/lib/complexValues/map.js b/node_modules/concordance/lib/complexValues/map.js new file mode 100644 index 000000000..7ff13b217 --- /dev/null +++ b/node_modules/concordance/lib/complexValues/map.js @@ -0,0 +1,78 @@ +'use strict' + +const constants = require('../constants') +const recursorUtils = require('../recursorUtils') +const object = require('./object') + +const SHALLOW_EQUAL = constants.SHALLOW_EQUAL +const UNEQUAL = constants.UNEQUAL + +function describe (props) { + return new DescribedMapValue(Object.assign({ + size: props.value.size + }, props)) +} +exports.describe = describe + +function deserialize (state, recursor) { + return new DeserializedMapValue(state, recursor) +} +exports.deserialize = deserialize + +const tag = Symbol('MapValue') +exports.tag = tag + +class MapValue extends object.ObjectValue { + constructor (props) { + super(props) + this.size = props.size + } + + compare (expected) { + const result = super.compare(expected) + if (result !== SHALLOW_EQUAL) return result + + return this.size === expected.size + ? SHALLOW_EQUAL + : UNEQUAL + } + + prepareDiff (expected) { + // Maps should be compared, even if they have a different number of entries. + return {compareResult: super.compare(expected)} + } + + serialize () { + return [this.size, super.serialize()] + } +} +Object.defineProperty(MapValue.prototype, 'tag', { value: tag }) + +class DescribedMapValue extends object.DescribedMixin(MapValue) { + createIterableRecursor () { + const size = this.size + if (size === 0) return recursorUtils.NOOP_RECURSOR + + let index = 0 + let entries + const next = () => { + if (index === size) return null + + if (!entries) { + entries = Array.from(this.value) + } + + const entry = entries[index++] + return this.describeMapEntry(this.describeAny(entry[0]), this.describeAny(entry[1])) + } + + return { size, next } + } +} + +class DeserializedMapValue extends object.DeserializedMixin(MapValue) { + constructor (state, recursor) { + super(state[1], recursor) + this.size = state[0] + } +} diff --git a/node_modules/concordance/lib/complexValues/object.js b/node_modules/concordance/lib/complexValues/object.js new file mode 100644 index 000000000..97a59285c --- /dev/null +++ b/node_modules/concordance/lib/complexValues/object.js @@ -0,0 +1,283 @@ +'use strict' + +const functionNameSupport = require('function-name-support') + +const constants = require('../constants') +const getObjectKeys = require('../getObjectKeys') +const ObjectFormatter = require('../formatUtils').ObjectFormatter +const hasLength = require('../hasLength') +const recursorUtils = require('../recursorUtils') +const stats = require('../metaDescriptors/stats') + +const DEEP_EQUAL = constants.DEEP_EQUAL +const SHALLOW_EQUAL = constants.SHALLOW_EQUAL +const UNEQUAL = constants.UNEQUAL + +function describe (props) { + const isArray = props.stringTag === 'Array' + const object = props.value + return new DescribedObjectValue(Object.assign({ + isArray, + isIterable: object[Symbol.iterator] !== undefined, + isList: isArray || hasLength(object) + }, props)) +} +exports.describe = describe + +function deserialize (state, recursor) { + return new DeserializedObjectValue(state, recursor) +} +exports.deserialize = deserialize + +const tag = Symbol('ObjectValue') +exports.tag = tag + +class ObjectValue { + constructor (props) { + this.ctor = props.ctor + this.pointer = props.pointer + this.stringTag = props.stringTag + + this.isArray = props.isArray === true + this.isIterable = props.isIterable === true + this.isList = props.isList === true + } + + compare (expected) { + if (this.tag !== expected.tag) return UNEQUAL + if (this.stringTag !== expected.stringTag || !this.hasSameCtor(expected)) return UNEQUAL + return SHALLOW_EQUAL + } + + hasSameCtor (expected) { + return this.ctor === expected.ctor + } + + formatShallow (theme, indent) { + return new ObjectFormatter(this, theme, indent) + } + + serialize () { + return [ + this.ctor, this.pointer, this.stringTag, + this.isArray, this.isIterable, this.isList, + functionNameSupport.bitFlags + ] + } +} +Object.defineProperty(ObjectValue.prototype, 'isComplex', { value: true }) +Object.defineProperty(ObjectValue.prototype, 'tag', { value: tag }) +exports.ObjectValue = ObjectValue + +const DescribedObjectValue = DescribedMixin(ObjectValue) +const DeserializedObjectValue = DeserializedMixin(ObjectValue) + +function DescribedMixin (base) { + return class extends base { + constructor (props) { + super(props) + + this.value = props.value + this.describeAny = props.describeAny + this.describeItem = props.describeItem + this.describeMapEntry = props.describeMapEntry + this.describeProperty = props.describeProperty + + this.iterableState = null + this.listState = null + this.propertyState = null + } + + compare (expected) { + return this.value === expected.value + ? DEEP_EQUAL + : super.compare(expected) + } + + createPropertyRecursor () { + const objectKeys = getObjectKeys(this.value, this.isList ? this.value.length : 0) + const size = objectKeys.size + if (size === 0) return recursorUtils.NOOP_RECURSOR + + let index = 0 + const next = () => { + if (index === size) return null + + const key = objectKeys.keys[index++] + return this.describeProperty(key, this.describeAny(this.value[key])) + } + + return { size, next } + } + + createListRecursor () { + if (!this.isList) return recursorUtils.NOOP_RECURSOR + + const size = this.value.length + if (size === 0) return recursorUtils.NOOP_RECURSOR + + let index = 0 + const next = () => { + if (index === size) return null + + const current = index + index++ + return this.describeItem(current, this.describeAny(this.value[current])) + } + + return { size, next } + } + + createIterableRecursor () { + if (this.isArray || !this.isIterable) return recursorUtils.NOOP_RECURSOR + + const iterator = this.value[Symbol.iterator]() + let first = iterator.next() + + let done = false + let size = -1 + if (first.done) { + if (first.value === undefined) { + size = 0 + done = true + } else { + size = 1 + } + } + + let index = 0 + const next = () => { + if (done) return null + + while (!done) { + const current = first || iterator.next() + if (current === first) { + first = null + } + if (current.done) { + done = true + } + + const item = current.value + if (done && item === undefined) return null + + if (this.isList && this.value[index] === item) { + index++ + } else { + return this.describeItem(index++, this.describeAny(item)) + } + } + } + + return { size, next } + } + + createRecursor () { + let recursedProperty = false + let recursedList = false + let recursedIterable = false + + let recursor = null + return () => { + let retval = null + do { + if (recursor !== null) { + retval = recursor.next() + if (retval === null) { + recursor = null + } + } + + while (recursor === null && (!recursedList || !recursedProperty || !recursedIterable)) { + // Prioritize recursing lists + if (!recursedList) { + const replay = recursorUtils.replay(this.listState, () => this.createListRecursor()) + this.listState = replay.state + recursor = replay.recursor + recursedList = true + if (recursor !== recursorUtils.NOOP_RECURSOR) { + retval = stats.describeListRecursor(recursor) + } + } else if (!recursedProperty) { + const replay = recursorUtils.replay(this.propertyState, () => this.createPropertyRecursor()) + this.propertyState = replay.state + recursor = replay.recursor + recursedProperty = true + if (recursor !== recursorUtils.NOOP_RECURSOR) { + retval = stats.describePropertyRecursor(recursor) + } + } else if (!recursedIterable) { + const replay = recursorUtils.replay(this.iterableState, () => this.createIterableRecursor()) + this.iterableState = replay.state + recursor = replay.recursor + recursedIterable = true + if (recursor !== recursorUtils.NOOP_RECURSOR) { + retval = stats.describeIterableRecursor(recursor) + } + } + } + } while (recursor !== null && retval === null) + + return retval + } + } + } +} +exports.DescribedMixin = DescribedMixin + +function DeserializedMixin (base) { + return class extends base { + constructor (state, recursor) { + super({ + ctor: state[0], + pointer: state[1], + stringTag: state[2], + isArray: state[3], + isIterable: state[4], + isList: state[5] + }) + + this.functionNameSupportFlags = state[6] + this.deserializedRecursor = recursor + this.replayState = null + } + + createRecursor () { + if (!this.deserializedRecursor) return () => null + + const replay = recursorUtils.replay(this.replayState, () => ({ size: -1, next: this.deserializedRecursor })) + this.replayState = replay.state + return replay.recursor.next + } + + hasSameCtor (expected) { + if (this.ctor === expected.ctor) return true + + if (this.functionNameSupportFlags === functionNameSupport.bitFlags) { + // The engine used to create the serialization supports the same + // function name inference as the current engine. That said, unless + // the engine has full support for name inference, it's possible that + // names were lost simply due to refactoring. Ctors are not the same + // only if the engine has full support, or if ctors were inferred. + if (functionNameSupport.hasFullSupport === true || (this.ctor !== null && expected.ctor !== null)) return false + } else if (functionNameSupport.isSubsetOf(this.functionNameSupportFlags)) { + // The engine used to create the serialization could infer more function + // names than the current engine. Assume `expected.ctor` comes from the + // current engine and treat the ctors as unequal only if the current + // engine could infer a ctor. + if (expected.ctor !== null) return false + } else { + /* istanbul ignore else */ + if (functionNameSupport.isSupersetOf(this.functionNameSupportFlags)) { + // The engine used to create the serialization could infer fewer + // function names than the current engine. Treat the ctors as unequal + // only if a ctor was in the serialization. + if (this.ctor !== null) return false + } + } + + return true + } + } +} +exports.DeserializedMixin = DeserializedMixin diff --git a/node_modules/concordance/lib/complexValues/promise.js b/node_modules/concordance/lib/complexValues/promise.js new file mode 100644 index 000000000..823d45386 --- /dev/null +++ b/node_modules/concordance/lib/complexValues/promise.js @@ -0,0 +1,40 @@ +'use strict' + +const constants = require('../constants') +const object = require('./object') + +const DEEP_EQUAL = constants.DEEP_EQUAL +const UNEQUAL = constants.UNEQUAL + +function describe (props) { + return new DescribedPromiseValue(props) +} +exports.describe = describe + +function deserialize (props) { + return new DeserializedPromiseValue(props) +} +exports.deserialize = deserialize + +const tag = Symbol('PromiseValue') +exports.tag = tag + +class PromiseValue extends object.ObjectValue {} +Object.defineProperty(PromiseValue.prototype, 'tag', { value: tag }) + +class DescribedPromiseValue extends object.DescribedMixin(PromiseValue) { + compare (expected) { + // When comparing described promises, require them to be the exact same + // object. + return super.compare(expected) === DEEP_EQUAL + ? DEEP_EQUAL + : UNEQUAL + } +} + +class DeserializedPromiseValue extends object.DeserializedMixin(PromiseValue) { + compare (expected) { + // Deserialized promises can never be compared using object references. + return super.compare(expected) + } +} diff --git a/node_modules/concordance/lib/complexValues/regexp.js b/node_modules/concordance/lib/complexValues/regexp.js new file mode 100644 index 000000000..1c66a2fd8 --- /dev/null +++ b/node_modules/concordance/lib/complexValues/regexp.js @@ -0,0 +1,90 @@ +'use strict' + +const constants = require('../constants') +const formatUtils = require('../formatUtils') +const lineBuilder = require('../lineBuilder') +const object = require('./object') + +const UNEQUAL = constants.UNEQUAL + +function describe (props) { + const regexp = props.value + return new DescribedRegexpValue(Object.assign({ + flags: getSortedFlags(regexp), + source: regexp.source + }, props)) +} +exports.describe = describe + +function deserialize (state, recursor) { + return new DeserializedRegexpValue(state, recursor) +} +exports.deserialize = deserialize + +const tag = Symbol('RegexpValue') +exports.tag = tag + +function getSortedFlags (regexp) { + const flags = regexp.flags || String(regexp).slice(regexp.source.length + 2) + return flags.split('').sort().join('') +} + +class RegexpValue extends object.ObjectValue { + constructor (props) { + super(props) + this.flags = props.flags + this.source = props.source + } + + compare (expected) { + return this.tag === expected.tag && this.flags === expected.flags && this.source === expected.source + ? super.compare(expected) + : UNEQUAL + } + + formatShallow (theme, indent) { + const ctor = this.ctor || this.stringTag + const regexp = formatUtils.wrap(theme.regexp.source, this.source) + formatUtils.wrap(theme.regexp.flags, this.flags) + + return super.formatShallow(theme, indent).customize({ + finalize: innerLines => { + if (ctor === 'RegExp' && innerLines.isEmpty) return lineBuilder.single(regexp) + + const innerIndentation = indent.increase() + const header = lineBuilder.first(formatUtils.formatCtorAndStringTag(theme, this) + ' ' + theme.object.openBracket) + .concat(lineBuilder.line(innerIndentation + regexp)) + + if (!innerLines.isEmpty) { + header.append(lineBuilder.line(innerIndentation + theme.regexp.separator)) + header.append(innerLines.withFirstPrefixed(innerIndentation).stripFlags()) + } + + return header.append(lineBuilder.last(indent + theme.object.closeBracket)) + }, + + maxDepth: () => { + return lineBuilder.single( + formatUtils.formatCtorAndStringTag(theme, this) + ' ' + + theme.object.openBracket + ' ' + + regexp + ' ' + + theme.maxDepth + ' ' + + theme.object.closeBracket) + } + }) + } + + serialize () { + return [this.flags, this.source, super.serialize()] + } +} +Object.defineProperty(RegexpValue.prototype, 'tag', { value: tag }) + +const DescribedRegexpValue = object.DescribedMixin(RegexpValue) + +class DeserializedRegexpValue extends object.DeserializedMixin(RegexpValue) { + constructor (state, recursor) { + super(state[2], recursor) + this.flags = state[0] + this.source = state[1] + } +} diff --git a/node_modules/concordance/lib/complexValues/set.js b/node_modules/concordance/lib/complexValues/set.js new file mode 100644 index 000000000..fc3623ace --- /dev/null +++ b/node_modules/concordance/lib/complexValues/set.js @@ -0,0 +1,78 @@ +'use strict' + +const constants = require('../constants') +const recursorUtils = require('../recursorUtils') +const object = require('./object') + +const SHALLOW_EQUAL = constants.SHALLOW_EQUAL +const UNEQUAL = constants.UNEQUAL + +function describe (props) { + return new DescribedSetValue(Object.assign({ + size: props.value.size + }, props)) +} +exports.describe = describe + +function deserialize (state, recursor) { + return new DeserializedSetValue(state, recursor) +} +exports.deserialize = deserialize + +const tag = Symbol('SetValue') +exports.tag = tag + +class SetValue extends object.ObjectValue { + constructor (props) { + super(props) + this.size = props.size + } + + compare (expected) { + const result = super.compare(expected) + if (result !== SHALLOW_EQUAL) return result + + return this.size === expected.size + ? SHALLOW_EQUAL + : UNEQUAL + } + + prepareDiff (expected) { + // Sets should be compared, even if they have a different number of items. + return {compareResult: super.compare(expected)} + } + + serialize () { + return [this.size, super.serialize()] + } +} +Object.defineProperty(SetValue.prototype, 'tag', { value: tag }) + +class DescribedSetValue extends object.DescribedMixin(SetValue) { + createIterableRecursor () { + const size = this.size + if (size === 0) return recursorUtils.NOOP_RECURSOR + + let index = 0 + let members + const next = () => { + if (index === size) return null + + if (!members) { + members = Array.from(this.value) + } + + const value = members[index] + return this.describeItem(index++, this.describeAny(value)) + } + + return { size, next } + } +} + +class DeserializedSetValue extends object.DeserializedMixin(SetValue) { + constructor (state, recursor) { + super(state[1], recursor) + this.size = state[0] + } +} diff --git a/node_modules/concordance/lib/complexValues/typedArray.js b/node_modules/concordance/lib/complexValues/typedArray.js new file mode 100644 index 000000000..5c482ff1f --- /dev/null +++ b/node_modules/concordance/lib/complexValues/typedArray.js @@ -0,0 +1,162 @@ +'use strict' + +const constants = require('../constants') +const formatUtils = require('../formatUtils') +const lineBuilder = require('../lineBuilder') +const recursorUtils = require('../recursorUtils') +const propertyStatsTag = require('../metaDescriptors/stats').propertyTag +const object = require('./object') + +const DEEP_EQUAL = constants.DEEP_EQUAL +const UNEQUAL = constants.UNEQUAL + +function getBuffer (value) { + // Assume at least Node.js 4.5.0, which introduces Buffer.from() + const buffer = Buffer.from(value.buffer) + return value.byteLength !== value.buffer.byteLength + ? buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) + : buffer +} +exports.getBuffer = getBuffer + +function describe (props) { + return new DescribedTypedArrayValue(Object.assign({ + buffer: getBuffer(props.value), + // Set isArray and isList so the property recursor excludes the byte accessors + isArray: true, + isList: true + }, props)) +} +exports.describe = describe + +function deserialize (state, recursor) { + return new DeserializedTypedArrayValue(state, recursor) +} +exports.deserialize = deserialize + +function deserializeBytes (buffer) { + return new Bytes(buffer) +} +exports.deserializeBytes = deserializeBytes + +const bytesTag = Symbol('Bytes') +exports.bytesTag = bytesTag + +const tag = Symbol('TypedArrayValue') +exports.tag = tag + +class Bytes { + constructor (buffer) { + this.buffer = buffer + } + + compare (expected) { + return expected.tag === bytesTag && this.buffer.equals(expected.buffer) + ? DEEP_EQUAL + : UNEQUAL + } + + formatDeep (theme, indent) { + const indentation = indent + const lines = lineBuilder.buffer() + + // Display 4-byte words, 8 per line + let string = '' + let isFirst = true + for (let offset = 0; offset < this.buffer.length; offset += 4) { + if (offset > 0) { + if (offset % 32 === 0) { + if (isFirst) { + lines.append(lineBuilder.first(string)) + isFirst = false + } else { + lines.append(lineBuilder.line(string)) + } + string = String(indentation) + } else { + string += ' ' + } + } + string += formatUtils.wrap(theme.typedArray.bytes, this.buffer.toString('hex', offset, offset + 4)) + } + + return isFirst + ? lineBuilder.single(string) + : lines.append(lineBuilder.last(string)) + } + + serialize () { + return this.buffer + } +} +Object.defineProperty(Bytes.prototype, 'tag', { value: bytesTag }) + +class TypedArrayValue extends object.ObjectValue { + constructor (props) { + super(props) + this.buffer = props.buffer + } + + formatShallow (theme, indent) { + return super.formatShallow(theme, indent).customize({ + shouldFormat (subject) { + if (subject.tag === propertyStatsTag) return subject.size > 1 + if (subject.isProperty === true) return subject.key.value !== 'byteLength' + if (subject.tag === bytesTag) return subject.buffer.byteLength > 0 + return true + } + }) + } +} +Object.defineProperty(TypedArrayValue.prototype, 'tag', { value: tag }) +exports.TypedArrayValue = TypedArrayValue + +function DescribedMixin (base) { + return class extends object.DescribedMixin(base) { + // The list isn't recursed. Instead a Bytes instance is returned by the main + // recursor. + createListRecursor () { + return recursorUtils.NOOP_RECURSOR + } + + createPropertyRecursor () { + const recursor = super.createPropertyRecursor() + const size = recursor.size + 1 + + let done = false + const next = () => { + if (done) return null + + const property = recursor.next() + if (property) return property + + done = true + return this.describeProperty('byteLength', this.describeAny(this.buffer.byteLength)) + } + + return { size, next } + } + + createRecursor () { + return recursorUtils.unshift(super.createRecursor(), new Bytes(this.buffer)) + } + } +} +exports.DescribedMixin = DescribedMixin + +const DescribedTypedArrayValue = DescribedMixin(TypedArrayValue) + +function DeserializedMixin (base) { + return class extends object.DeserializedMixin(base) { + constructor (state, recursor) { + super(state, recursor) + + // Get the Bytes descriptor from the recursor. It contains the buffer. + const bytesDescriptor = this.createRecursor()() + this.buffer = bytesDescriptor.buffer + } + } +} +exports.DeserializedMixin = DeserializedMixin + +const DeserializedTypedArrayValue = DeserializedMixin(TypedArrayValue) diff --git a/node_modules/concordance/lib/constants.js b/node_modules/concordance/lib/constants.js new file mode 100644 index 000000000..f85235faa --- /dev/null +++ b/node_modules/concordance/lib/constants.js @@ -0,0 +1,13 @@ +'use strict' + +const AMBIGUOUS = Symbol('AMBIGUOUS') +const DEEP_EQUAL = Symbol('DEEP_EQUAL') +const SHALLOW_EQUAL = Symbol('SHALLOW_EQUAL') +const UNEQUAL = Symbol('UNEQUAL') + +module.exports = { + AMBIGUOUS, + DEEP_EQUAL, + SHALLOW_EQUAL, + UNEQUAL +} diff --git a/node_modules/concordance/lib/describe.js b/node_modules/concordance/lib/describe.js new file mode 100644 index 000000000..a8aa94572 --- /dev/null +++ b/node_modules/concordance/lib/describe.js @@ -0,0 +1,170 @@ +'use strict' + +const argumentsValue = require('./complexValues/arguments') +const arrayBufferValue = require('./complexValues/arrayBuffer') +const boxedValue = require('./complexValues/boxed') +const dataViewValue = require('./complexValues/dataView') +const dateValue = require('./complexValues/date') +const errorValue = require('./complexValues/error') +const functionValue = require('./complexValues/function') +const globalValue = require('./complexValues/global') +const mapValue = require('./complexValues/map') +const objectValue = require('./complexValues/object') +const promiseValue = require('./complexValues/promise') +const regexpValue = require('./complexValues/regexp') +const setValue = require('./complexValues/set') +const typedArrayValue = require('./complexValues/typedArray') + +const itemDescriptor = require('./metaDescriptors/item') +const mapEntryDescriptor = require('./metaDescriptors/mapEntry') +const propertyDescriptor = require('./metaDescriptors/property') + +const booleanValue = require('./primitiveValues/boolean') +const nullValue = require('./primitiveValues/null') +const numberValue = require('./primitiveValues/number') +const stringValue = require('./primitiveValues/string') +const symbolValue = require('./primitiveValues/symbol') +const undefinedValue = require('./primitiveValues/undefined') + +const getCtor = require('./getCtor') +const getStringTag = require('./getStringTag') +const pluginRegistry = require('./pluginRegistry') +const Registry = require('./Registry') + +const SpecializedComplexes = new Map([ + ['Arguments', argumentsValue.describe], + ['ArrayBuffer', arrayBufferValue.describe], + ['DataView', dataViewValue.describe], + ['Date', dateValue.describe], + ['Error', errorValue.describe], + ['Float32Array', typedArrayValue.describe], + ['Float64Array', typedArrayValue.describe], + ['Function', functionValue.describe], + ['GeneratorFunction', functionValue.describe], + ['global', globalValue.describe], + ['Int16Array', typedArrayValue.describe], + ['Int32Array', typedArrayValue.describe], + ['Int8Array', typedArrayValue.describe], + ['Map', mapValue.describe], + ['Promise', promiseValue.describe], + ['RegExp', regexpValue.describe], + ['Set', setValue.describe], + ['Uint16Array', typedArrayValue.describe], + ['Uint32Array', typedArrayValue.describe], + ['Uint8Array', typedArrayValue.describe], + ['Uint8ClampedArray', typedArrayValue.describe] +]) + +function describePrimitive (value) { + if (value === null) return nullValue.describe() + if (value === undefined) return undefinedValue.describe() + if (value === true || value === false) return booleanValue.describe(value) + + const type = typeof value + if (type === 'number') return numberValue.describe(value) + if (type === 'string') return stringValue.describe(value) + if (type === 'symbol') return symbolValue.describe(value) + + return null +} + +function unboxComplex (tag, complex) { + // Try to unbox by calling `valueOf()`. `describePrimitive()` will return + // `null` if the resulting value is not a primitive, in which case it's + // ignored. + if (typeof complex.valueOf === 'function') { + const value = complex.valueOf() + if (value !== complex) return describePrimitive(value) + } + + return null +} + +function registerPlugins (plugins) { + if (!Array.isArray(plugins) || plugins.length === 0) return () => null + + const tryFns = pluginRegistry.getTryDescribeValues(plugins) + return (value, stringTag, ctor) => { + for (const tryDescribeValue of tryFns) { + const describeValue = tryDescribeValue(value, stringTag, ctor) + if (describeValue) return describeValue + } + + return null + } +} + +function describeComplex (value, registry, tryPlugins, describeAny, describeItem, describeMapEntry, describeProperty) { + if (registry.has(value)) return registry.get(value) + + const stringTag = getStringTag(value) + const ctor = getCtor(stringTag, value) + const pointer = registry.alloc(value) + + let unboxed + let describeValue = tryPlugins(value, stringTag, ctor) + if (describeValue === null) { + if (SpecializedComplexes.has(stringTag)) { + describeValue = SpecializedComplexes.get(stringTag) + } else { + unboxed = unboxComplex(stringTag, value) + if (unboxed !== null) { + describeValue = boxedValue.describe + } else { + describeValue = objectValue.describe + } + } + } + + const descriptor = describeValue({ + ctor, + describeAny, + describeItem, + describeMapEntry, + describeProperty, + pointer: pointer.index, + stringTag, + unboxed, + value + }) + pointer.descriptor = descriptor + return descriptor +} + +function describe (value, options) { + const primitive = describePrimitive(value) + if (primitive !== null) return primitive + + const registry = new Registry() + const tryPlugins = registerPlugins(options && options.plugins) + const curriedComplex = c => { + return describeComplex(c, registry, tryPlugins, describeAny, describeItem, describeMapEntry, describeProperty) + } + + const describeAny = any => { + const descriptor = describePrimitive(any) + return descriptor !== null + ? descriptor + : curriedComplex(any) + } + + const describeItem = (index, valueDescriptor) => { + return valueDescriptor.isPrimitive === true + ? itemDescriptor.describePrimitive(index, valueDescriptor) + : itemDescriptor.describeComplex(index, valueDescriptor) + } + + const describeMapEntry = (keyDescriptor, valueDescriptor) => { + return mapEntryDescriptor.describe(keyDescriptor, valueDescriptor) + } + + const describeProperty = (key, valueDescriptor) => { + const keyDescriptor = describePrimitive(key) + return valueDescriptor.isPrimitive === true + ? propertyDescriptor.describePrimitive(keyDescriptor, valueDescriptor) + : propertyDescriptor.describeComplex(keyDescriptor, valueDescriptor) + } + + return curriedComplex(value) +} +module.exports = describe diff --git a/node_modules/concordance/lib/diff.js b/node_modules/concordance/lib/diff.js new file mode 100644 index 000000000..16191143f --- /dev/null +++ b/node_modules/concordance/lib/diff.js @@ -0,0 +1,391 @@ +'use strict' + +const constants = require('./constants') +const describe = require('./describe') +const lineBuilder = require('./lineBuilder') +const recursorUtils = require('./recursorUtils') +const shouldCompareDeep = require('./shouldCompareDeep') +const symbolProperties = require('./symbolProperties') +const themeUtils = require('./themeUtils') +const Circular = require('./Circular') +const Indenter = require('./Indenter') + +const AMBIGUOUS = constants.AMBIGUOUS +const DEEP_EQUAL = constants.DEEP_EQUAL +const UNEQUAL = constants.UNEQUAL +const SHALLOW_EQUAL = constants.SHALLOW_EQUAL +const NOOP = Symbol('NOOP') + +const alwaysFormat = () => true + +function compareComplexShape (lhs, rhs) { + let result = lhs.compare(rhs) + if (result === DEEP_EQUAL) return DEEP_EQUAL + if (result === UNEQUAL || !shouldCompareDeep(result, lhs, rhs)) return UNEQUAL + + let collectedSymbolProperties = false + let lhsRecursor = lhs.createRecursor() + let rhsRecursor = rhs.createRecursor() + + do { + lhs = lhsRecursor() + rhs = rhsRecursor() + + if (lhs === null && rhs === null) return SHALLOW_EQUAL + if (lhs === null || rhs === null) return UNEQUAL + + result = lhs.compare(rhs) + if (result === UNEQUAL) return UNEQUAL + if ( + result === AMBIGUOUS && + lhs.isProperty === true && !collectedSymbolProperties && + shouldCompareDeep(result, lhs, rhs) + ) { + collectedSymbolProperties = true + const lhsCollector = new symbolProperties.Collector(lhs, lhsRecursor) + const rhsCollector = new symbolProperties.Collector(rhs, rhsRecursor) + + lhsRecursor = recursorUtils.sequence( + lhsCollector.createRecursor(), + recursorUtils.unshift(lhsRecursor, lhsCollector.collectAll())) + rhsRecursor = recursorUtils.sequence( + rhsCollector.createRecursor(), + recursorUtils.unshift(rhsRecursor, rhsCollector.collectAll())) + } + } while (true) +} + +function diffDescriptors (lhs, rhs, options) { + const theme = themeUtils.normalize(options) + const invert = options ? options.invert === true : false + + const lhsCircular = new Circular() + const rhsCircular = new Circular() + const maxDepth = (options && options.maxDepth) || 0 + + let indent = new Indenter(0, ' ') + + const lhsStack = [] + const rhsStack = [] + let topIndex = -1 + + const buffer = lineBuilder.buffer() + const diffStack = [] + let diffIndex = -1 + + const isCircular = descriptor => lhsCircular.has(descriptor) || rhsCircular.has(descriptor) + + const format = (builder, subject, circular) => { + if (diffIndex >= 0 && !diffStack[diffIndex].shouldFormat(subject)) return + + if (circular.has(subject)) { + diffStack[diffIndex].formatter.append(builder.single(theme.circular)) + return + } + + const formatStack = [] + let formatIndex = -1 + + do { + if (circular.has(subject)) { + formatStack[formatIndex].formatter.append(builder.single(theme.circular), subject) + } else { + let didFormat = false + if (typeof subject.formatDeep === 'function') { + let formatted = subject.formatDeep(themeUtils.applyModifiers(subject, theme), indent) + if (formatted !== null) { + didFormat = true + + if (formatIndex === -1) { + formatted = builder.setDefaultGutter(formatted) + if (diffIndex === -1) { + buffer.append(formatted) + } else { + diffStack[diffIndex].formatter.append(formatted, subject) + } + } else { + formatStack[formatIndex].formatter.append(formatted, subject) + } + } + } + + if (!didFormat && typeof subject.formatShallow === 'function') { + const formatter = subject.formatShallow(themeUtils.applyModifiers(subject, theme), indent) + const recursor = subject.createRecursor() + + if (formatter.increaseIndent && maxDepth > 0 && indent.level === maxDepth) { + const isEmpty = recursor() === null + let formatted = !isEmpty && typeof formatter.maxDepth === 'function' + ? formatter.maxDepth() + : formatter.finalize() + + if (formatIndex === -1) { + formatted = builder.setDefaultGutter(formatted) + diffStack[diffIndex].formatter.append(formatted, subject) + } else { + formatStack[formatIndex].formatter.append(formatted, subject) + } + } else { + formatStack.push({ + formatter, + recursor, + decreaseIndent: formatter.increaseIndent, + shouldFormat: formatter.shouldFormat || alwaysFormat, + subject + }) + formatIndex++ + + if (formatter.increaseIndent) indent = indent.increase() + circular.add(subject) + } + } + } + + while (formatIndex >= 0) { + do { + subject = formatStack[formatIndex].recursor() + } while (subject && !formatStack[formatIndex].shouldFormat(subject)) + + if (subject) { + break + } + + const record = formatStack.pop() + formatIndex-- + if (record.decreaseIndent) indent = indent.decrease() + circular.delete(record.subject) + + let formatted = record.formatter.finalize() + if (formatIndex === -1) { + formatted = builder.setDefaultGutter(formatted) + if (diffIndex === -1) { + buffer.append(formatted) + } else { + diffStack[diffIndex].formatter.append(formatted, record.subject) + } + } else { + formatStack[formatIndex].formatter.append(formatted, record.subject) + } + } + } while (formatIndex >= 0) + } + + do { + let compareResult = NOOP + if (lhsCircular.has(lhs)) { + compareResult = lhsCircular.get(lhs) === rhsCircular.get(rhs) + ? DEEP_EQUAL + : UNEQUAL + } else if (rhsCircular.has(rhs)) { + compareResult = UNEQUAL + } + + let firstPassSymbolProperty = false + if (lhs.isProperty === true) { + compareResult = lhs.compare(rhs) + if (compareResult === AMBIGUOUS) { + const parent = lhsStack[topIndex].subject + firstPassSymbolProperty = parent.isSymbolPropertiesCollector !== true && parent.isSymbolPropertiesComparable !== true + } + } + + let didFormat = false + let mustRecurse = false + if (compareResult !== DEEP_EQUAL && !firstPassSymbolProperty && typeof lhs.prepareDiff === 'function') { + const lhsRecursor = topIndex === -1 ? null : lhsStack[topIndex].recursor + const rhsRecursor = topIndex === -1 ? null : rhsStack[topIndex].recursor + + const instructions = lhs.prepareDiff( + rhs, + lhsRecursor, + rhsRecursor, + compareComplexShape, + isCircular) + + if (instructions !== null) { + if (topIndex >= 0) { + if (typeof instructions.lhsRecursor === 'function') { + lhsStack[topIndex].recursor = instructions.lhsRecursor + } + if (typeof instructions.rhsRecursor === 'function') { + rhsStack[topIndex].recursor = instructions.rhsRecursor + } + } + + if (instructions.compareResult) { + compareResult = instructions.compareResult + } + if (instructions.mustRecurse === true) { + mustRecurse = true + } else { + if (instructions.actualIsExtraneous === true) { + format(lineBuilder.actual, lhs, lhsCircular) + didFormat = true + } else if (instructions.multipleAreExtraneous === true) { + for (const extraneous of instructions.descriptors) { + format(lineBuilder.actual, extraneous, lhsCircular) + } + didFormat = true + } else if (instructions.expectedIsMissing === true) { + format(lineBuilder.expected, rhs, rhsCircular) + didFormat = true + } else if (instructions.multipleAreMissing === true) { + for (const missing of instructions.descriptors) { + format(lineBuilder.expected, missing, rhsCircular) + } + didFormat = true + } else if (instructions.isUnequal === true) { + format(lineBuilder.actual, lhs, lhsCircular) + format(lineBuilder.expected, rhs, rhsCircular) + didFormat = true + } else if (!instructions.compareResult) { + // TODO: Throw a useful, custom error + throw new Error('Illegal result of prepareDiff()') + } + } + } + } + + if (!didFormat) { + if (compareResult === NOOP) { + compareResult = lhs.compare(rhs) + } + + if (!mustRecurse) { + mustRecurse = shouldCompareDeep(compareResult, lhs, rhs) + } + + if (compareResult === DEEP_EQUAL) { + format(lineBuilder, lhs, lhsCircular) + } else if (mustRecurse) { + if (compareResult === AMBIGUOUS && lhs.isProperty === true) { + // Replace both sides by a pseudo-descriptor which collects symbol + // properties instead. + lhs = new symbolProperties.Collector(lhs, lhsStack[topIndex].recursor) + rhs = new symbolProperties.Collector(rhs, rhsStack[topIndex].recursor) + // Replace the current recursors so they can continue correctly after + // the collectors have been "compared". This is necessary since the + // collectors eat the first value after the last symbol property. + lhsStack[topIndex].recursor = recursorUtils.unshift(lhsStack[topIndex].recursor, lhs.collectAll()) + rhsStack[topIndex].recursor = recursorUtils.unshift(rhsStack[topIndex].recursor, rhs.collectAll()) + } + + if (typeof lhs.diffShallow === 'function') { + const formatter = lhs.diffShallow(rhs, themeUtils.applyModifiers(lhs, theme), indent) + diffStack.push({ + formatter, + origin: lhs, + decreaseIndent: formatter.increaseIndent, + exceedsMaxDepth: formatter.increaseIndent && maxDepth > 0 && indent.level >= maxDepth, + shouldFormat: formatter.shouldFormat || alwaysFormat + }) + diffIndex++ + + if (formatter.increaseIndent) indent = indent.increase() + } else if (typeof lhs.formatShallow === 'function') { + const formatter = lhs.formatShallow(themeUtils.applyModifiers(lhs, theme), indent) + diffStack.push({ + formatter, + decreaseIndent: formatter.increaseIndent, + exceedsMaxDepth: formatter.increaseIndent && maxDepth > 0 && indent.level >= maxDepth, + shouldFormat: formatter.shouldFormat || alwaysFormat, + subject: lhs + }) + diffIndex++ + + if (formatter.increaseIndent) indent = indent.increase() + } + + lhsCircular.add(lhs) + rhsCircular.add(rhs) + + lhsStack.push({ diffIndex, subject: lhs, recursor: lhs.createRecursor() }) + rhsStack.push({ diffIndex, subject: rhs, recursor: rhs.createRecursor() }) + topIndex++ + } else { + const diffed = typeof lhs.diffDeep === 'function' + ? lhs.diffDeep(rhs, themeUtils.applyModifiers(lhs, theme), indent) + : null + + if (diffed === null) { + format(lineBuilder.actual, lhs, lhsCircular) + format(lineBuilder.expected, rhs, rhsCircular) + } else { + if (diffIndex === -1) { + buffer.append(diffed) + } else { + diffStack[diffIndex].formatter.append(diffed, lhs) + } + } + } + } + + while (topIndex >= 0) { + lhs = lhsStack[topIndex].recursor() + rhs = rhsStack[topIndex].recursor() + + if (lhs !== null && rhs !== null) { + break + } + + if (lhs === null && rhs === null) { + const lhsRecord = lhsStack.pop() + const rhsRecord = rhsStack.pop() + lhsCircular.delete(lhsRecord.subject) + rhsCircular.delete(rhsRecord.subject) + topIndex-- + + if (lhsRecord.diffIndex === diffIndex) { + const record = diffStack.pop() + diffIndex-- + if (record.decreaseIndent) indent = indent.decrease() + + let formatted = record.formatter.finalize() + if (record.exceedsMaxDepth && !formatted.hasGutter) { + // The record exceeds the max depth, but contains no actual diff. + // Discard the potentially deep formatting and format just the + // original subject. + const subject = lhsRecord.subject + const formatter = subject.formatShallow(themeUtils.applyModifiers(subject, theme), indent) + const isEmpty = subject.createRecursor()() === null + formatted = !isEmpty && typeof formatter.maxDepth === 'function' + ? formatter.maxDepth() + : formatter.finalize() + } + + if (diffIndex === -1) { + buffer.append(formatted) + } else { + diffStack[diffIndex].formatter.append(formatted, record.subject) + } + } + } else { + let builder, circular, stack, subject + if (lhs === null) { + builder = lineBuilder.expected + circular = rhsCircular + stack = rhsStack + subject = rhs + } else { + builder = lineBuilder.actual + circular = lhsCircular + stack = lhsStack + subject = lhs + } + + do { + format(builder, subject, circular) + subject = stack[topIndex].recursor() + } while (subject !== null) + } + } + } while (topIndex >= 0) + + return buffer.toString({diff: true, invert, theme}) +} +exports.diffDescriptors = diffDescriptors + +function diff (actual, expected, options) { + return diffDescriptors(describe(actual, options), describe(expected, options), options) +} +exports.diff = diff diff --git a/node_modules/concordance/lib/encoder.js b/node_modules/concordance/lib/encoder.js new file mode 100644 index 000000000..ea1be3855 --- /dev/null +++ b/node_modules/concordance/lib/encoder.js @@ -0,0 +1,293 @@ +'use strict' + +const flattenDeep = require('lodash.flattendeep') + +// Indexes are hexadecimal to make reading the binary output easier. +const valueTypes = { + zero: 0x00, + int8: 0x01, // Note that the hex value equals the number of bytes required + int16: 0x02, // to store the integer. + int24: 0x03, + int32: 0x04, + int40: 0x05, + int48: 0x06, + // Leave room for int56 and int64 + numberString: 0x09, + negativeZero: 0x0A, + notANumber: 0x0B, + infinity: 0x0C, + negativeInfinity: 0x0D, + undefined: 0x0E, + null: 0x0F, + true: 0x10, + false: 0x11, + utf8: 0x12, + bytes: 0x13, + list: 0x14, + descriptor: 0x15 +} + +const descriptorSymbol = Symbol('descriptor') +exports.descriptorSymbol = descriptorSymbol + +function encodeInteger (type, value) { + const encoded = Buffer.alloc(type) + encoded.writeIntLE(value, 0, type) + return [type, encoded] +} + +function encodeValue (value) { + if (Object.is(value, 0)) return valueTypes.zero + if (Object.is(value, -0)) return valueTypes.negativeZero + if (Object.is(value, NaN)) return valueTypes.notANumber + if (value === Infinity) return valueTypes.infinity + if (value === -Infinity) return valueTypes.negativeInfinity + if (value === undefined) return valueTypes.undefined + if (value === null) return valueTypes.null + if (value === true) return valueTypes.true + if (value === false) return valueTypes.false + + const type = typeof value + if (type === 'number') { + if (Number.isInteger(value)) { + // The integer types are signed, so int8 can only store 7 bits, int16 + // only 15, etc. + if (value >= -0x80 && value < 0x80) return encodeInteger(valueTypes.int8, value) + if (value >= -0x8000 && value < 0x8000) return encodeInteger(valueTypes.int16, value) + if (value >= -0x800000 && value < 0x800000) return encodeInteger(valueTypes.int24, value) + if (value >= -0x80000000 && value < 0x80000000) return encodeInteger(valueTypes.int32, value) + if (value >= -0x8000000000 && value < 0x8000000000) return encodeInteger(valueTypes.int40, value) + if (value >= -0x800000000000 && value < 0x800000000000) return encodeInteger(valueTypes.int48, value) + // Fall through to encoding the value as a number string. + } + + const encoded = Buffer.from(String(value), 'utf8') + return [valueTypes.numberString, encodeValue(encoded.length), encoded] + } + + if (type === 'string') { + const encoded = Buffer.from(value, 'utf8') + return [valueTypes.utf8, encodeValue(encoded.length), encoded] + } + + if (Buffer.isBuffer(value)) { + return [valueTypes.bytes, encodeValue(value.byteLength), value] + } + + if (Array.isArray(value)) { + return [ + value[descriptorSymbol] === true ? valueTypes.descriptor : valueTypes.list, + encodeValue(value.length), + value.map(encodeValue) + ] + } + + const hex = `0x${type.toString(16).toUpperCase()}` + throw new TypeError(`Unexpected value with type ${hex}`) +} + +function decodeValue (buffer, byteOffset) { + const type = buffer.readUInt8(byteOffset) + byteOffset += 1 + + if (type === valueTypes.zero) return { byteOffset, value: 0 } + if (type === valueTypes.negativeZero) return { byteOffset, value: -0 } + if (type === valueTypes.notANumber) return { byteOffset, value: NaN } + if (type === valueTypes.infinity) return { byteOffset, value: Infinity } + if (type === valueTypes.negativeInfinity) return { byteOffset, value: -Infinity } + if (type === valueTypes.undefined) return { byteOffset, value: undefined } + if (type === valueTypes.null) return { byteOffset, value: null } + if (type === valueTypes.true) return { byteOffset, value: true } + if (type === valueTypes.false) return { byteOffset, value: false } + + if ( + type === valueTypes.int8 || type === valueTypes.int16 || type === valueTypes.int24 || + type === valueTypes.int32 || type === valueTypes.int40 || type === valueTypes.int48 + ) { + const value = buffer.readIntLE(byteOffset, type) + byteOffset += type + return { byteOffset, value } + } + + if (type === valueTypes.numberString || type === valueTypes.utf8 || type === valueTypes.bytes) { + const length = decodeValue(buffer, byteOffset) + const start = length.byteOffset + const end = start + length.value + + if (type === valueTypes.numberString) { + const value = Number(buffer.toString('utf8', start, end)) + return { byteOffset: end, value } + } + + if (type === valueTypes.utf8) { + const value = buffer.toString('utf8', start, end) + return { byteOffset: end, value } + } + + const value = buffer.slice(start, end) + return { byteOffset: end, value } + } + + if (type === valueTypes.list || type === valueTypes.descriptor) { + const length = decodeValue(buffer, byteOffset) + byteOffset = length.byteOffset + + const value = new Array(length.value) + if (type === valueTypes.descriptor) { + value[descriptorSymbol] = true + } + + for (let index = 0; index < length.value; index++) { + const item = decodeValue(buffer, byteOffset) + byteOffset = item.byteOffset + value[index] = item.value + } + + return { byteOffset, value } + } + + const hex = `0x${type.toString(16).toUpperCase()}` + throw new TypeError(`Could not decode type ${hex}`) +} + +function buildBuffer (numberOrArray) { + if (typeof numberOrArray === 'number') { + const byte = Buffer.alloc(1) + byte.writeUInt8(numberOrArray) + return byte + } + + const array = flattenDeep(numberOrArray) + const buffers = new Array(array.length) + let byteLength = 0 + for (let index = 0; index < array.length; index++) { + if (typeof array[index] === 'number') { + byteLength += 1 + const byte = Buffer.alloc(1) + byte.writeUInt8(array[index]) + buffers[index] = byte + } else { + byteLength += array[index].byteLength + buffers[index] = array[index] + } + } + return Buffer.concat(buffers, byteLength) +} + +function encode (serializerVersion, rootRecord, usedPlugins) { + const buffers = [] + let byteOffset = 0 + + const versionHeader = Buffer.alloc(2) + versionHeader.writeUInt16LE(serializerVersion) + buffers.push(versionHeader) + byteOffset += versionHeader.byteLength + + const rootOffset = Buffer.alloc(4) + buffers.push(rootOffset) + byteOffset += rootOffset.byteLength + + const numPlugins = buildBuffer(encodeValue(usedPlugins.size)) + buffers.push(numPlugins) + byteOffset += numPlugins.byteLength + + for (const name of usedPlugins.keys()) { + const plugin = usedPlugins.get(name) + const record = buildBuffer([ + encodeValue(name), + encodeValue(plugin.serializerVersion) + ]) + buffers.push(record) + byteOffset += record.byteLength + } + + const queue = [rootRecord] + const pointers = [rootOffset] + while (queue.length > 0) { + pointers.shift().writeUInt32LE(byteOffset, 0) + + const record = queue.shift() + const recordHeader = buildBuffer([ + encodeValue(record.pluginIndex), + encodeValue(record.id), + encodeValue(record.children.length) + ]) + buffers.push(recordHeader) + byteOffset += recordHeader.byteLength + + // Add pointers before encoding the state. This allows, if it ever becomes + // necessary, for records to be extracted from a buffer without having to + // parse the (variable length) state field. + for (const child of record.children) { + queue.push(child) + + const pointer = Buffer.alloc(4) + pointers.push(pointer) + buffers.push(pointer) + byteOffset += 4 + } + + const state = buildBuffer(encodeValue(record.state)) + buffers.push(state) + byteOffset += state.byteLength + } + + return Buffer.concat(buffers, byteOffset) +} +exports.encode = encode + +function decodePlugins (buffer) { + const $numPlugins = decodeValue(buffer, 0) + let byteOffset = $numPlugins.byteOffset + + const usedPlugins = new Map() + const lastIndex = $numPlugins.value + for (let index = 1; index <= lastIndex; index++) { + const $name = decodeValue(buffer, byteOffset) + const name = $name.value + byteOffset = $name.byteOffset + + const serializerVersion = decodeValue(buffer, byteOffset).value + usedPlugins.set(index, {name, serializerVersion}) + } + + return usedPlugins +} +exports.decodePlugins = decodePlugins + +function decodeRecord (buffer, byteOffset) { + const $pluginIndex = decodeValue(buffer, byteOffset) + const pluginIndex = $pluginIndex.value + byteOffset = $pluginIndex.byteOffset + + const $id = decodeValue(buffer, byteOffset) + const id = $id.value + byteOffset = $id.byteOffset + + const $numPointers = decodeValue(buffer, byteOffset) + const numPointers = $numPointers.value + byteOffset = $numPointers.byteOffset + + const pointerAddresses = new Array(numPointers) + for (let index = 0; index < numPointers; index++) { + pointerAddresses[index] = buffer.readUInt32LE(byteOffset) + byteOffset += 4 + } + + const state = decodeValue(buffer, byteOffset).value + return {id, pluginIndex, state, pointerAddresses} +} +exports.decodeRecord = decodeRecord + +function extractVersion (buffer) { + return buffer.readUInt16LE(0) +} +exports.extractVersion = extractVersion + +function decode (buffer) { + const rootOffset = buffer.readUInt32LE(2) + const pluginBuffer = buffer.slice(6, rootOffset) + const rootRecord = decodeRecord(buffer, rootOffset) + return {pluginBuffer, rootRecord} +} +exports.decode = decode diff --git a/node_modules/concordance/lib/format.js b/node_modules/concordance/lib/format.js new file mode 100644 index 000000000..7629579d7 --- /dev/null +++ b/node_modules/concordance/lib/format.js @@ -0,0 +1,101 @@ +'use strict' + +const describe = require('./describe') +const lineBuilder = require('./lineBuilder') +const themeUtils = require('./themeUtils') +const Circular = require('./Circular') +const Indenter = require('./Indenter') + +const alwaysFormat = () => true +const fixedIndent = new Indenter(0, ' ') + +function formatDescriptor (subject, options) { + const theme = themeUtils.normalize(options) + if (subject.isPrimitive === true) { + const formatted = subject.formatDeep(themeUtils.applyModifiers(subject, theme), fixedIndent) + return formatted.toString({diff: false}) + } + + const circular = new Circular() + const maxDepth = (options && options.maxDepth) || 0 + + let indent = fixedIndent + + const buffer = lineBuilder.buffer() + const stack = [] + let topIndex = -1 + + do { + if (circular.has(subject)) { + stack[topIndex].formatter.append(lineBuilder.single(theme.circular), subject) + } else { + let didFormat = false + if (typeof subject.formatDeep === 'function') { + const formatted = subject.formatDeep(themeUtils.applyModifiers(subject, theme), indent) + if (formatted !== null) { + didFormat = true + if (topIndex === -1) { + buffer.append(formatted) + } else { + stack[topIndex].formatter.append(formatted, subject) + } + } + } + + if (!didFormat && typeof subject.formatShallow === 'function') { + const formatter = subject.formatShallow(themeUtils.applyModifiers(subject, theme), indent) + const recursor = subject.createRecursor() + + if (formatter.increaseIndent && maxDepth > 0 && indent.level === maxDepth) { + const isEmpty = recursor() === null + const formatted = !isEmpty && typeof formatter.maxDepth === 'function' + ? formatter.maxDepth() + : formatter.finalize() + stack[topIndex].formatter.append(formatted, subject) + } else { + stack.push({ + formatter, + recursor, + decreaseIndent: formatter.increaseIndent, + shouldFormat: formatter.shouldFormat || alwaysFormat, + subject + }) + topIndex++ + + if (formatter.increaseIndent) indent = indent.increase() + circular.add(subject) + } + } + } + + while (topIndex >= 0) { + do { + subject = stack[topIndex].recursor() + } while (subject && !stack[topIndex].shouldFormat(subject)) + + if (subject) { + break + } + + const record = stack.pop() + topIndex-- + if (record.decreaseIndent) indent = indent.decrease() + circular.delete(record.subject) + + const formatted = record.formatter.finalize() + if (topIndex === -1) { + buffer.append(formatted) + } else { + stack[topIndex].formatter.append(formatted, record.subject) + } + } + } while (topIndex >= 0) + + return buffer.toString({diff: false}) +} +exports.formatDescriptor = formatDescriptor + +function format (value, options) { + return formatDescriptor(describe(value, options), options) +} +exports.format = format diff --git a/node_modules/concordance/lib/formatUtils.js b/node_modules/concordance/lib/formatUtils.js new file mode 100644 index 000000000..07e8b2c57 --- /dev/null +++ b/node_modules/concordance/lib/formatUtils.js @@ -0,0 +1,123 @@ +'use strict' + +const lineBuilder = require('./lineBuilder') + +function wrap (fromTheme, value) { + return fromTheme.open + value + fromTheme.close +} +exports.wrap = wrap + +function formatCtorAndStringTag (theme, object) { + if (!object.ctor) return wrap(theme.object.stringTag, object.stringTag) + + let retval = wrap(theme.object.ctor, object.ctor) + if (object.stringTag && object.stringTag !== object.ctor && object.stringTag !== 'Object') { + retval += ' ' + wrap(theme.object.secondaryStringTag, object.stringTag) + } + return retval +} +exports.formatCtorAndStringTag = formatCtorAndStringTag + +class ObjectFormatter { + constructor (object, theme, indent) { + this.object = object + this.theme = theme + this.indent = indent + + this.increaseIndent = true + + this.innerLines = lineBuilder.buffer() + this.pendingStats = null + } + + append (formatted, origin) { + if (origin.isStats === true) { + this.pendingStats = formatted + } else { + if (this.pendingStats !== null) { + if (!this.innerLines.isEmpty) { + this.innerLines.append(this.pendingStats) + } + this.pendingStats = null + } + this.innerLines.append(formatted) + } + } + + finalize () { + const variant = this.object.isList + ? this.theme.list + : this.theme.object + + const ctor = this.object.ctor + const stringTag = this.object.stringTag + const prefix = (ctor === 'Array' || ctor === 'Object') && ctor === stringTag + ? '' + : formatCtorAndStringTag(this.theme, this.object) + ' ' + + if (this.innerLines.isEmpty) { + return lineBuilder.single(prefix + variant.openBracket + variant.closeBracket) + } + + return lineBuilder.first(prefix + variant.openBracket) + .concat(this.innerLines.withFirstPrefixed(this.indent.increase()).stripFlags()) + .append(lineBuilder.last(this.indent + variant.closeBracket)) + } + + maxDepth () { + const variant = this.object.isList + ? this.theme.list + : this.theme.object + + return lineBuilder.single( + formatCtorAndStringTag(this.theme, this.object) + ' ' + variant.openBracket + + ' ' + this.theme.maxDepth + ' ' + variant.closeBracket) + } + + shouldFormat () { + return true + } + + customize (methods) { + if (methods.finalize) { + this.finalize = () => methods.finalize(this.innerLines) + } + if (methods.maxDepth) { + this.maxDepth = methods.maxDepth + } + if (methods.shouldFormat) { + this.shouldFormat = methods.shouldFormat + } + + return this + } +} +exports.ObjectFormatter = ObjectFormatter + +class SingleValueFormatter { + constructor (theme, finalizeFn, increaseIndent) { + this.theme = theme + this.finalizeFn = finalizeFn + this.hasValue = false + this.increaseIndent = increaseIndent === true + this.value = null + } + + append (formatted) { + if (this.hasValue) throw new Error('Formatter buffer can only take one formatted value.') + + this.hasValue = true + this.value = formatted + } + + finalize () { + if (!this.hasValue) throw new Error('Formatter buffer never received a formatted value.') + + return this.finalizeFn(this.value) + } + + maxDepth () { + return this.finalizeFn(lineBuilder.single(this.theme.maxDepth)) + } +} +exports.SingleValueFormatter = SingleValueFormatter diff --git a/node_modules/concordance/lib/getCtor.js b/node_modules/concordance/lib/getCtor.js new file mode 100644 index 000000000..4bbdfd158 --- /dev/null +++ b/node_modules/concordance/lib/getCtor.js @@ -0,0 +1,43 @@ +'use strict' + +const hop = Object.prototype.hasOwnProperty + +function getCtor (stringTag, value) { + if (value.constructor) { + const name = value.constructor.name + return typeof name === 'string' && name !== '' + ? name + : null + } + + if (value.constructor === undefined) { + if (stringTag !== 'Object' || value instanceof Object) return null + + // Values without a constructor, that do not inherit from `Object`, but are + // tagged as objects, may come from `Object.create(null)`. Or they can come + // from a different realm, e.g.: + // + // ``` + // require('vm').runInNewContext(` + // const Foo = function () {} + // Foo.prototype.constructor = undefined + // return new Foo() + // `) + // ``` + // + // Treat such objects as if they came from `Object.create(null)` (in the + // current realm) only if they do not have inherited properties. This allows + // these objects to be compared with object literals. + // + // This means `Object.create(null)` is not differentiated from `{}`. + + // Using `const` prevents Crankshaft optimizations + for (var p in value) { // eslint-disable-line no-var + if (!hop.call(value, p)) return null + } + return stringTag + } + + return null +} +module.exports = getCtor diff --git a/node_modules/concordance/lib/getObjectKeys.js b/node_modules/concordance/lib/getObjectKeys.js new file mode 100644 index 000000000..4b0c3e368 --- /dev/null +++ b/node_modules/concordance/lib/getObjectKeys.js @@ -0,0 +1,36 @@ +'use strict' + +function getObjectKeys (obj, excludeListItemAccessorsBelowLength) { + const keys = [] + let size = 0 + + // Sort property names, they should never be order-sensitive + const nameCandidates = Object.getOwnPropertyNames(obj).sort() + // Comparators should verify symbols in an order-insensitive manner if + // possible. + const symbolCandidates = Object.getOwnPropertySymbols(obj) + + for (let i = 0; i < nameCandidates.length; i++) { + const name = nameCandidates[i] + + let accept = true + if (excludeListItemAccessorsBelowLength > 0) { + const index = Number(name) + accept = (index % 1 !== 0) || index >= excludeListItemAccessorsBelowLength + } + + if (accept && Object.getOwnPropertyDescriptor(obj, name).enumerable) { + keys[size++] = name + } + } + + for (let i = 0; i < symbolCandidates.length; i++) { + const symbol = symbolCandidates[i] + if (Object.getOwnPropertyDescriptor(obj, symbol).enumerable) { + keys[size++] = symbol + } + } + + return { keys, size } +} +module.exports = getObjectKeys diff --git a/node_modules/concordance/lib/getStringTag.js b/node_modules/concordance/lib/getStringTag.js new file mode 100644 index 000000000..994ec4bec --- /dev/null +++ b/node_modules/concordance/lib/getStringTag.js @@ -0,0 +1,30 @@ +'use strict' + +const ts = Object.prototype.toString +function getStringTag (value) { + return ts.call(value).slice(8, -1) +} + +const fts = Function.prototype.toString +const promiseCtorString = fts.call(Promise) +const isPromise = value => { + if (!value.constructor) return false + + try { + return fts.call(value.constructor) === promiseCtorString + } catch (err) { + return false + } +} + +if (getStringTag(Promise.resolve()) === 'Promise') { + module.exports = getStringTag +} else { + const getStringTagWithPromiseWorkaround = value => { + const stringTag = getStringTag(value) + return stringTag === 'Object' && isPromise(value) + ? 'Promise' + : stringTag + } + module.exports = getStringTagWithPromiseWorkaround +} diff --git a/node_modules/concordance/lib/hasLength.js b/node_modules/concordance/lib/hasLength.js new file mode 100644 index 000000000..18df0d369 --- /dev/null +++ b/node_modules/concordance/lib/hasLength.js @@ -0,0 +1,7 @@ +'use strict' + +const hop = Object.prototype.hasOwnProperty +function hasLength (obj) { + return Array.isArray(obj) || (hop.call(obj, 'length') && typeof obj.length === 'number') +} +module.exports = hasLength diff --git a/node_modules/concordance/lib/isEnumerable.js b/node_modules/concordance/lib/isEnumerable.js new file mode 100644 index 000000000..90c2df1e1 --- /dev/null +++ b/node_modules/concordance/lib/isEnumerable.js @@ -0,0 +1,7 @@ +'use strict' + +function isEnumerable (obj, key) { + const desc = Object.getOwnPropertyDescriptor(obj, key) + return desc && desc.enumerable +} +module.exports = isEnumerable diff --git a/node_modules/concordance/lib/lineBuilder.js b/node_modules/concordance/lib/lineBuilder.js new file mode 100644 index 000000000..934ac0095 --- /dev/null +++ b/node_modules/concordance/lib/lineBuilder.js @@ -0,0 +1,309 @@ +'use strict' + +const ACTUAL = Symbol('lineBuilder.gutters.ACTUAL') +const EXPECTED = Symbol('lineBuilder.gutters.EXPECTED') + +function translateGutter (theme, invert, gutter) { + if (invert) { + if (gutter === ACTUAL) return theme.diffGutters.expected + if (gutter === EXPECTED) return theme.diffGutters.actual + } else { + if (gutter === ACTUAL) return theme.diffGutters.actual + if (gutter === EXPECTED) return theme.diffGutters.expected + } + return theme.diffGutters.padding +} + +class Line { + constructor (isFirst, isLast, gutter, stringValue) { + this.isFirst = isFirst + this.isLast = isLast + this.gutter = gutter + this.stringValue = stringValue + } + + * [Symbol.iterator] () { + yield this + } + + get isEmpty () { + return false + } + + get hasGutter () { + return this.gutter !== null + } + + get isSingle () { + return this.isFirst && this.isLast + } + + append (other) { + return this.concat(other) + } + + concat (other) { + return new Collection() + .append(this) + .append(other) + } + + toString (options) { + if (options.diff === false) return this.stringValue + + return translateGutter(options.theme, options.invert, this.gutter) + this.stringValue + } + + mergeWithInfix (infix, other) { + if (other.isLine !== true) { + return new Collection() + .append(this) + .mergeWithInfix(infix, other) + } + + return new Line(this.isFirst, other.isLast, other.gutter, this.stringValue + infix + other.stringValue) + } + + withFirstPrefixed (prefix) { + if (!this.isFirst) return this + + return new Line(true, this.isLast, this.gutter, prefix + this.stringValue) + } + + withLastPostfixed (postfix) { + if (!this.isLast) return this + + return new Line(this.isFirst, true, this.gutter, this.stringValue + postfix) + } + + stripFlags () { + return new Line(false, false, this.gutter, this.stringValue) + } + + decompose () { + return new Collection() + .append(this) + .decompose() + } +} +Object.defineProperty(Line.prototype, 'isLine', {value: true}) + +class Collection { + constructor () { + this.buffer = [] + } + + * [Symbol.iterator] () { + for (const appended of this.buffer) { + for (const line of appended) yield line + } + } + + get isEmpty () { + return this.buffer.length === 0 + } + + get hasGutter () { + for (const line of this) { + if (line.hasGutter) return true + } + return false + } + + get isSingle () { + const iterator = this[Symbol.iterator]() + iterator.next() + return iterator.next().done === true + } + + append (lineOrLines) { + if (!lineOrLines.isEmpty) this.buffer.push(lineOrLines) + return this + } + + concat (other) { + return new Collection() + .append(this) + .append(other) + } + + toString (options) { + let lines = this + + if (options.invert) { + lines = new Collection() + let buffer = new Collection() + + let prev = null + for (const line of this) { + if (line.gutter === ACTUAL) { + if (prev !== null && prev.gutter !== ACTUAL && !buffer.isEmpty) { + lines.append(buffer) + buffer = new Collection() + } + + buffer.append(line) + } else if (line.gutter === EXPECTED) { + lines.append(line) + } else { + if (!buffer.isEmpty) { + lines.append(buffer) + buffer = new Collection() + } + + lines.append(line) + } + + prev = line + } + lines.append(buffer) + } + + return Array.from(lines, line => line.toString(options)).join('\n') + } + + mergeWithInfix (infix, from) { + if (from.isEmpty) throw new Error('Cannot merge, `from` is empty.') + + const otherLines = Array.from(from) + if (!otherLines[0].isFirst) throw new Error('Cannot merge, `from` has no first line.') + + const merged = new Collection() + let seenLast = false + for (const line of this) { + if (seenLast) throw new Error('Cannot merge line, the last line has already been seen.') + + if (!line.isLast) { + merged.append(line) + continue + } + + seenLast = true + for (const other of otherLines) { + if (other.isFirst) { + merged.append(line.mergeWithInfix(infix, other)) + } else { + merged.append(other) + } + } + } + return merged + } + + withFirstPrefixed (prefix) { + return new Collection() + .append(Array.from(this, line => line.withFirstPrefixed(prefix))) + } + + withLastPostfixed (postfix) { + return new Collection() + .append(Array.from(this, line => line.withLastPostfixed(postfix))) + } + + stripFlags () { + return new Collection() + .append(Array.from(this, line => line.stripFlags())) + } + + decompose () { + const first = {actual: new Collection(), expected: new Collection()} + const last = {actual: new Collection(), expected: new Collection()} + const remaining = new Collection() + + for (const line of this) { + if (line.isFirst && line.gutter === ACTUAL) { + first.actual.append(line) + } else if (line.isFirst && line.gutter === EXPECTED) { + first.expected.append(line) + } else if (line.isLast && line.gutter === ACTUAL) { + last.actual.append(line) + } else if (line.isLast && line.gutter === EXPECTED) { + last.expected.append(line) + } else { + remaining.append(line) + } + } + + return {first, last, remaining} + } +} +Object.defineProperty(Collection.prototype, 'isCollection', {value: true}) + +function setDefaultGutter (iterable, gutter) { + return new Collection() + .append(Array.from(iterable, line => { + return line.gutter === null + ? new Line(line.isFirst, line.isLast, gutter, line.stringValue) + : line + })) +} + +module.exports = { + buffer () { + return new Collection() + }, + + first (stringValue) { + return new Line(true, false, null, stringValue) + }, + + last (stringValue) { + return new Line(false, true, null, stringValue) + }, + + line (stringValue) { + return new Line(false, false, null, stringValue) + }, + + single (stringValue) { + return new Line(true, true, null, stringValue) + }, + + setDefaultGutter (lineOrCollection) { + return lineOrCollection + }, + + actual: { + first (stringValue) { + return new Line(true, false, ACTUAL, stringValue) + }, + + last (stringValue) { + return new Line(false, true, ACTUAL, stringValue) + }, + + line (stringValue) { + return new Line(false, false, ACTUAL, stringValue) + }, + + single (stringValue) { + return new Line(true, true, ACTUAL, stringValue) + }, + + setDefaultGutter (lineOrCollection) { + return setDefaultGutter(lineOrCollection, ACTUAL) + } + }, + + expected: { + first (stringValue) { + return new Line(true, false, EXPECTED, stringValue) + }, + + last (stringValue) { + return new Line(false, true, EXPECTED, stringValue) + }, + + line (stringValue) { + return new Line(false, false, EXPECTED, stringValue) + }, + + single (stringValue) { + return new Line(true, true, EXPECTED, stringValue) + }, + + setDefaultGutter (lineOrCollection) { + return setDefaultGutter(lineOrCollection, EXPECTED) + } + } +} diff --git a/node_modules/concordance/lib/metaDescriptors/item.js b/node_modules/concordance/lib/metaDescriptors/item.js new file mode 100644 index 000000000..1539b624a --- /dev/null +++ b/node_modules/concordance/lib/metaDescriptors/item.js @@ -0,0 +1,254 @@ +'use strict' + +const constants = require('../constants') +const formatUtils = require('../formatUtils') +const recursorUtils = require('../recursorUtils') + +const DEEP_EQUAL = constants.DEEP_EQUAL +const UNEQUAL = constants.UNEQUAL + +function describeComplex (index, value) { + return new ComplexItem(index, value) +} +exports.describeComplex = describeComplex + +function deserializeComplex (index, recursor) { + const value = recursor() + return new ComplexItem(index, value) +} +exports.deserializeComplex = deserializeComplex + +function describePrimitive (index, value) { + return new PrimitiveItem(index, value) +} +exports.describePrimitive = describePrimitive + +function deserializePrimitive (state) { + const index = state[0] + const value = state[1] + return new PrimitiveItem(index, value) +} +exports.deserializePrimitive = deserializePrimitive + +const complexTag = Symbol('ComplexItem') +exports.complexTag = complexTag + +const primitiveTag = Symbol('PrimitiveItem') +exports.primitiveTag = primitiveTag + +class ComplexItem { + constructor (index, value) { + this.index = index + this.value = value + } + + createRecursor () { + return recursorUtils.singleValue(this.value) + } + + compare (expected) { + return expected.tag === complexTag && this.index === expected.index + ? this.value.compare(expected.value) + : UNEQUAL + } + + formatShallow (theme, indent) { + const increaseValueIndent = theme.item.increaseValueIndent === true + return new formatUtils.SingleValueFormatter(theme, value => { + if (typeof theme.item.customFormat === 'function') { + return theme.item.customFormat(theme, indent, value) + } + + return value.withLastPostfixed(theme.item.after) + }, increaseValueIndent) + } + + prepareDiff (expected, lhsRecursor, rhsRecursor, compareComplexShape, isCircular) { + // Circular values cannot be compared. They must be treated as being unequal when diffing. + if (isCircular(this.value) || isCircular(expected.value)) return {compareResult: UNEQUAL} + + // Try to line up this or remaining items with the expected items. + const lhsFork = recursorUtils.fork(lhsRecursor) + const rhsFork = recursorUtils.fork(rhsRecursor) + const initialExpected = expected + + let expectedIsMissing = false + while (!expectedIsMissing && expected !== null && expected.isItem === true) { + if (expected.tag === complexTag) { + expectedIsMissing = compareComplexShape(this.value, expected.value) !== UNEQUAL + } + + expected = rhsFork.shared() + } + + let actualIsExtraneous = false + if (initialExpected.tag === complexTag) { + let actual = this + while (!actualIsExtraneous && actual !== null && actual.isItem === true) { + if (actual.tag === complexTag) { + actualIsExtraneous = compareComplexShape(actual.value, initialExpected.value) !== UNEQUAL + } + + actual = lhsFork.shared() + } + } else if (initialExpected.tag === primitiveTag) { + let actual = this + while (!actualIsExtraneous && actual !== null && actual.isItem === true) { + if (actual.tag === primitiveTag) { + actualIsExtraneous = initialExpected.value.compare(actual.value) === DEEP_EQUAL + } + + actual = lhsFork.shared() + } + } + + if (actualIsExtraneous && !expectedIsMissing) { + return { + actualIsExtraneous: true, + lhsRecursor: lhsFork.recursor, + rhsRecursor: recursorUtils.map( + recursorUtils.unshift(rhsFork.recursor, initialExpected), + next => { + if (next.isItem !== true) return next + + next.index++ + return next + }) + } + } + + if (expectedIsMissing && !actualIsExtraneous) { + return { + expectedIsMissing: true, + lhsRecursor: recursorUtils.map( + recursorUtils.unshift(lhsFork.recursor, this), + next => { + if (next.isItem !== true) return next + + next.index++ + return next + }), + rhsRecursor: rhsFork.recursor + } + } + + const mustRecurse = this.tag === complexTag && initialExpected.tag === complexTag && + this.value.compare(initialExpected.value) !== UNEQUAL + return { + mustRecurse, + isUnequal: !mustRecurse, + lhsRecursor: lhsFork.recursor, + rhsRecursor: rhsFork.recursor + } + } + + serialize () { + return this.index + } +} +Object.defineProperty(ComplexItem.prototype, 'isItem', { value: true }) +Object.defineProperty(ComplexItem.prototype, 'tag', { value: complexTag }) + +class PrimitiveItem { + constructor (index, value) { + this.index = index + this.value = value + } + + compare (expected) { + return expected.tag === primitiveTag && this.index === expected.index + ? this.value.compare(expected.value) + : UNEQUAL + } + + formatDeep (theme, indent) { + const increaseValueIndent = theme.item.increaseValueIndent === true + const valueIndent = increaseValueIndent ? indent.increase() : indent + + // Since the value is formatted directly, modifiers are not applied. Apply + // modifiers to the item descriptor instead. + const formatted = this.value.formatDeep(theme, valueIndent) + + if (typeof theme.item.customFormat === 'function') { + return theme.item.customFormat(theme, indent, formatted) + } + + return formatted.withLastPostfixed(theme.item.after) + } + + prepareDiff (expected, lhsRecursor, rhsRecursor, compareComplexShape, isCircular) { + const compareResult = this.compare(expected) + // Short-circuit when values are deeply equal. + if (compareResult === DEEP_EQUAL) return {compareResult} + + // Short-circut when values can be diffed directly. + if ( + expected.tag === primitiveTag && + this.value.tag === expected.value.tag && typeof this.value.diffDeep === 'function' + ) { + return {compareResult} + } + + // Try to line up this or remaining items with the expected items. + const rhsFork = recursorUtils.fork(rhsRecursor) + const initialExpected = expected + + do { + if (expected === null || expected.isItem !== true) { + return { + actualIsExtraneous: true, + rhsRecursor: recursorUtils.map( + recursorUtils.unshift(rhsFork.recursor, initialExpected), + next => { + if (next.isItem !== true) return next + + next.index++ + return next + }) + } + } + + if (this.value.compare(expected.value) === DEEP_EQUAL) { + return { + expectedIsMissing: true, + lhsRecursor: recursorUtils.map( + recursorUtils.unshift(lhsRecursor, this), + next => { + if (next.isItem !== true) return next + + next.index++ + return next + }), + rhsRecursor: rhsFork.recursor + } + } + + expected = rhsFork.shared() + } while (true) + } + + diffDeep (expected, theme, indent) { + // Verify a diff can be returned. + if (this.tag !== expected.tag || typeof this.value.diffDeep !== 'function') return null + + const increaseValueIndent = theme.property.increaseValueIndent === true + const valueIndent = increaseValueIndent ? indent.increase() : indent + + // Since the value is diffed directly, modifiers are not applied. Apply + // modifiers to the item descriptor instead. + const diff = this.value.diffDeep(expected.value, theme, valueIndent) + if (diff === null) return null + + if (typeof theme.item.customFormat === 'function') { + return theme.item.customFormat(theme, indent, diff) + } + + return diff.withLastPostfixed(theme.item.after) + } + + serialize () { + return [this.index, this.value] + } +} +Object.defineProperty(PrimitiveItem.prototype, 'isItem', { value: true }) +Object.defineProperty(PrimitiveItem.prototype, 'tag', { value: primitiveTag }) diff --git a/node_modules/concordance/lib/metaDescriptors/mapEntry.js b/node_modules/concordance/lib/metaDescriptors/mapEntry.js new file mode 100644 index 000000000..f1c100e4e --- /dev/null +++ b/node_modules/concordance/lib/metaDescriptors/mapEntry.js @@ -0,0 +1,223 @@ +'use strict' + +const constants = require('../constants') +const lineBuilder = require('../lineBuilder') +const recursorUtils = require('../recursorUtils') +const themeUtils = require('../themeUtils') + +const DEEP_EQUAL = constants.DEEP_EQUAL +const UNEQUAL = constants.UNEQUAL +const SHALLOW_EQUAL = constants.SHALLOW_EQUAL + +function describe (keyDescriptor, valueDescriptor) { + const keyIsPrimitive = keyDescriptor.isPrimitive === true + const valueIsPrimitive = valueDescriptor.isPrimitive === true + + return new MapEntry(keyDescriptor, valueDescriptor, keyIsPrimitive, valueIsPrimitive) +} +exports.describe = describe + +function deserialize (state, recursor) { + const keyIsPrimitive = state[0] + const valueIsPrimitive = state[1] + const keyDescriptor = recursor() + const valueDescriptor = recursor() + + return new MapEntry(keyDescriptor, valueDescriptor, keyIsPrimitive, valueIsPrimitive) +} +exports.deserialize = deserialize + +const tag = Symbol('MapEntry') +exports.tag = tag + +function mergeWithKey (theme, key, values) { + const lines = lineBuilder.buffer() + const keyRemainder = lineBuilder.buffer() + for (const line of key) { + if (!line.isLast && !line.hasGutter) { + lines.append(line) + } else { + keyRemainder.append(line) + } + } + for (const value of values) { + lines.append(keyRemainder.mergeWithInfix(theme.mapEntry.separator, value).withLastPostfixed(theme.mapEntry.after)) + } + return lines +} + +class MapEntry { + constructor (key, value, keyIsPrimitive, valueIsPrimitive) { + this.key = key + this.value = value + this.keyIsPrimitive = keyIsPrimitive + this.valueIsPrimitive = valueIsPrimitive + } + + createRecursor () { + let emitKey = true + let emitValue = true + + return () => { + if (emitKey) { + emitKey = false + return this.key + } + + if (emitValue) { + emitValue = false + return this.value + } + + return null + } + } + + compare (expected) { + if (this.tag !== expected.tag) return UNEQUAL + if (this.keyIsPrimitive !== expected.keyIsPrimitive) return UNEQUAL + if (this.valueIsPrimitive !== expected.valueIsPrimitive) return UNEQUAL + + if (!this.keyIsPrimitive) return SHALLOW_EQUAL + + const keyResult = this.key.compare(expected.key) + if (keyResult !== DEEP_EQUAL) return keyResult + + if (!this.valueIsPrimitive) return SHALLOW_EQUAL + return this.value.compare(expected.value) + } + + formatDeep (theme, indent) { + // Verify the map entry can be formatted directly. + if (!this.keyIsPrimitive || typeof this.value.formatDeep !== 'function') return null + + // Since formatShallow() would result in theme modifiers being applied + // before the key and value are formatted, do the same here. + const value = this.value.formatDeep(themeUtils.applyModifiersToOriginal(this.value, theme), indent) + if (value === null) return null + + const key = this.key.formatDeep(themeUtils.applyModifiersToOriginal(this.key, theme), indent) + return mergeWithKey(theme, key, [value]) + } + + formatShallow (theme, indent) { + let key = null + const values = [] + return { + append: (formatted, origin) => { + if (this.key === origin) { + key = formatted + } else { + values.push(formatted) + } + }, + finalize () { + return mergeWithKey(theme, key, values) + } + } + } + + diffDeep (expected, theme, indent) { + // Verify a diff can be returned. + if (this.tag !== expected.tag || typeof this.value.diffDeep !== 'function') return null + // Only use this logic to format value diffs when the keys are primitive and equal. + if (!this.keyIsPrimitive || !expected.keyIsPrimitive || this.key.compare(expected.key) !== DEEP_EQUAL) { + return null + } + + // Since formatShallow() would result in theme modifiers being applied + // before the key and value are formatted, do the same here. + const diff = this.value.diffDeep(expected.value, themeUtils.applyModifiersToOriginal(this.value, theme), indent) + if (diff === null) return null + + const key = this.key.formatDeep(themeUtils.applyModifiersToOriginal(this.key, theme), indent, '') + return mergeWithKey(theme, key, [diff]) + } + + prepareDiff (expected, lhsRecursor, rhsRecursor, compareComplexShape, isCircular) { + // Circular values cannot be compared. They must be treated as being unequal when diffing. + if (isCircular(this.value) || isCircular(expected.value)) return {compareResult: UNEQUAL} + + const compareResult = this.compare(expected) + const keysAreEqual = this.tag === expected.tag && this.key.compare(expected.key) === DEEP_EQUAL + // Short-circuit when keys and/or values are deeply equal. + if (compareResult === DEEP_EQUAL || keysAreEqual) return {compareResult} + + // Try to line up this or remaining map entries with the expected entries. + const lhsFork = recursorUtils.fork(lhsRecursor) + const rhsFork = recursorUtils.fork(rhsRecursor) + const initialExpected = expected + + let expectedIsMissing = false + while (!expectedIsMissing && expected !== null && this.tag === expected.tag) { + if (expected.keyIsPrimitive) { + expectedIsMissing = this.key.compare(expected.key) !== UNEQUAL + } else { + expectedIsMissing = compareComplexShape(this.key, expected.key) !== UNEQUAL + } + + expected = rhsFork.shared() + } + + let actualIsExtraneous = false + if (this.tag === initialExpected.tag) { + if (initialExpected.keyIsPrimitive) { + let actual = this + while (!actualIsExtraneous && actual !== null && this.tag === actual.tag) { + if (actual.keyIsPrimitive) { + actualIsExtraneous = initialExpected.key.compare(actual.key) === DEEP_EQUAL + } + + actual = lhsFork.shared() + } + } else { + let actual = this + while (!actualIsExtraneous && actual !== null && this.tag === actual.tag) { + if (!actual.keyIsPrimitive) { + actualIsExtraneous = compareComplexShape(actual.key, initialExpected.key) !== UNEQUAL + } + + actual = lhsFork.shared() + } + } + } + + if (actualIsExtraneous && !expectedIsMissing) { + return { + actualIsExtraneous: true, + lhsRecursor: lhsFork.recursor, + rhsRecursor: recursorUtils.unshift(rhsFork.recursor, initialExpected) + } + } + + if (expectedIsMissing && !actualIsExtraneous) { + return { + expectedIsMissing: true, + lhsRecursor: recursorUtils.unshift(lhsFork.recursor, this), + rhsRecursor: rhsFork.recursor + } + } + + let mustRecurse = false + if (!this.keyIsPrimitive && !initialExpected.keyIsPrimitive) { + if (this.valueIsPrimitive || initialExpected.valueIsPrimitive) { + mustRecurse = this.value.compare(initialExpected.value) !== UNEQUAL + } else { + mustRecurse = compareComplexShape(this.value, initialExpected.value) !== UNEQUAL + } + } + + return { + mustRecurse, + isUnequal: !mustRecurse, + lhsRecursor: lhsFork.recursor, + rhsRecursor: rhsFork.recursor + } + } + + serialize () { + return [this.keyIsPrimitive, this.valueIsPrimitive] + } +} +Object.defineProperty(MapEntry.prototype, 'isMapEntry', { value: true }) +Object.defineProperty(MapEntry.prototype, 'tag', { value: tag }) diff --git a/node_modules/concordance/lib/metaDescriptors/pointer.js b/node_modules/concordance/lib/metaDescriptors/pointer.js new file mode 100644 index 000000000..f569d2891 --- /dev/null +++ b/node_modules/concordance/lib/metaDescriptors/pointer.js @@ -0,0 +1,31 @@ +'use strict' + +const UNEQUAL = require('../constants').UNEQUAL + +function describe (index) { + return new Pointer(index) +} +exports.describe = describe + +exports.deserialize = describe + +const tag = Symbol('Pointer') +exports.tag = tag + +class Pointer { + constructor (index) { + this.index = index + } + + // Pointers cannot be compared, and are not expected to be part of the + // comparisons. + compare (expected) { + return UNEQUAL + } + + serialize () { + return this.index + } +} +Object.defineProperty(Pointer.prototype, 'isPointer', { value: true }) +Object.defineProperty(Pointer.prototype, 'tag', { value: tag }) diff --git a/node_modules/concordance/lib/metaDescriptors/property.js b/node_modules/concordance/lib/metaDescriptors/property.js new file mode 100644 index 000000000..b5d34271d --- /dev/null +++ b/node_modules/concordance/lib/metaDescriptors/property.js @@ -0,0 +1,190 @@ +'use strict' + +const constants = require('../constants') +const formatUtils = require('../formatUtils') +const recursorUtils = require('../recursorUtils') +const symbolPrimitive = require('../primitiveValues/symbol').tag + +const AMBIGUOUS = constants.AMBIGUOUS +const DEEP_EQUAL = constants.DEEP_EQUAL +const UNEQUAL = constants.UNEQUAL + +function describeComplex (key, value) { + return new ComplexProperty(key, value) +} +exports.describeComplex = describeComplex + +function deserializeComplex (key, recursor) { + const value = recursor() + return new ComplexProperty(key, value) +} +exports.deserializeComplex = deserializeComplex + +function describePrimitive (key, value) { + return new PrimitiveProperty(key, value) +} +exports.describePrimitive = describePrimitive + +function deserializePrimitive (state) { + const key = state[0] + const value = state[1] + return new PrimitiveProperty(key, value) +} +exports.deserializePrimitive = deserializePrimitive + +const complexTag = Symbol('ComplexProperty') +exports.complexTag = complexTag + +const primitiveTag = Symbol('PrimitiveProperty') +exports.primitiveTag = primitiveTag + +class Property { + constructor (key) { + this.key = key + } + + compareKeys (expected) { + const result = this.key.compare(expected.key) + // Return AMBIGUOUS if symbol keys are unequal. It's likely that properties + // are compared in order of declaration, which is not the desired strategy. + // Returning AMBIGUOUS allows compare() and diff() to recognize this + // situation and sort the symbol properties before comparing them. + return result === UNEQUAL && this.key.tag === symbolPrimitive && expected.key.tag === symbolPrimitive + ? AMBIGUOUS + : result + } + + prepareDiff (expected, lhsRecursor, rhsRecursor, compareComplexShape, isCircular) { + // Circular values cannot be compared. They must be treated as being unequal when diffing. + if (isCircular(this.value) || isCircular(expected.value)) return {compareResult: UNEQUAL} + + // Try to line up this or remaining properties with the expected properties. + const rhsFork = recursorUtils.fork(rhsRecursor) + const initialExpected = expected + + do { + if (expected === null || expected.isProperty !== true) { + return { + actualIsExtraneous: true, + rhsRecursor: recursorUtils.unshift(rhsFork.recursor, initialExpected) + } + } else if (this.key.compare(expected.key) === DEEP_EQUAL) { + if (expected === initialExpected) { + return null + } else { + return { + expectedIsMissing: true, + lhsRecursor: recursorUtils.unshift(lhsRecursor, this), + rhsRecursor: rhsFork.recursor + } + } + } + + expected = rhsFork.shared() + } while (true) + } +} +Object.defineProperty(Property.prototype, 'isProperty', { value: true }) + +class ComplexProperty extends Property { + constructor (key, value) { + super(key) + this.value = value + } + + createRecursor () { + return recursorUtils.singleValue(this.value) + } + + compare (expected) { + if (expected.isProperty !== true) return UNEQUAL + + const keyResult = this.compareKeys(expected) + if (keyResult !== DEEP_EQUAL) return keyResult + + return this.tag === expected.tag + ? this.value.compare(expected.value) + : UNEQUAL + } + + formatShallow (theme, indent) { + const increaseValueIndent = theme.property.increaseValueIndent === true + return new formatUtils.SingleValueFormatter(theme, value => { + if (typeof theme.property.customFormat === 'function') { + return theme.property.customFormat(theme, indent, this.key, value) + } + + return value + .withFirstPrefixed(this.key.formatAsKey(theme) + theme.property.separator) + .withLastPostfixed(theme.property.after) + }, increaseValueIndent) + } + + serialize () { + return this.key + } +} +Object.defineProperty(ComplexProperty.prototype, 'tag', { value: complexTag }) + +class PrimitiveProperty extends Property { + constructor (key, value) { + super(key) + this.value = value + } + + compare (expected) { + if (expected.isProperty !== true) return UNEQUAL + + const keyResult = this.compareKeys(expected) + if (keyResult !== DEEP_EQUAL) return keyResult + + return this.tag !== expected.tag + ? UNEQUAL + : this.value.compare(expected.value) + } + + formatDeep (theme, indent) { + const increaseValueIndent = theme.property.increaseValueIndent === true + const valueIndent = increaseValueIndent ? indent.increase() : indent + + // Since the key and value are formatted directly, modifiers are not + // applied. Apply modifiers to the property descriptor instead. + const formatted = this.value.formatDeep(theme, valueIndent) + + if (typeof theme.property.customFormat === 'function') { + return theme.property.customFormat(theme, indent, this.key, formatted) + } + + return formatted + .withFirstPrefixed(this.key.formatAsKey(theme) + theme.property.separator) + .withLastPostfixed(theme.property.after) + } + + diffDeep (expected, theme, indent) { + // Verify a diff can be returned. + if (this.tag !== expected.tag || typeof this.value.diffDeep !== 'function') return null + // Only use this logic to diff values when the keys are the same. + if (this.key.compare(expected.key) !== DEEP_EQUAL) return null + + const increaseValueIndent = theme.property.increaseValueIndent === true + const valueIndent = increaseValueIndent ? indent.increase() : indent + + // Since the key and value are diffed directly, modifiers are not + // applied. Apply modifiers to the property descriptor instead. + const diff = this.value.diffDeep(expected.value, theme, valueIndent) + if (diff === null) return null + + if (typeof theme.property.customFormat === 'function') { + return theme.property.customFormat(theme, indent, this.key, diff) + } + + return diff + .withFirstPrefixed(this.key.formatAsKey(theme) + theme.property.separator) + .withLastPostfixed(theme.property.after) + } + + serialize () { + return [this.key, this.value] + } +} +Object.defineProperty(PrimitiveProperty.prototype, 'tag', { value: primitiveTag }) diff --git a/node_modules/concordance/lib/metaDescriptors/stats.js b/node_modules/concordance/lib/metaDescriptors/stats.js new file mode 100644 index 000000000..93bde9d1e --- /dev/null +++ b/node_modules/concordance/lib/metaDescriptors/stats.js @@ -0,0 +1,136 @@ +'use strict' + +const constants = require('../constants') +const lineBuilder = require('../lineBuilder') +const recursorUtils = require('../recursorUtils') + +const DEEP_EQUAL = constants.DEEP_EQUAL +const UNEQUAL = constants.UNEQUAL + +function describeIterableRecursor (recursor) { + return new IterableStats(recursor.size) +} +exports.describeIterableRecursor = describeIterableRecursor + +function describeListRecursor (recursor) { + return new ListStats(recursor.size) +} +exports.describeListRecursor = describeListRecursor + +function describePropertyRecursor (recursor) { + return new PropertyStats(recursor.size) +} +exports.describePropertyRecursor = describePropertyRecursor + +function deserializeIterableStats (size) { + return new IterableStats(size) +} +exports.deserializeIterableStats = deserializeIterableStats + +function deserializeListStats (size) { + return new ListStats(size) +} +exports.deserializeListStats = deserializeListStats + +function deserializePropertyStats (size) { + return new PropertyStats(size) +} +exports.deserializePropertyStats = deserializePropertyStats + +const iterableTag = Symbol('IterableStats') +exports.iterableTag = iterableTag + +const listTag = Symbol('ListStats') +exports.listTag = listTag + +const propertyTag = Symbol('PropertyStats') +exports.propertyTag = propertyTag + +class Stats { + constructor (size) { + this.size = size + } + + formatDeep (theme) { + return lineBuilder.single(theme.stats.separator) + } + + prepareDiff (expected, lhsRecursor, rhsRecursor, compareComplexShape) { + if (expected.isStats !== true || expected.tag === this.tag) return null + + // Try to line up stats descriptors with the same tag. + const rhsFork = recursorUtils.fork(rhsRecursor) + const initialExpected = expected + + const missing = [] + while (expected !== null && this.tag !== expected.tag) { + missing.push(expected) + expected = rhsFork.shared() + } + + if (expected !== null && missing.length > 0) { + return { + multipleAreMissing: true, + descriptors: missing, + lhsRecursor: recursorUtils.unshift(lhsRecursor, this), + // Use original `rhsRecursor`, not `rhsFork`, since the consumed + // descriptors are returned with the `missing` array. + rhsRecursor: recursorUtils.unshift(rhsRecursor, expected) + } + } + + const lhsFork = recursorUtils.fork(lhsRecursor) + let actual = this + + const extraneous = [] + while (actual !== null && actual.tag !== initialExpected.tag) { + extraneous.push(actual) + actual = lhsFork.shared() + } + + if (actual !== null && extraneous.length > 0) { + return { + multipleAreExtraneous: true, + descriptors: extraneous, + // Use original `lhsRecursor`, not `lhsFork`, since the consumed + // descriptors are returned with the `extraneous` array. + lhsRecursor: recursorUtils.unshift(lhsRecursor, actual), + rhsRecursor: recursorUtils.unshift(rhsFork.recursor, initialExpected) + } + } + + return null + } + + serialize () { + return this.size + } +} +Object.defineProperty(Stats.prototype, 'isStats', { value: true }) + +class IterableStats extends Stats { + compare (expected) { + return expected.tag === iterableTag && this.size === expected.size + ? DEEP_EQUAL + : UNEQUAL + } +} +Object.defineProperty(IterableStats.prototype, 'tag', { value: iterableTag }) + +class ListStats extends Stats { + compare (expected) { + return expected.tag === listTag && this.size === expected.size + ? DEEP_EQUAL + : UNEQUAL + } +} +Object.defineProperty(ListStats.prototype, 'tag', { value: listTag }) + +class PropertyStats extends Stats { + compare (expected) { + return expected.tag === propertyTag && this.size === expected.size + ? DEEP_EQUAL + : UNEQUAL + } +} +Object.defineProperty(PropertyStats.prototype, 'tag', { value: propertyTag }) diff --git a/node_modules/concordance/lib/pluginRegistry.js b/node_modules/concordance/lib/pluginRegistry.js new file mode 100644 index 000000000..b4fb76b37 --- /dev/null +++ b/node_modules/concordance/lib/pluginRegistry.js @@ -0,0 +1,222 @@ +'use strict' + +const semver = require('semver') + +const pkg = require('../package.json') +const constants = require('./constants') +const object = require('./complexValues/object') +const lineBuilder = require('./lineBuilder') +const formatUtils = require('./formatUtils') +const itemDescriptor = require('./metaDescriptors/item') +const propertyDescriptor = require('./metaDescriptors/property') +const stringDescriptor = require('./primitiveValues/string') +const recursorUtils = require('./recursorUtils') +const themeUtils = require('./themeUtils') + +const API_VERSION = 1 +const CONCORDANCE_VERSION = pkg.version + +const descriptorRegistry = new Map() +const registry = new Map() + +class PluginError extends Error { + constructor (message, plugin) { + super(message) + this.name = 'PluginError' + this.plugin = plugin + } +} + +class PluginTypeError extends TypeError { + constructor (message, plugin) { + super(message) + this.name = 'PluginTypeError' + this.plugin = plugin + } +} + +class UnsupportedApiError extends PluginError { + constructor (plugin) { + super('Plugin requires an unsupported API version', plugin) + this.name = 'UnsupportedApiError' + } +} + +class UnsupportedError extends PluginError { + constructor (plugin) { + super('Plugin does not support this version of Concordance', plugin) + this.name = 'UnsupportedError' + } +} + +class DuplicateDescriptorTagError extends PluginError { + constructor (tag, plugin) { + super(`Could not add descriptor: tag ${String(tag)} has already been registered`, plugin) + this.name = 'DuplicateDescriptorTagError' + this.tag = tag + } +} + +class DuplicateDescriptorIdError extends PluginError { + constructor (id, plugin) { + const printed = typeof id === 'number' + ? `0x${id.toString(16).toUpperCase()}` + : String(id) + super(`Could not add descriptor: id ${printed} has already been registered`, plugin) + this.name = 'DuplicateDescriptorIdError' + this.id = id + } +} + +function verify (plugin) { + if (typeof plugin.name !== 'string' || !plugin.name) { + throw new PluginTypeError('Plugin must have a `name`', plugin) + } + + if (plugin.apiVersion !== API_VERSION) { + throw new UnsupportedApiError(plugin) + } + + if ('minimalConcordanceVersion' in plugin) { + if (!semver.valid(plugin.minimalConcordanceVersion)) { + throw new PluginTypeError('If specified, `minimalConcordanceVersion` must be a valid SemVer version', plugin) + } + + const range = `>=${plugin.minimalConcordanceVersion}` + if (!semver.satisfies(CONCORDANCE_VERSION, range)) { + throw new UnsupportedError(plugin) + } + } +} + +// Selectively expose descriptor tags. +const publicDescriptorTags = Object.freeze({ + complexItem: itemDescriptor.complexTag, + primitiveItem: itemDescriptor.primitiveTag, + primitiveProperty: propertyDescriptor.primitiveTag, + string: stringDescriptor.tag +}) + +// Don't expose `setDefaultGutter()`. +const publicLineBuilder = Object.freeze({ + buffer: lineBuilder.buffer, + first: lineBuilder.first, + last: lineBuilder.last, + line: lineBuilder.line, + single: lineBuilder.single, + actual: Object.freeze({ + buffer: lineBuilder.actual.buffer, + first: lineBuilder.actual.first, + last: lineBuilder.actual.last, + line: lineBuilder.actual.line, + single: lineBuilder.actual.single + }), + expected: Object.freeze({ + buffer: lineBuilder.expected.buffer, + first: lineBuilder.expected.first, + last: lineBuilder.expected.last, + line: lineBuilder.expected.line, + single: lineBuilder.expected.single + }) +}) + +function modifyTheme (descriptor, modifier) { + themeUtils.addModifier(descriptor, modifier) + return descriptor +} + +function add (plugin) { + verify(plugin) + + const name = plugin.name + if (registry.has(name)) return registry.get(name) + + const id2deserialize = new Map() + const tag2id = new Map() + const addDescriptor = (id, tag, deserialize) => { + if (id2deserialize.has(id)) throw new DuplicateDescriptorIdError(id, plugin) + if (descriptorRegistry.has(tag) || tag2id.has(tag)) throw new DuplicateDescriptorTagError(tag, plugin) + + id2deserialize.set(id, deserialize) + tag2id.set(tag, id) + } + + const tryDescribeValue = plugin.register({ + // Concordance makes assumptions about when AMBIGUOUS occurs. Do not expose + // it to plugins. + UNEQUAL: constants.UNEQUAL, + SHALLOW_EQUAL: constants.SHALLOW_EQUAL, + DEEP_EQUAL: constants.DEEP_EQUAL, + + ObjectValue: object.ObjectValue, + DescribedMixin: object.DescribedMixin, + DeserializedMixin: object.DeserializedMixin, + + addDescriptor, + applyThemeModifiers: themeUtils.applyModifiers, + descriptorTags: publicDescriptorTags, + lineBuilder: publicLineBuilder, + mapRecursor: recursorUtils.map, + modifyTheme, + wrapFromTheme: formatUtils.wrap + }) + + const registered = { + id2deserialize, + serializerVersion: plugin.serializerVersion, + name, + tag2id, + theme: plugin.theme || {}, + tryDescribeValue + } + + registry.set(name, registered) + for (const tag of tag2id.keys()) { + descriptorRegistry.set(tag, registered) + } + + return registered +} +exports.add = add + +function getDeserializers (plugins) { + return plugins.map(plugin => { + const registered = add(plugin) + return { + id2deserialize: registered.id2deserialize, + name: registered.name, + serializerVersion: registered.serializerVersion + } + }) +} +exports.getDeserializers = getDeserializers + +function getThemes (plugins) { + return plugins.map(plugin => { + const registered = add(plugin) + return { + name: registered.name, + theme: registered.theme + } + }) +} +exports.getThemes = getThemes + +function getTryDescribeValues (plugins) { + return plugins.map(plugin => add(plugin).tryDescribeValue) +} +exports.getTryDescribeValues = getTryDescribeValues + +function resolveDescriptorRef (tag) { + if (!descriptorRegistry.has(tag)) return null + + const registered = descriptorRegistry.get(tag) + return { + id: registered.tag2id.get(tag), + name: registered.name, + serialization: { + serializerVersion: registered.serializerVersion + } + } +} +exports.resolveDescriptorRef = resolveDescriptorRef diff --git a/node_modules/concordance/lib/primitiveValues/boolean.js b/node_modules/concordance/lib/primitiveValues/boolean.js new file mode 100644 index 000000000..7bad50408 --- /dev/null +++ b/node_modules/concordance/lib/primitiveValues/boolean.js @@ -0,0 +1,40 @@ +'use strict' + +const constants = require('../constants') +const formatUtils = require('../formatUtils') +const lineBuilder = require('../lineBuilder') + +const DEEP_EQUAL = constants.DEEP_EQUAL +const UNEQUAL = constants.UNEQUAL + +function describe (value) { + return new BooleanValue(value) +} +exports.describe = describe + +exports.deserialize = describe + +const tag = Symbol('BooleanValue') +exports.tag = tag + +class BooleanValue { + constructor (value) { + this.value = value + } + + compare (expected) { + return this.tag === expected.tag && this.value === expected.value + ? DEEP_EQUAL + : UNEQUAL + } + + formatDeep (theme) { + return lineBuilder.single(formatUtils.wrap(theme.boolean, this.value === true ? 'true' : 'false')) + } + + serialize () { + return this.value + } +} +Object.defineProperty(BooleanValue.prototype, 'isPrimitive', { value: true }) +Object.defineProperty(BooleanValue.prototype, 'tag', { value: tag }) diff --git a/node_modules/concordance/lib/primitiveValues/null.js b/node_modules/concordance/lib/primitiveValues/null.js new file mode 100644 index 000000000..9436ed9a1 --- /dev/null +++ b/node_modules/concordance/lib/primitiveValues/null.js @@ -0,0 +1,32 @@ +'use strict' + +const constants = require('../constants') +const formatUtils = require('../formatUtils') +const lineBuilder = require('../lineBuilder') + +const DEEP_EQUAL = constants.DEEP_EQUAL +const UNEQUAL = constants.UNEQUAL + +function describe () { + return new NullValue() +} +exports.describe = describe + +exports.deserialize = describe + +const tag = Symbol('NullValue') +exports.tag = tag + +class NullValue { + compare (expected) { + return expected.tag === tag + ? DEEP_EQUAL + : UNEQUAL + } + + formatDeep (theme) { + return lineBuilder.single(formatUtils.wrap(theme.null, 'null')) + } +} +Object.defineProperty(NullValue.prototype, 'isPrimitive', { value: true }) +Object.defineProperty(NullValue.prototype, 'tag', { value: tag }) diff --git a/node_modules/concordance/lib/primitiveValues/number.js b/node_modules/concordance/lib/primitiveValues/number.js new file mode 100644 index 000000000..d1dec8edb --- /dev/null +++ b/node_modules/concordance/lib/primitiveValues/number.js @@ -0,0 +1,41 @@ +'use strict' + +const constants = require('../constants') +const formatUtils = require('../formatUtils') +const lineBuilder = require('../lineBuilder') + +const DEEP_EQUAL = constants.DEEP_EQUAL +const UNEQUAL = constants.UNEQUAL + +function describe (value) { + return new NumberValue(value) +} +exports.describe = describe + +exports.deserialize = describe + +const tag = Symbol('NumberValue') +exports.tag = tag + +class NumberValue { + constructor (value) { + this.value = value + } + + compare (expected) { + return expected.tag === tag && Object.is(this.value, expected.value) + ? DEEP_EQUAL + : UNEQUAL + } + + formatDeep (theme) { + const string = Object.is(this.value, -0) ? '-0' : String(this.value) + return lineBuilder.single(formatUtils.wrap(theme.number, string)) + } + + serialize () { + return this.value + } +} +Object.defineProperty(NumberValue.prototype, 'isPrimitive', { value: true }) +Object.defineProperty(NumberValue.prototype, 'tag', { value: tag }) diff --git a/node_modules/concordance/lib/primitiveValues/string.js b/node_modules/concordance/lib/primitiveValues/string.js new file mode 100644 index 000000000..af120022f --- /dev/null +++ b/node_modules/concordance/lib/primitiveValues/string.js @@ -0,0 +1,306 @@ +'use strict' + +const fastDiff = require('fast-diff') +const keyword = require('esutils').keyword + +const constants = require('../constants') +const formatUtils = require('../formatUtils') +const lineBuilder = require('../lineBuilder') + +const DEEP_EQUAL = constants.DEEP_EQUAL +const UNEQUAL = constants.UNEQUAL + +function describe (value) { + return new StringValue(value) +} +exports.describe = describe + +exports.deserialize = describe + +const tag = Symbol('StringValue') +exports.tag = tag + +// TODO: Escape invisible characters (e.g. zero-width joiner, non-breaking space), +// ambiguous characters (other kinds of spaces, combining characters). Use +// http://graphemica.com/blocks/control-pictures where applicable. +function basicEscape (string) { + return string.replace(/\\/g, '\\\\') +} + +const CRLF_CONTROL_PICTURE = '\u240D\u240A' +const LF_CONTROL_PICTURE = '\u240A' +const CR_CONTROL_PICTURE = '\u240D' + +const MATCH_CONTROL_PICTURES = new RegExp(`${CR_CONTROL_PICTURE}|${LF_CONTROL_PICTURE}|${CR_CONTROL_PICTURE}`, 'g') + +function escapeLinebreak (string) { + if (string === '\r\n') return CRLF_CONTROL_PICTURE + if (string === '\n') return LF_CONTROL_PICTURE + if (string === '\r') return CR_CONTROL_PICTURE + return string +} + +function themeControlPictures (theme, resetWrap, str) { + return str.replace(MATCH_CONTROL_PICTURES, picture => { + return resetWrap.close + formatUtils.wrap(theme.string.controlPicture, picture) + resetWrap.open + }) +} + +const MATCH_SINGLE_QUOTE = /'/g +const MATCH_DOUBLE_QUOTE = /"/g +const MATCH_BACKTICKS = /`/g +function escapeQuotes (line, string) { + const quote = line.escapeQuote + if (quote === '\'') return string.replace(MATCH_SINGLE_QUOTE, "\\'") + if (quote === '"') return string.replace(MATCH_DOUBLE_QUOTE, '\\"') + if (quote === '`') return string.replace(MATCH_BACKTICKS, '\\`') + return string +} + +function includesLinebreaks (string) { + return string.includes('\r') || string.includes('\n') +} + +function diffLine (theme, actual, expected) { + const outcome = fastDiff(actual, expected) + + // TODO: Compute when line is mostly unequal (80%? 90%?) and treat it as being + // completely unequal. + const isPartiallyEqual = !( + (outcome.length === 2 && outcome[0][1] === actual && outcome[1][1] === expected) || + // Discount line ending control pictures, which will be equal even when the + // rest of the line isn't. + ( + outcome.length === 3 && + outcome[2][0] === fastDiff.EQUAL && + MATCH_CONTROL_PICTURES.test(outcome[2][1]) && + outcome[0][1] + outcome[2][1] === actual && + outcome[1][1] + outcome[2][1] === expected + ) + ) + + let stringActual = '' + let stringExpected = '' + + const noopWrap = { open: '', close: '' } + const deleteWrap = isPartiallyEqual ? theme.string.diff.delete : noopWrap + const insertWrap = isPartiallyEqual ? theme.string.diff.insert : noopWrap + const equalWrap = isPartiallyEqual ? theme.string.diff.equal : noopWrap + for (const diff of outcome) { + if (diff[0] === fastDiff.DELETE) { + stringActual += formatUtils.wrap(deleteWrap, diff[1]) + } else if (diff[0] === fastDiff.INSERT) { + stringExpected += formatUtils.wrap(insertWrap, diff[1]) + } else { + const string = formatUtils.wrap(equalWrap, themeControlPictures(theme, equalWrap, diff[1])) + stringActual += string + stringExpected += string + } + } + + if (!isPartiallyEqual) { + stringActual = formatUtils.wrap(theme.string.diff.deleteLine, stringActual) + stringExpected = formatUtils.wrap(theme.string.diff.insertLine, stringExpected) + } + + return [stringActual, stringExpected] +} + +const LINEBREAKS = /\r\n|\r|\n/g + +function gatherLines (string) { + const lines = [] + let prevIndex = 0 + for (let match; (match = LINEBREAKS.exec(string)); prevIndex = match.index + match[0].length) { + lines.push(string.slice(prevIndex, match.index) + escapeLinebreak(match[0])) + } + lines.push(string.slice(prevIndex)) + return lines +} + +class StringValue { + constructor (value) { + this.value = value + } + + compare (expected) { + return expected.tag === tag && this.value === expected.value + ? DEEP_EQUAL + : UNEQUAL + } + + get includesLinebreaks () { + return includesLinebreaks(this.value) + } + + formatDeep (theme, indent) { + // Escape backslashes + let escaped = basicEscape(this.value) + + if (!this.includesLinebreaks) { + escaped = escapeQuotes(theme.string.line, escaped) + return lineBuilder.single(formatUtils.wrap(theme.string.line, formatUtils.wrap(theme.string, escaped))) + } + + escaped = escapeQuotes(theme.string.multiline, escaped) + const lineStrings = gatherLines(escaped).map(string => { + return formatUtils.wrap(theme.string, themeControlPictures(theme, theme.string, string)) + }) + const lastIndex = lineStrings.length - 1 + const indentation = indent + return lineBuilder.buffer() + .append( + lineStrings.map((string, index) => { + if (index === 0) return lineBuilder.first(theme.string.multiline.start + string) + if (index === lastIndex) return lineBuilder.last(indentation + string + theme.string.multiline.end) + return lineBuilder.line(indentation + string) + })) + } + + formatAsKey (theme) { + const key = this.value + if (keyword.isIdentifierNameES6(key, true) || String(parseInt(key, 10)) === key) { + return key + } + + const escaped = basicEscape(key) + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/'/g, "\\'") + return formatUtils.wrap(theme.string.line, formatUtils.wrap(theme.string, escaped)) + } + + diffDeep (expected, theme, indent) { + if (expected.tag !== tag) return null + + const escapedActual = basicEscape(this.value) + const escapedExpected = basicEscape(expected.value) + + if (!includesLinebreaks(escapedActual) && !includesLinebreaks(escapedExpected)) { + const result = diffLine(theme, + escapeQuotes(theme.string.line, escapedActual), + escapeQuotes(theme.string.line, escapedExpected)) + + return lineBuilder.actual.single(formatUtils.wrap(theme.string.line, result[0])) + .concat(lineBuilder.expected.single(formatUtils.wrap(theme.string.line, result[1]))) + } + + const actualLines = gatherLines(escapeQuotes(theme.string.multiline, escapedActual)) + const expectedLines = gatherLines(escapeQuotes(theme.string.multiline, escapedExpected)) + + const indentation = indent + const lines = lineBuilder.buffer() + const lastActualIndex = actualLines.length - 1 + const lastExpectedIndex = expectedLines.length - 1 + + let actualBuffer = [] + let expectedBuffer = [] + let mustOpenNextExpected = false + for (let actualIndex = 0, expectedIndex = 0, extraneousOffset = 0; actualIndex < actualLines.length;) { + if (actualLines[actualIndex] === expectedLines[expectedIndex]) { + lines.append(actualBuffer) + lines.append(expectedBuffer) + actualBuffer = [] + expectedBuffer = [] + + let string = actualLines[actualIndex] + string = themeControlPictures(theme, theme.string.diff.equal, string) + string = formatUtils.wrap(theme.string.diff.equal, string) + + if (actualIndex === 0) { + lines.append(lineBuilder.first(theme.string.multiline.start + string)) + } else if (actualIndex === lastActualIndex && expectedIndex === lastExpectedIndex) { + lines.append(lineBuilder.last(indentation + string + theme.string.multiline.end)) + } else { + lines.append(lineBuilder.line(indentation + string)) + } + + actualIndex++ + expectedIndex++ + continue + } + + let expectedIsMissing = false + { + const compare = actualLines[actualIndex] + for (let index = expectedIndex; !expectedIsMissing && index < expectedLines.length; index++) { + expectedIsMissing = compare === expectedLines[index] + } + } + + let actualIsExtraneous = (actualIndex - extraneousOffset) > lastExpectedIndex + if (!actualIsExtraneous) { + const compare = expectedLines[expectedIndex] + for (let index = actualIndex; !actualIsExtraneous && index < actualLines.length; index++) { + actualIsExtraneous = compare === actualLines[index] + } + + if (!actualIsExtraneous && (actualIndex - extraneousOffset) === lastExpectedIndex && actualIndex < lastActualIndex) { + actualIsExtraneous = true + } + } + + if (actualIsExtraneous && !expectedIsMissing) { + const string = formatUtils.wrap(theme.string.diff.deleteLine, actualLines[actualIndex]) + + if (actualIndex === 0) { + actualBuffer.push(lineBuilder.actual.first(theme.string.multiline.start + string)) + mustOpenNextExpected = true + } else if (actualIndex === lastActualIndex) { + actualBuffer.push(lineBuilder.actual.last(indentation + string + theme.string.multiline.end)) + } else { + actualBuffer.push(lineBuilder.actual.line(indentation + string)) + } + + actualIndex++ + extraneousOffset++ + } else if (expectedIsMissing && !actualIsExtraneous) { + const string = formatUtils.wrap(theme.string.diff.insertLine, expectedLines[expectedIndex]) + + if (mustOpenNextExpected) { + expectedBuffer.push(lineBuilder.expected.first(theme.string.multiline.start + string)) + mustOpenNextExpected = false + } else if (expectedIndex === lastExpectedIndex) { + expectedBuffer.push(lineBuilder.expected.last(indentation + string + theme.string.multiline.end)) + } else { + expectedBuffer.push(lineBuilder.expected.line(indentation + string)) + } + + expectedIndex++ + } else { + const result = diffLine(theme, actualLines[actualIndex], expectedLines[expectedIndex]) + + if (actualIndex === 0) { + actualBuffer.push(lineBuilder.actual.first(theme.string.multiline.start + result[0])) + mustOpenNextExpected = true + } else if (actualIndex === lastActualIndex) { + actualBuffer.push(lineBuilder.actual.last(indentation + result[0] + theme.string.multiline.end)) + } else { + actualBuffer.push(lineBuilder.actual.line(indentation + result[0])) + } + + if (mustOpenNextExpected) { + expectedBuffer.push(lineBuilder.expected.first(theme.string.multiline.start + result[1])) + mustOpenNextExpected = false + } else if (expectedIndex === lastExpectedIndex) { + expectedBuffer.push(lineBuilder.expected.last(indentation + result[1] + theme.string.multiline.end)) + } else { + expectedBuffer.push(lineBuilder.expected.line(indentation + result[1])) + } + + actualIndex++ + expectedIndex++ + } + } + + lines.append(actualBuffer) + lines.append(expectedBuffer) + return lines + } + + serialize () { + return this.value + } +} +Object.defineProperty(StringValue.prototype, 'isPrimitive', { value: true }) +Object.defineProperty(StringValue.prototype, 'tag', { value: tag }) diff --git a/node_modules/concordance/lib/primitiveValues/symbol.js b/node_modules/concordance/lib/primitiveValues/symbol.js new file mode 100644 index 000000000..3778b4118 --- /dev/null +++ b/node_modules/concordance/lib/primitiveValues/symbol.js @@ -0,0 +1,114 @@ +'use strict' + +const stringEscape = require('js-string-escape') +const wellKnownSymbols = require('well-known-symbols') + +const constants = require('../constants') +const formatUtils = require('../formatUtils') +const lineBuilder = require('../lineBuilder') + +const DEEP_EQUAL = constants.DEEP_EQUAL +const UNEQUAL = constants.UNEQUAL + +function describe (value) { + let stringCompare = null + + const key = Symbol.keyFor(value) + if (key !== undefined) { + stringCompare = `Symbol.for(${stringEscape(key)})` + } else if (wellKnownSymbols.isWellKnown(value)) { + stringCompare = wellKnownSymbols.getLabel(value) + } + + return new SymbolValue({ + stringCompare, + value + }) +} +exports.describe = describe + +function deserialize (state) { + const stringCompare = state[0] + const string = state[1] || state[0] + + return new DeserializedSymbolValue({ + string, + stringCompare, + value: null + }) +} +exports.deserialize = deserialize + +const tag = Symbol('SymbolValue') +exports.tag = tag + +class SymbolValue { + constructor (props) { + this.stringCompare = props.stringCompare + this.value = props.value + } + + compare (expected) { + if (expected.tag !== tag) return UNEQUAL + + if (this.stringCompare !== null) { + return this.stringCompare === expected.stringCompare + ? DEEP_EQUAL + : UNEQUAL + } + + return this.value === expected.value + ? DEEP_EQUAL + : UNEQUAL + } + + formatString () { + if (this.stringCompare !== null) return this.stringCompare + return stringEscape(this.value.toString()) + } + + formatDeep (theme) { + return lineBuilder.single(formatUtils.wrap(theme.symbol, this.formatString())) + } + + formatAsKey (theme) { + return formatUtils.wrap(theme.property.keyBracket, formatUtils.wrap(theme.symbol, this.formatString())) + } + + serialize () { + const string = this.formatString() + return this.stringCompare === string + ? [this.stringCompare] + : [this.stringCompare, string] + } +} +Object.defineProperty(SymbolValue.prototype, 'isPrimitive', { value: true }) +Object.defineProperty(SymbolValue.prototype, 'tag', { value: tag }) + +class DeserializedSymbolValue extends SymbolValue { + constructor (props) { + super(props) + this.string = props.string + } + + compare (expected) { + if (expected.tag !== tag) return UNEQUAL + + if (this.stringCompare !== null) { + return this.stringCompare === expected.stringCompare + ? DEEP_EQUAL + : UNEQUAL + } + + // Symbols that are not in the global symbol registry, and are not + // well-known, cannot be compared when deserialized. Treat symbols + // as equal if they are formatted the same. + return this.string === expected.formatString() + ? DEEP_EQUAL + : UNEQUAL + } + + formatString () { + return this.string + } +} diff --git a/node_modules/concordance/lib/primitiveValues/undefined.js b/node_modules/concordance/lib/primitiveValues/undefined.js new file mode 100644 index 000000000..507556e61 --- /dev/null +++ b/node_modules/concordance/lib/primitiveValues/undefined.js @@ -0,0 +1,32 @@ +'use strict' + +const constants = require('../constants') +const formatUtils = require('../formatUtils') +const lineBuilder = require('../lineBuilder') + +const DEEP_EQUAL = constants.DEEP_EQUAL +const UNEQUAL = constants.UNEQUAL + +function describe () { + return new UndefinedValue() +} +exports.describe = describe + +exports.deserialize = describe + +const tag = Symbol('UndefinedValue') +exports.tag = tag + +class UndefinedValue { + compare (expected) { + return expected.tag === tag + ? DEEP_EQUAL + : UNEQUAL + } + + formatDeep (theme) { + return lineBuilder.single(formatUtils.wrap(theme.undefined, 'undefined')) + } +} +Object.defineProperty(UndefinedValue.prototype, 'isPrimitive', { value: true }) +Object.defineProperty(UndefinedValue.prototype, 'tag', { value: tag }) diff --git a/node_modules/concordance/lib/recursorUtils.js b/node_modules/concordance/lib/recursorUtils.js new file mode 100644 index 000000000..ff639b5b8 --- /dev/null +++ b/node_modules/concordance/lib/recursorUtils.js @@ -0,0 +1,110 @@ +'use strict' + +const NOOP_RECURSOR = { + size: 0, + next () { return null } +} +exports.NOOP_RECURSOR = NOOP_RECURSOR + +function fork (recursor) { + const buffer = [] + + return { + shared () { + const next = recursor() + if (next !== null) buffer.push(next) + return next + }, + + recursor () { + if (buffer.length > 0) return buffer.shift() + return recursor() + } + } +} +exports.fork = fork + +function map (recursor, mapFn) { + return () => { + const next = recursor() + if (next === null) return null + + return mapFn(next) + } +} +exports.map = map + +function replay (state, create) { + if (!state) { + const recursor = create() + if (recursor === NOOP_RECURSOR) { + state = recursor + } else { + state = Object.assign({ + buffer: [], + done: false + }, recursor) + } + } + + if (state === NOOP_RECURSOR) return {state, recursor: state} + + let done = false + let index = 0 + const next = () => { + if (done) return null + + let retval = state.buffer[index] + if (retval === undefined) { + retval = state.buffer[index] = state.next() + } + + index++ + if (retval === null) { + done = true + } + return retval + } + + return {state, recursor: {next, size: state.size}} +} +exports.replay = replay + +function sequence (first, second) { + let fromFirst = true + return () => { + if (fromFirst) { + const next = first() + if (next !== null) return next + + fromFirst = false + } + + return second() + } +} +exports.sequence = sequence + +function singleValue (value) { + let done = false + return () => { + if (done) return null + + done = true + return value + } +} +exports.singleValue = singleValue + +function unshift (recursor, value) { + return () => { + if (value !== null) { + const next = value + value = null + return next + } + + return recursor() + } +} +exports.unshift = unshift diff --git a/node_modules/concordance/lib/serialize.js b/node_modules/concordance/lib/serialize.js new file mode 100644 index 000000000..07f568415 --- /dev/null +++ b/node_modules/concordance/lib/serialize.js @@ -0,0 +1,339 @@ +'use strict' + +const md5hex = require('md5-hex') + +const encoder = require('./encoder') +const pluginRegistry = require('./pluginRegistry') + +const argumentsValue = require('./complexValues/arguments') +const arrayBufferValue = require('./complexValues/arrayBuffer') +const boxedValue = require('./complexValues/boxed') +const dataViewValue = require('./complexValues/dataView') +const dateValue = require('./complexValues/date') +const errorValue = require('./complexValues/error') +const functionValue = require('./complexValues/function') +const globalValue = require('./complexValues/global') +const mapValue = require('./complexValues/map') +const objectValue = require('./complexValues/object') +const promiseValue = require('./complexValues/promise') +const regexpValue = require('./complexValues/regexp') +const setValue = require('./complexValues/set') +const typedArrayValue = require('./complexValues/typedArray') + +const itemDescriptor = require('./metaDescriptors/item') +const mapEntryDescriptor = require('./metaDescriptors/mapEntry') +const pointerDescriptor = require('./metaDescriptors/pointer') +const propertyDescriptor = require('./metaDescriptors/property') +const statsDescriptors = require('./metaDescriptors/stats') + +const booleanValue = require('./primitiveValues/boolean') +const nullValue = require('./primitiveValues/null') +const numberValue = require('./primitiveValues/number') +const stringValue = require('./primitiveValues/string') +const symbolValue = require('./primitiveValues/symbol') +const undefinedValue = require('./primitiveValues/undefined') + +// Increment if encoding layout, descriptor IDs, or value types change. Previous +// Concordance versions will not be able to decode buffers generated by a newer +// version, so changing this value will require a major version bump of +// Concordance itself. The version is encoded as an unsigned 16 bit integer. +const VERSION = 2 + +// Adding or removing mappings or changing an index requires the version in +// encoder.js to be bumped, which necessitates a major version bump of +// Concordance itself. Indexes are hexadecimal to make reading the binary +// output easier. +const mappings = [ + [0x01, booleanValue.tag, booleanValue.deserialize], + [0x02, nullValue.tag, nullValue.deserialize], + [0x03, numberValue.tag, numberValue.deserialize], + [0x04, stringValue.tag, stringValue.deserialize], + [0x05, symbolValue.tag, symbolValue.deserialize], + [0x06, undefinedValue.tag, undefinedValue.deserialize], + + [0x07, objectValue.tag, objectValue.deserialize], + [0x08, statsDescriptors.iterableTag, statsDescriptors.deserializeIterableStats], + [0x09, statsDescriptors.listTag, statsDescriptors.deserializeListStats], + [0x0A, itemDescriptor.complexTag, itemDescriptor.deserializeComplex], + [0x0B, itemDescriptor.primitiveTag, itemDescriptor.deserializePrimitive], + [0x0C, statsDescriptors.propertyTag, statsDescriptors.deserializePropertyStats], + [0x0D, propertyDescriptor.complexTag, propertyDescriptor.deserializeComplex], + [0x0E, propertyDescriptor.primitiveTag, propertyDescriptor.deserializePrimitive], + [0x0F, pointerDescriptor.tag, pointerDescriptor.deserialize], + + [0x10, mapValue.tag, mapValue.deserialize], + [0x11, mapEntryDescriptor.tag, mapEntryDescriptor.deserialize], + + [0x12, argumentsValue.tag, argumentsValue.deserialize], + [0x13, arrayBufferValue.tag, arrayBufferValue.deserialize], + [0x14, boxedValue.tag, boxedValue.deserialize], + [0x15, dataViewValue.tag, dataViewValue.deserialize], + [0x16, dateValue.tag, dateValue.deserialize], + [0x17, errorValue.tag, errorValue.deserialize], + [0x18, functionValue.tag, functionValue.deserialize], + [0x19, globalValue.tag, globalValue.deserialize], + [0x1A, promiseValue.tag, promiseValue.deserialize], + [0x1B, regexpValue.tag, regexpValue.deserialize], + [0x1C, setValue.tag, setValue.deserialize], + [0x1D, typedArrayValue.tag, typedArrayValue.deserialize], + [0x1E, typedArrayValue.bytesTag, typedArrayValue.deserializeBytes] +] +const tag2id = new Map(mappings.map(mapping => [mapping[1], mapping[0]])) +const id2deserialize = new Map(mappings.map(mapping => [mapping[0], mapping[2]])) + +class DescriptorSerializationError extends Error { + constructor (descriptor) { + super('Could not serialize descriptor') + this.name = 'DescriptorSerializationError' + this.descriptor = descriptor + } +} + +class MissingPluginError extends Error { + constructor (pluginName) { + super(`Could not deserialize buffer: missing plugin ${JSON.stringify(pluginName)}`) + this.name = 'MissingPluginError' + this.pluginName = pluginName + } +} + +class PointerLookupError extends Error { + constructor (index) { + super(`Could not deserialize buffer: pointer ${index} could not be resolved`) + this.name = 'PointerLookupError' + this.index = index + } +} + +class UnsupportedPluginError extends Error { + constructor (pluginName, serializerVersion) { + super(`Could not deserialize buffer: plugin ${JSON.stringify(pluginName)} expects a different serialization`) + this.name = 'UnsupportedPluginError' + this.pluginName = pluginName + this.serializerVersion = serializerVersion + } +} + +class UnsupportedVersion extends Error { + constructor (serializerVersion) { + super('Could not deserialize buffer: a different serialization was expected') + this.name = 'UnsupportedVersion' + this.serializerVersion = serializerVersion + } +} + +function shallowSerializeDescriptor (descriptor, resolvePluginRef) { + if (!descriptor.serialize) return undefined + + return serializeState(descriptor.serialize(), resolvePluginRef) +} + +function serializeState (state, resolvePluginRef) { + if (Array.isArray(state)) return state.map(serializeState) + + if (state && state.tag) { + let id, pluginIndex + if (tag2id.has(state.tag)) { + id = tag2id.get(state.tag) + pluginIndex = 0 + } else { + const ref = resolvePluginRef(state.tag) + if (ref) { + id = ref.id + pluginIndex = ref.pluginIndex + } + } + + if (id !== undefined) { + const serialized = [pluginIndex, id, shallowSerializeDescriptor(state, resolvePluginRef)] + serialized[encoder.descriptorSymbol] = true + return serialized + } + } + + return state +} + +function serialize (descriptor) { + const usedPlugins = new Map() + const resolvePluginRef = tag => { + const ref = pluginRegistry.resolveDescriptorRef(tag) + if (!ref) return null + + if (!usedPlugins.has(ref.name)) { + // Start at 1, since 0 is reserved for Concordance's descriptors. + const index = usedPlugins.size + 1 + usedPlugins.set(ref.name, Object.assign({index}, ref.serialization)) + } + + return { + id: ref.id, + pluginIndex: usedPlugins.get(ref.name).index + } + } + + const seen = new Set() + + const stack = [] + let topIndex = -1 + + let rootRecord + do { + if (descriptor.isComplex === true) { + if (seen.has(descriptor.pointer)) { + descriptor = pointerDescriptor.describe(descriptor.pointer) + } else { + seen.add(descriptor.pointer) + } + } + + let id + let pluginIndex = 0 + if (tag2id.has(descriptor.tag)) { + id = tag2id.get(descriptor.tag) + } else { + const ref = resolvePluginRef(descriptor.tag) + if (!ref) throw new DescriptorSerializationError(descriptor) + + id = ref.id + pluginIndex = ref.pluginIndex + } + + const record = { + id, + pluginIndex, + children: [], + state: shallowSerializeDescriptor(descriptor, resolvePluginRef) + } + if (!rootRecord) { + rootRecord = record + } else { + stack[topIndex].children.push(record) + } + + if (descriptor.createRecursor) { + stack.push({ recursor: descriptor.createRecursor(), children: record.children }) + topIndex++ + } + + while (topIndex >= 0) { + descriptor = stack[topIndex].recursor() + if (descriptor === null) { + stack.pop() + topIndex-- + } else { + break + } + } + } while (topIndex >= 0) + + return encoder.encode(VERSION, rootRecord, usedPlugins) +} +exports.serialize = serialize + +function deserializeState (state, getDescriptorDeserializer) { + if (state && state[encoder.descriptorSymbol] === true) { + return shallowDeserializeDescriptor(state, getDescriptorDeserializer) + } + + return Array.isArray(state) + ? state.map(item => deserializeState(item, getDescriptorDeserializer)) + : state +} + +function shallowDeserializeDescriptor (entry, getDescriptorDeserializer) { + const deserializeDescriptor = getDescriptorDeserializer(entry[0], entry[1]) + return deserializeDescriptor(entry[2]) +} + +function deserializeRecord (record, getDescriptorDeserializer, buffer) { + const deserializeDescriptor = getDescriptorDeserializer(record.pluginIndex, record.id) + const state = deserializeState(record.state, getDescriptorDeserializer) + + if (record.pointerAddresses.length === 0) { + return deserializeDescriptor(state) + } + + const endIndex = record.pointerAddresses.length + let index = 0 + const recursor = () => { + if (index === endIndex) return null + + const recursorRecord = encoder.decodeRecord(buffer, record.pointerAddresses[index++]) + return deserializeRecord(recursorRecord, getDescriptorDeserializer, buffer) + } + + return deserializeDescriptor(state, recursor) +} + +function buildPluginMap (buffer, options) { + const cache = options && options.deserializedPluginsCache + const cacheKey = md5hex(buffer) + if (cache && cache.has(cacheKey)) return cache.get(cacheKey) + + const decodedPlugins = encoder.decodePlugins(buffer) + if (decodedPlugins.size === 0) { + const pluginMap = new Map() + if (cache) cache.set(cacheKey, pluginMap) + return pluginMap + } + + const deserializerLookup = new Map() + if (Array.isArray(options && options.plugins)) { + for (const deserializer of pluginRegistry.getDeserializers(options.plugins)) { + deserializerLookup.set(deserializer.name, deserializer) + } + } + + const pluginMap = new Map() + for (const index of decodedPlugins.keys()) { + const used = decodedPlugins.get(index) + const pluginName = used.name + const serializerVersion = used.serializerVersion + + // TODO: Allow plugin author to encode a helpful message in its serialization + if (!deserializerLookup.has(pluginName)) { + throw new MissingPluginError(pluginName) + } + if (serializerVersion !== deserializerLookup.get(pluginName).serializerVersion) { + throw new UnsupportedPluginError(pluginName, serializerVersion) + } + + pluginMap.set(index, deserializerLookup.get(pluginName).id2deserialize) + } + + if (cache) cache.set(cacheKey, pluginMap) + return pluginMap +} + +function deserialize (buffer, options) { + const version = encoder.extractVersion(buffer) + if (version !== VERSION) throw new UnsupportedVersion(version) + + const decoded = encoder.decode(buffer) + const pluginMap = buildPluginMap(decoded.pluginBuffer, options) + + const descriptorsByPointerIndex = new Map() + const mapPointerDescriptor = descriptor => { + if (descriptor.isPointer === true) { + if (!descriptorsByPointerIndex.has(descriptor.index)) throw new PointerLookupError(descriptor.index) + + return descriptorsByPointerIndex.get(descriptor.index) + } else if (descriptor.isComplex === true) { + descriptorsByPointerIndex.set(descriptor.pointer, descriptor) + } + return descriptor + } + + const getDescriptorDeserializer = (pluginIndex, id) => { + return (state, recursor) => { + const deserializeDescriptor = pluginIndex === 0 + ? id2deserialize.get(id) + : pluginMap.get(pluginIndex).get(id) + + return mapPointerDescriptor(deserializeDescriptor(state, recursor)) + } + } + return deserializeRecord(decoded.rootRecord, getDescriptorDeserializer, buffer) +} +exports.deserialize = deserialize diff --git a/node_modules/concordance/lib/shouldCompareDeep.js b/node_modules/concordance/lib/shouldCompareDeep.js new file mode 100644 index 000000000..a59135507 --- /dev/null +++ b/node_modules/concordance/lib/shouldCompareDeep.js @@ -0,0 +1,17 @@ +'use strict' + +const argumentsObject = require('./complexValues/arguments').tag +const constants = require('./constants') + +const AMBIGUOUS = constants.AMBIGUOUS +const SHALLOW_EQUAL = constants.SHALLOW_EQUAL + +function shouldCompareDeep (result, lhs, rhs) { + if (result === SHALLOW_EQUAL) return true + if (result !== AMBIGUOUS) return false + + // Properties are only ambiguous if they have symbol keys. These properties + // must be compared in an order-insensitive manner. + return lhs.tag === argumentsObject || lhs.isProperty === true +} +module.exports = shouldCompareDeep diff --git a/node_modules/concordance/lib/symbolProperties.js b/node_modules/concordance/lib/symbolProperties.js new file mode 100644 index 000000000..623a428fc --- /dev/null +++ b/node_modules/concordance/lib/symbolProperties.js @@ -0,0 +1,106 @@ +'use strict' + +const constants = require('./constants') +const recursorUtils = require('./recursorUtils') + +const DEEP_EQUAL = constants.DEEP_EQUAL +const SHALLOW_EQUAL = constants.SHALLOW_EQUAL +const UNEQUAL = constants.UNEQUAL + +class Comparable { + constructor (properties) { + this.properties = properties + this.ordered = properties.slice() + } + + createRecursor () { + const length = this.ordered.length + let index = 0 + return () => { + if (index === length) return null + + return this.ordered[index++] + } + } + + compare (expected) { + if (this.properties.length !== expected.properties.length) return UNEQUAL + + // Compare property keys, reordering the expected properties in the process + // so values can be compared if all keys are equal. + const ordered = [] + const processed = new Set() + for (const property of this.properties) { + let extraneous = true + for (const other of expected.properties) { + if (processed.has(other.key)) continue + + if (property.key.compare(other.key) === DEEP_EQUAL) { + extraneous = false + processed.add(other.key) + ordered.push(other) + break + } + } + + if (extraneous) return UNEQUAL + } + expected.ordered = ordered + + return SHALLOW_EQUAL + } + + prepareDiff (expected) { + // Reorder the expected properties before recursion starts. + const missingProperties = [] + const ordered = [] + const processed = new Set() + for (const other of expected.properties) { + let missing = true + for (const property of this.properties) { + if (processed.has(property.key)) continue + + if (property.key.compare(other.key) === DEEP_EQUAL) { + missing = false + processed.add(property.key) + ordered.push(other) + break + } + } + + if (missing) { + missingProperties.push(other) + } + } + expected.ordered = ordered.concat(missingProperties) + + return {mustRecurse: true} + } +} +Object.defineProperty(Comparable.prototype, 'isSymbolPropertiesComparable', { value: true }) +exports.Comparable = Comparable + +class Collector { + constructor (firstProperty, recursor) { + this.properties = [firstProperty] + this.recursor = recursor + this.remainder = null + } + + collectAll () { + do { + const next = this.recursor() + if (next && next.isProperty === true) { // All properties will have symbol keys + this.properties.push(next) + } else { + return next + } + } while (true) + } + + createRecursor () { + return recursorUtils.singleValue(new Comparable(this.properties)) + } +} +Object.defineProperty(Collector.prototype, 'isSymbolPropertiesCollector', { value: true }) +exports.Collector = Collector diff --git a/node_modules/concordance/lib/themeUtils.js b/node_modules/concordance/lib/themeUtils.js new file mode 100644 index 000000000..a0a64b586 --- /dev/null +++ b/node_modules/concordance/lib/themeUtils.js @@ -0,0 +1,195 @@ +'use strict' + +const cloneDeep = require('lodash.clonedeep') +const merge = require('lodash.merge') + +const pluginRegistry = require('./pluginRegistry') + +function freezeTheme (theme) { + const queue = [theme] + while (queue.length > 0) { + const object = queue.shift() + Object.freeze(object) + + for (const key of Object.keys(object)) { + const value = object[key] + if (value !== null && typeof value === 'object') { + queue.push(value) + } + } + } + + return theme +} + +const defaultTheme = freezeTheme({ + boolean: { open: '', close: '' }, + circular: '[Circular]', + date: { + invalid: 'invalid', + value: { open: '', close: '' } + }, + diffGutters: { + actual: '- ', + expected: '+ ', + padding: ' ' + }, + error: { + ctor: { open: '(', close: ')' }, + name: { open: '', close: '' } + }, + function: { + name: { open: '', close: '' }, + stringTag: { open: '', close: '' } + }, + global: { open: '', close: '' }, + item: { + after: ',', + customFormat: null, + increaseValueIndent: false + }, + list: { openBracket: '[', closeBracket: ']' }, + mapEntry: { + after: ',', + separator: ' => ' + }, + maxDepth: '…', + null: { open: '', close: '' }, + number: { open: '', close: '' }, + object: { + openBracket: '{', + closeBracket: '}', + ctor: { open: '', close: '' }, + stringTag: { open: '@', close: '' }, + secondaryStringTag: { open: '@', close: '' } + }, + property: { + after: ',', + customFormat: null, + keyBracket: { open: '[', close: ']' }, + separator: ': ', + increaseValueIndent: false + }, + regexp: { + source: { open: '/', close: '/' }, + flags: { open: '', close: '' }, + separator: '---' + }, + stats: { separator: '---' }, + string: { + open: '', + close: '', + line: { open: "'", close: "'", escapeQuote: "'" }, + multiline: { start: '`', end: '`', escapeQuote: '``' }, + controlPicture: { open: '', close: '' }, + diff: { + insert: { open: '', close: '' }, + delete: { open: '', close: '' }, + equal: { open: '', close: '' }, + insertLine: { open: '', close: '' }, + deleteLine: { open: '', close: '' } + } + }, + symbol: { open: '', close: '' }, + typedArray: { + bytes: { open: '', close: '' } + }, + undefined: { open: '', close: '' } +}) + +const pluginRefs = new Map() +pluginRefs.count = 0 +const normalizedPluginThemes = new Map() +function normalizePlugins (plugins) { + if (!Array.isArray(plugins) || plugins.length === 0) return null + + const refs = [] + const themes = [] + for (const fromPlugin of pluginRegistry.getThemes(plugins)) { + if (!pluginRefs.has(fromPlugin.name)) { + pluginRefs.set(fromPlugin.name, pluginRefs.count++) + } + + refs.push(pluginRefs.get(fromPlugin.name)) + themes.push(fromPlugin.theme) + } + + const ref = refs.join('.') + if (normalizedPluginThemes.has(ref)) { + return { + ref, + theme: normalizedPluginThemes.get(ref) + } + } + + const theme = freezeTheme(themes.reduce((acc, pluginTheme) => { + return merge(acc, pluginTheme) + }, cloneDeep(defaultTheme))) + normalizedPluginThemes.set(ref, theme) + return {ref, theme} +} + +const normalizedCache = new WeakMap() +function normalize (options) { + options = Object.assign({plugins: [], theme: null}, options) + + const normalizedPlugins = normalizePlugins(options.plugins) + if (!options.theme) { + return normalizedPlugins ? normalizedPlugins.theme : defaultTheme + } + + const entry = normalizedCache.get(options.theme) || {theme: null, withPlugins: new Map()} + if (!normalizedCache.has(options.theme)) normalizedCache.set(options.theme, entry) + + if (normalizedPlugins) { + if (entry.withPlugins.has(normalizedPlugins.ref)) { + return entry.withPlugins.get(normalizedPlugins.ref) + } + + const theme = freezeTheme(merge(cloneDeep(normalizedPlugins.theme), options.theme)) + entry.withPlugins.set(normalizedPlugins.ref, theme) + return theme + } + + if (!entry.theme) { + entry.theme = freezeTheme(merge(cloneDeep(defaultTheme), options.theme)) + } + return entry.theme +} +exports.normalize = normalize + +const modifiers = new WeakMap() +function addModifier (descriptor, modifier) { + if (modifiers.has(descriptor)) { + modifiers.get(descriptor).add(modifier) + } else { + modifiers.set(descriptor, new Set([modifier])) + } +} +exports.addModifier = addModifier + +const modifierCache = new WeakMap() +const originalCache = new WeakMap() +function applyModifiers (descriptor, theme) { + if (!modifiers.has(descriptor)) return theme + + return Array.from(modifiers.get(descriptor)).reduce((prev, modifier) => { + const cache = modifierCache.get(modifier) || new WeakMap() + if (!modifierCache.has(modifier)) modifierCache.set(modifier, cache) + + if (cache.has(prev)) return cache.get(prev) + + const modifiedTheme = cloneDeep(prev) + modifier(modifiedTheme) + freezeTheme(modifiedTheme) + cache.set(prev, modifiedTheme) + originalCache.set(modifiedTheme, theme) + return modifiedTheme + }, theme) +} +exports.applyModifiers = applyModifiers + +function applyModifiersToOriginal (descriptor, theme) { + return applyModifiers(descriptor, originalCache.get(theme) || theme) +} +exports.applyModifiersToOriginal = applyModifiersToOriginal |