'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