392 lines
13 KiB
JavaScript
392 lines
13 KiB
JavaScript
|
'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
|