diff --git a/README.md b/README.md index 866cb11..cb66b3a 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,9 @@ means Concordance's behavior is consistent, no matter how you use it. Symbol properties are compared by identity. * `Promise` values are compared by identity only. * `Symbol` values are compared by identity only. +* Recursion stops whenever a circular reference is encountered. If the same + cycle is present in the actual and expected values they're considered equal, + but they're unequal otherwise. ### Formatting details diff --git a/lib/Circular.js b/lib/Circular.js index ae749e6..c5c0202 100644 --- a/lib/Circular.js +++ b/lib/Circular.js @@ -1,11 +1,35 @@ 'use strict' -class Circular extends Set { +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) { - super.add(descriptor) + 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/lib/Registry.js b/lib/Registry.js index d7b56f1..f4a4b68 100644 --- a/lib/Registry.js +++ b/lib/Registry.js @@ -1,7 +1,5 @@ 'use strict' -const describePointer = require('./metaDescriptors/pointer').describe - class Registry { constructor () { this.counter = 0 @@ -13,12 +11,13 @@ class Registry { } get (value) { - return this.map.get(value) + return this.map.get(value).descriptor } - add (value) { - const pointer = this.counter++ - this.map.set(value, describePointer(pointer)) + alloc (value) { + const index = ++this.counter + const pointer = {descriptor: null, index} + this.map.set(value, pointer) return pointer } } diff --git a/lib/compare.js b/lib/compare.js index 614949f..7e24228 100644 --- a/lib/compare.js +++ b/lib/compare.js @@ -23,9 +23,8 @@ function shortcircuitPrimitive (value) { } function compareDescriptors (lhs, rhs) { - const circular = new Circular() - const lhsLookup = new Map() - const rhsLookup = new Map() + const lhsCircular = new Circular() + const rhsCircular = new Circular() const lhsStack = [] const rhsStack = [] @@ -33,30 +32,13 @@ function compareDescriptors (lhs, rhs) { do { let result - - if (lhs.isComplex === true) { - lhsLookup.set(lhs.pointer, lhs) - } - if (rhs.isComplex === true) { - rhsLookup.set(rhs.pointer, rhs) - } - - if (lhs.isPointer === true) { - if (rhs.isPointer === true && lhs.compare(rhs) === DEEP_EQUAL) { - result = DEEP_EQUAL - } else { - lhs = lhsLookup.get(lhs.pointer) - } - } - if (rhs.isPointer === true) { - rhs = rhsLookup.get(rhs.pointer) - } - - if (circular.has(lhs) && circular.has(rhs)) { + if (lhsCircular.has(lhs)) { + result = lhsCircular.get(lhs) === rhsCircular.get(rhs) + ? DEEP_EQUAL + : UNEQUAL + } else if (rhsCircular.has(rhs)) { result = UNEQUAL - } - - if (!result) { + } else { result = lhs.compare(rhs) } @@ -76,11 +58,11 @@ function compareDescriptors (lhs, rhs) { rhsStack[topIndex].recursor = recursorUtils.unshift(rhsStack[topIndex].recursor, rhs.collectAll()) } - circular.add(lhs) - circular.add(rhs) + lhsCircular.add(lhs) + rhsCircular.add(rhs) - lhsStack.push({ origin: lhs, recursor: lhs.createRecursor() }) - rhsStack.push({ origin: rhs, recursor: rhs.createRecursor() }) + lhsStack.push({ subject: lhs, recursor: lhs.createRecursor() }) + rhsStack.push({ subject: rhs, recursor: rhs.createRecursor() }) topIndex++ } @@ -94,8 +76,8 @@ function compareDescriptors (lhs, rhs) { if (lhs === null && rhs === null) { const lhsRecord = lhsStack.pop() const rhsRecord = rhsStack.pop() - circular.delete(lhsRecord.origin) - circular.delete(rhsRecord.origin) + lhsCircular.delete(lhsRecord.subject) + rhsCircular.delete(rhsRecord.subject) topIndex-- } else { return false diff --git a/lib/describe.js b/lib/describe.js index f1f6f7e..a8aa945 100644 --- a/lib/describe.js +++ b/lib/describe.js @@ -99,7 +99,7 @@ function describeComplex (value, registry, tryPlugins, describeAny, describeItem const stringTag = getStringTag(value) const ctor = getCtor(stringTag, value) - const pointer = registry.add(value) + const pointer = registry.alloc(value) let unboxed let describeValue = tryPlugins(value, stringTag, ctor) @@ -115,17 +115,20 @@ function describeComplex (value, registry, tryPlugins, describeAny, describeItem } } } - return describeValue({ + + const descriptor = describeValue({ ctor, describeAny, describeItem, describeMapEntry, describeProperty, - pointer, + pointer: pointer.index, stringTag, unboxed, value }) + pointer.descriptor = descriptor + return descriptor } function describe (value, options) { diff --git a/lib/diff.js b/lib/diff.js index ce85517..5fe4d22 100644 --- a/lib/diff.js +++ b/lib/diff.js @@ -59,9 +59,8 @@ function diffDescriptors (lhs, rhs, options) { const theme = themeUtils.normalize(options) const invert = options ? options.invert === true : false - const circular = new Circular() - const lhsLookup = new Map() - const rhsLookup = new Map() + const lhsCircular = new Circular() + const rhsCircular = new Circular() const maxDepth = (options && options.maxDepth) || 0 let indent = new Indenter(0, ' ') @@ -74,9 +73,9 @@ function diffDescriptors (lhs, rhs, options) { const diffStack = [] let diffIndex = -1 - const isCircular = descriptor => circular.has(descriptor) + const isCircular = descriptor => lhsCircular.has(descriptor) || rhsCircular.has(descriptor) - const format = (builder, subject, lookup) => { + const format = (builder, subject, circular) => { if (diffIndex >= 0 && !diffStack[diffIndex].shouldFormat(subject)) return if (circular.has(subject)) { @@ -88,17 +87,8 @@ function diffDescriptors (lhs, rhs, options) { let formatIndex = -1 do { - if (subject.isComplex === true) { - lookup.set(subject.pointer, subject) - } - - const origin = subject - if (subject.isPointer === true) { - subject = lookup.get(subject.pointer) - } - if (circular.has(subject)) { - formatStack[formatIndex].formatter.append(builder.single(theme.circular), origin) + formatStack[formatIndex].formatter.append(builder.single(theme.circular), subject) } else { let didFormat = false if (typeof subject.formatDeep === 'function') { @@ -111,10 +101,10 @@ function diffDescriptors (lhs, rhs, options) { if (diffIndex === -1) { buffer.append(formatted) } else { - diffStack[diffIndex].formatter.append(formatted, origin) + diffStack[diffIndex].formatter.append(formatted, subject) } } else { - formatStack[formatIndex].formatter.append(formatted, origin) + formatStack[formatIndex].formatter.append(formatted, subject) } } } @@ -131,14 +121,13 @@ function diffDescriptors (lhs, rhs, options) { if (formatIndex === -1) { formatted = builder.setDefaultGutter(formatted) - diffStack[diffIndex].formatter.append(formatted, origin) + diffStack[diffIndex].formatter.append(formatted, subject) } else { - formatStack[formatIndex].formatter.append(formatted, origin) + formatStack[formatIndex].formatter.append(formatted, subject) } } else { formatStack.push({ formatter, - origin, recursor, decreaseIndent: formatter.increaseIndent, shouldFormat: formatter.shouldFormat || alwaysFormat, @@ -172,37 +161,24 @@ function diffDescriptors (lhs, rhs, options) { if (diffIndex === -1) { buffer.append(formatted) } else { - diffStack[diffIndex].formatter.append(formatted, record.origin) + diffStack[diffIndex].formatter.append(formatted, record.subject) } } else { - formatStack[formatIndex].formatter.append(formatted, record.origin) + formatStack[formatIndex].formatter.append(formatted, record.subject) } } } while (formatIndex >= 0) } do { - if (lhs.isComplex === true) { - lhsLookup.set(lhs.pointer, lhs) - } - if (rhs.isComplex === true) { - rhsLookup.set(rhs.pointer, rhs) - } - - let equalPointers = false - const lhsOrigin = lhs - if (lhs.isPointer === true) { - if (rhs.isPointer === true && lhs.compare(rhs) === DEEP_EQUAL) { - equalPointers = true - } else { - lhs = lhsLookup.get(lhs.pointer) - } - } - if (rhs.isPointer === true) { - rhs = rhsLookup.get(rhs.pointer) - } - 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) { @@ -215,7 +191,7 @@ function diffDescriptors (lhs, rhs, options) { let didFormat = false let mustRecurse = false - if (!equalPointers && !firstPassSymbolProperty && typeof lhs.prepareDiff === 'function') { + 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 @@ -243,24 +219,24 @@ function diffDescriptors (lhs, rhs, options) { mustRecurse = true } else { if (instructions.actualIsExtraneous === true) { - format(lineBuilder.actual, lhs, lhsLookup) + format(lineBuilder.actual, lhs, lhsCircular) didFormat = true } else if (instructions.multipleAreExtraneous === true) { for (const extraneous of instructions.descriptors) { - format(lineBuilder.actual, extraneous, lhsLookup) + format(lineBuilder.actual, extraneous, lhsCircular) } didFormat = true } else if (instructions.expectedIsMissing === true) { - format(lineBuilder.expected, rhs, rhsLookup) + format(lineBuilder.expected, rhs, rhsCircular) didFormat = true } else if (instructions.multipleAreMissing === true) { for (const missing of instructions.descriptors) { - format(lineBuilder.expected, missing, rhsLookup) + format(lineBuilder.expected, missing, rhsCircular) } didFormat = true } else if (instructions.isUnequal === true) { - format(lineBuilder.actual, lhs, lhsLookup) - format(lineBuilder.expected, rhs, rhsLookup) + format(lineBuilder.actual, lhs, lhsCircular) + format(lineBuilder.expected, rhs, rhsCircular) didFormat = true } else if (!instructions.compareResult) { // TODO: Throw a useful, custom error @@ -272,9 +248,7 @@ function diffDescriptors (lhs, rhs, options) { if (!didFormat) { if (compareResult === NOOP) { - compareResult = equalPointers - ? DEEP_EQUAL - : lhs.compare(rhs) + compareResult = lhs.compare(rhs) } if (!mustRecurse) { @@ -282,7 +256,7 @@ function diffDescriptors (lhs, rhs, options) { } if (compareResult === DEEP_EQUAL) { - format(lineBuilder, lhs, lhsLookup) + format(lineBuilder, lhs, lhsCircular) } else if (mustRecurse) { if (compareResult === AMBIGUOUS && lhs.isProperty === true) { // Replace both sides by a pseudo-descriptor which collects symbol @@ -300,7 +274,7 @@ function diffDescriptors (lhs, rhs, options) { const formatter = lhs.diffShallow(rhs, themeUtils.applyModifiers(lhs, theme), indent) diffStack.push({ formatter, - origin: lhsOrigin, + origin: lhs, decreaseIndent: formatter.increaseIndent, exceedsMaxDepth: formatter.increaseIndent && maxDepth > 0 && indent.level >= maxDepth, shouldFormat: formatter.shouldFormat || alwaysFormat @@ -312,18 +286,18 @@ function diffDescriptors (lhs, rhs, options) { const formatter = lhs.formatShallow(themeUtils.applyModifiers(lhs, theme), indent) diffStack.push({ formatter, - origin: lhsOrigin, decreaseIndent: formatter.increaseIndent, exceedsMaxDepth: formatter.increaseIndent && maxDepth > 0 && indent.level >= maxDepth, - shouldFormat: formatter.shouldFormat || alwaysFormat + shouldFormat: formatter.shouldFormat || alwaysFormat, + subject: lhs }) diffIndex++ if (formatter.increaseIndent) indent = indent.increase() } - circular.add(lhs) - circular.add(rhs) + lhsCircular.add(lhs) + rhsCircular.add(rhs) lhsStack.push({ diffIndex, subject: lhs, recursor: lhs.createRecursor() }) rhsStack.push({ diffIndex, subject: rhs, recursor: rhs.createRecursor() }) @@ -334,13 +308,13 @@ function diffDescriptors (lhs, rhs, options) { : null if (diffed === null) { - format(lineBuilder.actual, lhs, lhsLookup) - format(lineBuilder.expected, rhs, rhsLookup) + format(lineBuilder.actual, lhs, lhsCircular) + format(lineBuilder.expected, rhs, rhsCircular) } else { if (diffIndex === -1) { buffer.append(diffed) } else { - diffStack[diffIndex].formatter.append(diffed, lhsOrigin) + diffStack[diffIndex].formatter.append(diffed, lhs) } } } @@ -357,8 +331,8 @@ function diffDescriptors (lhs, rhs, options) { if (lhs === null && rhs === null) { const lhsRecord = lhsStack.pop() const rhsRecord = rhsStack.pop() - circular.delete(lhsRecord.subject) - circular.delete(rhsRecord.subject) + lhsCircular.delete(lhsRecord.subject) + rhsCircular.delete(rhsRecord.subject) topIndex-- if (lhsRecord.diffIndex === diffIndex) { @@ -382,25 +356,25 @@ function diffDescriptors (lhs, rhs, options) { if (diffIndex === -1) { buffer.append(formatted) } else { - diffStack[diffIndex].formatter.append(formatted, record.origin) + diffStack[diffIndex].formatter.append(formatted, record.subject) } } } else { - let builder, lookup, stack, subject + let builder, circular, stack, subject if (lhs === null) { builder = lineBuilder.expected - lookup = rhsLookup + circular = rhsCircular stack = rhsStack subject = rhs } else { builder = lineBuilder.actual - lookup = lhsLookup + circular = lhsCircular stack = lhsStack subject = lhs } do { - format(builder, subject, lookup) + format(builder, subject, circular) subject = stack[topIndex].recursor() } while (subject !== null) } diff --git a/lib/format.js b/lib/format.js index 17eb728..7629579 100644 --- a/lib/format.js +++ b/lib/format.js @@ -17,7 +17,6 @@ function formatDescriptor (subject, options) { } const circular = new Circular() - const lookup = new Map() const maxDepth = (options && options.maxDepth) || 0 let indent = fixedIndent @@ -27,17 +26,8 @@ function formatDescriptor (subject, options) { let topIndex = -1 do { - if (subject.isComplex === true) { - lookup.set(subject.pointer, subject) - } - - const origin = subject - if (subject.isPointer === true) { - subject = lookup.get(subject.pointer) - } - if (circular.has(subject)) { - stack[topIndex].formatter.append(lineBuilder.single(theme.circular), origin) + stack[topIndex].formatter.append(lineBuilder.single(theme.circular), subject) } else { let didFormat = false if (typeof subject.formatDeep === 'function') { @@ -47,7 +37,7 @@ function formatDescriptor (subject, options) { if (topIndex === -1) { buffer.append(formatted) } else { - stack[topIndex].formatter.append(formatted, origin) + stack[topIndex].formatter.append(formatted, subject) } } } @@ -61,11 +51,10 @@ function formatDescriptor (subject, options) { const formatted = !isEmpty && typeof formatter.maxDepth === 'function' ? formatter.maxDepth() : formatter.finalize() - stack[topIndex].formatter.append(formatted, origin) + stack[topIndex].formatter.append(formatted, subject) } else { stack.push({ formatter, - origin, recursor, decreaseIndent: formatter.increaseIndent, shouldFormat: formatter.shouldFormat || alwaysFormat, @@ -97,7 +86,7 @@ function formatDescriptor (subject, options) { if (topIndex === -1) { buffer.append(formatted) } else { - stack[topIndex].formatter.append(formatted, record.origin) + stack[topIndex].formatter.append(formatted, record.subject) } } } while (topIndex >= 0) diff --git a/lib/metaDescriptors/item.js b/lib/metaDescriptors/item.js index 8592b4e..f8b8b0c 100644 --- a/lib/metaDescriptors/item.js +++ b/lib/metaDescriptors/item.js @@ -5,7 +5,6 @@ const formatUtils = require('../formatUtils') const recursorUtils = require('../recursorUtils') const DEEP_EQUAL = constants.DEEP_EQUAL -const SHALLOW_EQUAL = constants.SHALLOW_EQUAL const UNEQUAL = constants.UNEQUAL function describeComplex (index, value) { @@ -48,19 +47,8 @@ class ComplexItem { } compare (expected) { - if (expected.tag !== complexTag || this.index !== expected.index) return UNEQUAL - - const result = this.value.compare(expected.value) - if (result !== UNEQUAL) return result - - if (this.value.isPointer === true) { - return expected.value.isPointer === true - ? UNEQUAL - : SHALLOW_EQUAL - } - - return expected.value.isPointer === true - ? SHALLOW_EQUAL + return expected.tag === complexTag && this.index === expected.index + ? this.value.compare(expected.value) : UNEQUAL } diff --git a/lib/metaDescriptors/pointer.js b/lib/metaDescriptors/pointer.js index 63c8b45..f569d28 100644 --- a/lib/metaDescriptors/pointer.js +++ b/lib/metaDescriptors/pointer.js @@ -1,12 +1,9 @@ 'use strict' -const constants = require('../constants') +const UNEQUAL = require('../constants').UNEQUAL -const DEEP_EQUAL = constants.DEEP_EQUAL -const UNEQUAL = constants.UNEQUAL - -function describe (pointer) { - return new Pointer(pointer) +function describe (index) { + return new Pointer(index) } exports.describe = describe @@ -16,18 +13,18 @@ const tag = Symbol('Pointer') exports.tag = tag class Pointer { - constructor (pointer) { - this.pointer = pointer + constructor (index) { + this.index = index } + // Pointers cannot be compared, and are not expected to be part of the + // comparisons. compare (expected) { - return this.tag === expected.tag && this.pointer === expected.pointer - ? DEEP_EQUAL - : UNEQUAL + return UNEQUAL } serialize () { - return this.pointer + return this.index } } Object.defineProperty(Pointer.prototype, 'isPointer', { value: true }) diff --git a/lib/metaDescriptors/property.js b/lib/metaDescriptors/property.js index 6ad61dd..f9641fc 100644 --- a/lib/metaDescriptors/property.js +++ b/lib/metaDescriptors/property.js @@ -7,7 +7,6 @@ const symbolPrimitive = require('../primitiveValues/symbol').tag const AMBIGUOUS = constants.AMBIGUOUS const DEEP_EQUAL = constants.DEEP_EQUAL -const SHALLOW_EQUAL = constants.SHALLOW_EQUAL const UNEQUAL = constants.UNEQUAL function describeComplex (key, value) { @@ -103,19 +102,8 @@ class ComplexProperty extends Property { const keyResult = this.compareKeys(expected) if (keyResult !== DEEP_EQUAL) return keyResult - if (this.tag !== expected.tag) return UNEQUAL - - const result = this.value.compare(expected.value) - if (result !== UNEQUAL) return result - - if (this.value.isPointer === true) { - return expected.value.isPointer === true - ? UNEQUAL - : SHALLOW_EQUAL - } - - return expected.value.isPointer === true - ? SHALLOW_EQUAL + return this.tag === expected.tag + ? this.value.compare(expected.value) : UNEQUAL } diff --git a/lib/serialize.js b/lib/serialize.js index 321308b..07f5684 100644 --- a/lib/serialize.js +++ b/lib/serialize.js @@ -97,6 +97,14 @@ class MissingPluginError extends Error { } } +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`) @@ -164,11 +172,21 @@ function serialize (descriptor) { } } + 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)) { @@ -294,10 +312,27 @@ function deserialize (buffer, options) { 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) => { - if (pluginIndex === 0) return id2deserialize.get(id) + return (state, recursor) => { + const deserializeDescriptor = pluginIndex === 0 + ? id2deserialize.get(id) + : pluginMap.get(pluginIndex).get(id) - return pluginMap.get(pluginIndex).get(id) + return mapPointerDescriptor(deserializeDescriptor(state, recursor)) + } } return deserializeRecord(decoded.rootRecord, getDescriptorDeserializer, buffer) } diff --git a/test/lodash-isequal-comparison.js b/test/lodash-isequal-comparison.js index ddc39f3..862430a 100644 --- a/test/lodash-isequal-comparison.js +++ b/test/lodash-isequal-comparison.js @@ -262,7 +262,9 @@ test('have transitive equivalence for circular references of arrays', t => { t.true(isEqual(array1, array2)) t.true(isEqual(array2, array3)) - t.true(isEqual(array1, array3)) + // Concordance detects a different circular reference in array1 before it does + // in array3, making them unequal. + t.false(isEqual(array1, array3)) }) test('compare objects with circular references', t => { @@ -300,7 +302,9 @@ test('have transitive equivalence for circular references of objects', t => { t.true(isEqual(object1, object2)) t.true(isEqual(object2, object3)) - t.true(isEqual(object1, object3)) + // Concordance detects a different circular reference in object1 before it + // does in object3, making them unequal. + t.false(isEqual(object1, object3)) }) test('compare objects with multiple circular references', t => { diff --git a/test/snapshots/diff.js.md b/test/snapshots/diff.js.md index 468c29f..2fb5de8 100644 --- a/test/snapshots/diff.js.md +++ b/test/snapshots/diff.js.md @@ -545,19 +545,20 @@ Generated by [AVA](https://ava.li). > Snapshot 1 `%diffGutters.padding# %%object.openBracket#{%␊ - %diffGutters.padding# % obj%property.separator#: %%object.openBracket#{%␊ - %diffGutters.padding# % obj%property.separator#: %%circular#[Circular]%%property.after#,%␊ - %diffGutters.padding# % %object.closeBracket#}%%property.after#,%␊ + %diffGutters.actual#- % obj%property.separator#: %%object.openBracket#{%␊ + %diffGutters.actual#- % obj%property.separator#: %%circular#[Circular]%%property.after#,%␊ + %diffGutters.actual#- % %object.closeBracket#}%%property.after#,%␊ + %diffGutters.expected#+ % obj%property.separator#: %%circular#[Circular]%%property.after#,%␊ %diffGutters.padding# %%object.closeBracket#}%` > Snapshot 2 `%diffGutters.padding# %%object.openBracket#{%␊ %diffGutters.padding# % obj%property.separator#: %%object.openBracket#{%␊ - %diffGutters.padding# % obj%property.separator#: %%object.openBracket#{%␊ - %diffGutters.actual#- % obj%property.separator#: %%circular#[Circular]%%property.after#,%␊ + %diffGutters.actual#- % obj%property.separator#: %%circular#[Circular]%%property.after#,%␊ + %diffGutters.expected#+ % obj%property.separator#: %%object.openBracket#{%␊ %diffGutters.expected#+ % obj%property.separator#: %%circular#[Circular]%%property.after#,%␊ - %diffGutters.padding# % %object.closeBracket#}%%property.after#,%␊ + %diffGutters.expected#+ % %object.closeBracket#}%%property.after#,%␊ %diffGutters.padding# % %object.closeBracket#}%%property.after#,%␊ %diffGutters.padding# %%object.closeBracket#}%` @@ -565,10 +566,10 @@ Generated by [AVA](https://ava.li). `%diffGutters.padding# %%list.openBracket#[%␊ %diffGutters.padding# % %list.openBracket#[%␊ - %diffGutters.padding# % %list.openBracket#[%␊ - %diffGutters.actual#- % %circular#[Circular]%%item.after#,%␊ + %diffGutters.actual#- % %circular#[Circular]%%item.after#,%␊ + %diffGutters.expected#+ % %list.openBracket#[%␊ %diffGutters.expected#+ % %circular#[Circular]%%item.after#,%␊ - %diffGutters.padding# % %list.closeBracket#]%%item.after#,%␊ + %diffGutters.expected#+ % %list.closeBracket#]%%item.after#,%␊ %diffGutters.padding# % %list.closeBracket#]%%item.after#,%␊ %diffGutters.padding# %%list.closeBracket#]%` @@ -576,10 +577,10 @@ Generated by [AVA](https://ava.li). `%diffGutters.padding# %%object.ctor.open%Map%object.ctor.close% %object.openBracket#{%␊ %diffGutters.padding# % %string.line.open#'%%string.open%map%string.close%%string.line.close#'%%mapEntry.separator# => %%object.ctor.open%Map%object.ctor.close% %object.openBracket#{%␊ - %diffGutters.padding# % %string.line.open#'%%string.open%map%string.close%%string.line.close#'%%mapEntry.separator# => %%object.ctor.open%Map%object.ctor.close% %object.openBracket#{%␊ - %diffGutters.actual#- % %string.line.open#'%%string.open%map%string.close%%string.line.close#'%%mapEntry.separator# => %%circular#[Circular]%%mapEntry.after#,%␊ + %diffGutters.actual#- % %string.line.open#'%%string.open%map%string.close%%string.line.close#'%%mapEntry.separator# => %%circular#[Circular]%%mapEntry.after#,%␊ + %diffGutters.expected#+ % %string.line.open#'%%string.open%map%string.close%%string.line.close#'%%mapEntry.separator# => %%object.ctor.open%Map%object.ctor.close% %object.openBracket#{%␊ %diffGutters.expected#+ % %string.line.open#'%%string.open%map%string.close%%string.line.close#'%%mapEntry.separator# => %%circular#[Circular]%%mapEntry.after#,%␊ - %diffGutters.padding# % %object.closeBracket#}%%mapEntry.after#,%␊ + %diffGutters.expected#+ % %object.closeBracket#}%%mapEntry.after#,%␊ %diffGutters.padding# % %object.closeBracket#}%%mapEntry.after#,%␊ %diffGutters.padding# %%object.closeBracket#}%` @@ -589,16 +590,16 @@ Generated by [AVA](https://ava.li). %diffGutters.padding# % %object.openBracket#{%␊ %diffGutters.padding# % key%property.separator#: %%boolean.open%true%boolean.close%%property.after#,%␊ %diffGutters.padding# % %object.closeBracket#}%%mapEntry.separator# => %%object.ctor.open%Map%object.ctor.close% %object.openBracket#{%␊ - %diffGutters.padding# % %object.openBracket#{%␊ - %diffGutters.padding# % key%property.separator#: %%boolean.open%true%boolean.close%%property.after#,%␊ - %diffGutters.padding# % %object.closeBracket#}%%mapEntry.separator# => %%object.ctor.open%Map%object.ctor.close% %object.openBracket#{%␊ - %diffGutters.actual#- % %object.openBracket#{%␊ - %diffGutters.actual#- % key%property.separator#: %%boolean.open%true%boolean.close%%property.after#,%␊ - %diffGutters.actual#- % %object.closeBracket#}%%mapEntry.separator# => %%circular#[Circular]%%mapEntry.after#,%␊ + %diffGutters.actual#- % %object.openBracket#{%␊ + %diffGutters.actual#- % key%property.separator#: %%boolean.open%true%boolean.close%%property.after#,%␊ + %diffGutters.actual#- % %object.closeBracket#}%%mapEntry.separator# => %%circular#[Circular]%%mapEntry.after#,%␊ + %diffGutters.expected#+ % %object.openBracket#{%␊ + %diffGutters.expected#+ % key%property.separator#: %%boolean.open%true%boolean.close%%property.after#,%␊ + %diffGutters.expected#+ % %object.closeBracket#}%%mapEntry.separator# => %%object.ctor.open%Map%object.ctor.close% %object.openBracket#{%␊ %diffGutters.expected#+ % %object.openBracket#{%␊ %diffGutters.expected#+ % key%property.separator#: %%boolean.open%true%boolean.close%%property.after#,%␊ %diffGutters.expected#+ % %object.closeBracket#}%%mapEntry.separator# => %%circular#[Circular]%%mapEntry.after#,%␊ - %diffGutters.padding# % %object.closeBracket#}%%mapEntry.after#,%␊ + %diffGutters.expected#+ % %object.closeBracket#}%%mapEntry.after#,%␊ %diffGutters.padding# % %object.closeBracket#}%%mapEntry.after#,%␊ %diffGutters.padding# %%object.closeBracket#}%` diff --git a/test/snapshots/diff.js.snap b/test/snapshots/diff.js.snap index cb57f1e..288ce03 100644 Binary files a/test/snapshots/diff.js.snap and b/test/snapshots/diff.js.snap differ