Files
codeql-action/node_modules/concordance/lib/primitiveValues/string.js
2021-10-25 08:56:16 -07:00

319 lines
11 KiB
JavaScript

'use strict'
const keyword = require('esutils').keyword
const fastDiff = require('fast-diff')
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, invert) {
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: '' }
let deleteWrap = isPartiallyEqual ? theme.string.diff.delete : noopWrap
let insertWrap = isPartiallyEqual ? theme.string.diff.insert : noopWrap
const equalWrap = isPartiallyEqual ? theme.string.diff.equal : noopWrap
if (invert) {
[deleteWrap, insertWrap] = [insertWrap, deleteWrap]
}
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) {
const deleteLineWrap = invert ? theme.string.diff.insertLine : theme.string.diff.deleteLine
const insertLineWrap = invert ? theme.string.diff.deleteLine : theme.string.diff.insertLine
stringActual = formatUtils.wrap(deleteLineWrap, stringActual)
stringExpected = formatUtils.wrap(insertLineWrap, 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, invert) {
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),
invert,
)
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 || expectedIndex > 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 wrap = invert ? theme.string.diff.insertLine : theme.string.diff.deleteLine
const string = formatUtils.wrap(wrap, 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 wrap = invert ? theme.string.diff.deleteLine : theme.string.diff.insertLine
const string = formatUtils.wrap(wrap, 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], invert)
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 })