1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-24 16:19:12 -05:00

fix(ext/node): use primordials in ext/node/polyfills/_util (#21444)

This commit is contained in:
Kenta Moriuchi 2023-12-08 18:00:03 +09:00 committed by GitHub
parent 3a74fa60ca
commit b24356d9b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 344 additions and 167 deletions

View file

@ -21,12 +21,20 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials
// These are simplified versions of the "real" errors in Node.
import { primordials } from "ext:core/mod.js";
import { nextTick } from "ext:deno_node/_next_tick.ts";
const {
ArrayPrototypePop,
Error,
FunctionPrototypeApply,
FunctionPrototypeBind,
ObjectDefineProperties,
ObjectGetOwnPropertyDescriptors,
PromisePrototypeThen,
TypeError,
} = primordials;
class NodeFalsyValueRejectionError extends Error {
public reason: unknown;
@ -98,25 +106,26 @@ function callbackify<ResultT>(
}
const callbackified = function (this: unknown, ...args: unknown[]) {
const maybeCb = args.pop();
const maybeCb = ArrayPrototypePop(args);
if (typeof maybeCb !== "function") {
throw new NodeInvalidArgTypeError("last");
}
const cb = (...args: unknown[]) => {
maybeCb.apply(this, args);
FunctionPrototypeApply(maybeCb, this, args);
};
original.apply(this, args).then(
PromisePrototypeThen(
FunctionPrototypeApply(this, args),
(ret: unknown) => {
nextTick(cb.bind(this, null, ret));
nextTick(FunctionPrototypeBind(cb, this, null, ret));
},
(rej: unknown) => {
rej = rej || new NodeFalsyValueRejectionError(rej);
nextTick(cb.bind(this, rej));
nextTick(FunctionPrototypeBind(cb, this, rej));
},
);
};
const descriptors = Object.getOwnPropertyDescriptors(original);
const descriptors = ObjectGetOwnPropertyDescriptors(original);
// It is possible to manipulate a functions `length` or `name` property. This
// guards against the manipulation.
if (typeof descriptors.length.value === "number") {
@ -125,7 +134,7 @@ function callbackify<ResultT>(
if (typeof descriptors.name.value === "string") {
descriptors.name.value += "Callbackified";
}
Object.defineProperties(callbackified, descriptors);
ObjectDefineProperties(callbackified, descriptors);
return callbackified;
}

View file

@ -1,7 +1,9 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials
import { primordials } from "ext:core/mod.js";
const {
Error,
} = primordials;
/** Assertion error class for node compat layer's internal code. */
export class NodeCompatAssertionError extends Error {

View file

@ -2,8 +2,12 @@
// This module is vendored from std/async/delay.ts
// (with some modifications)
// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials
import { primordials } from "ext:core/mod.js";
import { clearTimeout, setTimeout } from "ext:deno_web/02_timers.js";
const {
Promise,
PromiseReject,
} = primordials;
/** Resolve a Promise after a given amount of milliseconds. */
export function delay(
@ -12,12 +16,12 @@ export function delay(
): Promise<void> {
const { signal } = options;
if (signal?.aborted) {
return Promise.reject(new DOMException("Delay was aborted.", "AbortError"));
return PromiseReject(signal.reason);
}
return new Promise((resolve, reject) => {
const abort = () => {
clearTimeout(i);
reject(new DOMException("Delay was aborted.", "AbortError"));
reject(signal!.reason);
};
const done = () => {
signal?.removeEventListener("abort", abort);

View file

@ -1,6 +1,7 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
const { ops } = globalThis.__bootstrap.core;
import { core } from "ext:core/mod.js";
const ops = core.ops;
export type OSType = "windows" | "linux" | "darwin" | "freebsd" | "openbsd";

View file

@ -1,15 +1,43 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// vendored from std/assert/mod.ts
// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials
import { primordials } from "ext:core/mod.js";
import { URLPrototype } from "ext:deno_url/00_url.js";
import { red } from "ext:deno_node/_util/std_fmt_colors.ts";
import {
buildMessage,
diff,
diffstr,
} from "ext:deno_node/_util/std_testing_diff.ts";
const {
DatePrototype,
ArrayPrototypeJoin,
ArrayPrototypeMap,
DatePrototypeGetTime,
Error,
NumberIsNaN,
Object,
ObjectIs,
ObjectKeys,
ObjectPrototypeIsPrototypeOf,
ReflectHas,
ReflectOwnKeys,
RegExpPrototype,
RegExpPrototypeTest,
SafeMap,
SafeRegExp,
String,
StringPrototypeReplace,
StringPrototypeSplit,
SymbolIterator,
TypeError,
WeakMapPrototype,
WeakSetPrototype,
WeakRefPrototype,
WeakRefPrototypeDeref,
} = primordials;
const FORMAT_PATTERN = new SafeRegExp(/(?=["\\])/g);
/** Converts the input into a string. Objects, Sets and Maps are sorted so as to
* make tests less flaky */
@ -26,7 +54,7 @@ export function format(v: unknown): string {
// getters should be true in assertEquals.
getters: true,
})
: `"${String(v).replace(/(?=["\\])/g, "\\")}"`;
: `"${StringPrototypeReplace(String(v), FORMAT_PATTERN, "\\")}"`;
}
const CAN_NOT_DISPLAY = "[Cannot display]";
@ -38,56 +66,75 @@ export class AssertionError extends Error {
}
}
function isKeyedCollection(x: unknown): x is Set<unknown> {
return [Symbol.iterator, "size"].every((k) => k in (x as Set<unknown>));
function isKeyedCollection(
x: unknown,
): x is { size: number; entries(): Iterable<[unknown, unknown]> } {
return ReflectHas(x, SymbolIterator) && ReflectHas(x, "size");
}
/** Deep equality comparison used in assertions */
export function equal(c: unknown, d: unknown): boolean {
const seen = new Map();
const seen = new SafeMap();
return (function compare(a: unknown, b: unknown): boolean {
// Have to render RegExp & Date for string comparison
// unless it's mistreated as object
if (
a &&
b &&
((a instanceof RegExp && b instanceof RegExp) ||
(a instanceof URL && b instanceof URL))
((ObjectPrototypeIsPrototypeOf(RegExpPrototype, a) &&
ObjectPrototypeIsPrototypeOf(RegExpPrototype, b)) ||
(ObjectPrototypeIsPrototypeOf(URLPrototype, a) &&
ObjectPrototypeIsPrototypeOf(URLPrototype, b)))
) {
return String(a) === String(b);
}
if (a instanceof Date && b instanceof Date) {
const aTime = a.getTime();
const bTime = b.getTime();
if (
ObjectPrototypeIsPrototypeOf(DatePrototype, a) &&
ObjectPrototypeIsPrototypeOf(DatePrototype, b)
) {
const aTime = DatePrototypeGetTime(a);
const bTime = DatePrototypeGetTime(b);
// Check for NaN equality manually since NaN is not
// equal to itself.
if (Number.isNaN(aTime) && Number.isNaN(bTime)) {
if (NumberIsNaN(aTime) && NumberIsNaN(bTime)) {
return true;
}
return aTime === bTime;
}
if (typeof a === "number" && typeof b === "number") {
return Number.isNaN(a) && Number.isNaN(b) || a === b;
return NumberIsNaN(a) && NumberIsNaN(b) || a === b;
}
if (Object.is(a, b)) {
if (ObjectIs(a, b)) {
return true;
}
if (a && typeof a === "object" && b && typeof b === "object") {
if (a && b && !constructorsEqual(a, b)) {
return false;
}
if (a instanceof WeakMap || b instanceof WeakMap) {
if (!(a instanceof WeakMap && b instanceof WeakMap)) return false;
if (
ObjectPrototypeIsPrototypeOf(WeakMapPrototype, a) ||
ObjectPrototypeIsPrototypeOf(WeakMapPrototype, b)
) {
if (
!(ObjectPrototypeIsPrototypeOf(WeakMapPrototype, a) &&
ObjectPrototypeIsPrototypeOf(WeakMapPrototype, b))
) return false;
throw new TypeError("cannot compare WeakMap instances");
}
if (a instanceof WeakSet || b instanceof WeakSet) {
if (!(a instanceof WeakSet && b instanceof WeakSet)) return false;
if (
ObjectPrototypeIsPrototypeOf(WeakSetPrototype, a) ||
ObjectPrototypeIsPrototypeOf(WeakSetPrototype, b)
) {
if (
!(ObjectPrototypeIsPrototypeOf(WeakSetPrototype, a) &&
ObjectPrototypeIsPrototypeOf(WeakSetPrototype, b))
) return false;
throw new TypeError("cannot compare WeakSet instances");
}
if (seen.get(a) === b) {
return true;
}
if (Object.keys(a || {}).length !== Object.keys(b || {}).length) {
if (ObjectKeys(a || {}).length !== ObjectKeys(b || {}).length) {
return false;
}
seen.set(a, b);
@ -98,7 +145,10 @@ export function equal(c: unknown, d: unknown): boolean {
let unmatchedEntries = a.size;
// TODO(petamoriken): use primordials
// deno-lint-ignore prefer-primordials
for (const [aKey, aValue] of a.entries()) {
// deno-lint-ignore prefer-primordials
for (const [bKey, bValue] of b.entries()) {
/* Given that Map keys can be references, we need
* to ensure that they are also deeply equal */
@ -111,27 +161,34 @@ export function equal(c: unknown, d: unknown): boolean {
}
}
}
return unmatchedEntries === 0;
}
const merged = { ...a, ...b };
for (
const key of [
...Object.getOwnPropertyNames(merged),
...Object.getOwnPropertySymbols(merged),
]
) {
const keys = ReflectOwnKeys(merged);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
type Key = keyof typeof merged;
if (!compare(a && a[key as Key], b && b[key as Key])) {
return false;
}
if (((key in a) && (!(key in b))) || ((key in b) && (!(key in a)))) {
if (
(ReflectHas(a, key) && !ReflectHas(b, key)) ||
(ReflectHas(b, key) && !ReflectHas(a, key))
) {
return false;
}
}
if (a instanceof WeakRef || b instanceof WeakRef) {
if (!(a instanceof WeakRef && b instanceof WeakRef)) return false;
return compare(a.deref(), b.deref());
if (
ObjectPrototypeIsPrototypeOf(WeakRefPrototype, a) ||
ObjectPrototypeIsPrototypeOf(WeakRefPrototype, b)
) {
if (
!(ObjectPrototypeIsPrototypeOf(WeakRefPrototype, a) &&
ObjectPrototypeIsPrototypeOf(WeakRefPrototype, b))
) return false;
return compare(WeakRefPrototypeDeref(a), WeakRefPrototypeDeref(b));
}
return true;
}
@ -166,8 +223,14 @@ export function assertEquals<T>(actual: T, expected: T, msg?: string) {
(typeof expected === "string");
const diffResult = stringDiff
? diffstr(actual as string, expected as string)
: diff(actualString.split("\n"), expectedString.split("\n"));
const diffMsg = buildMessage(diffResult, { stringDiff }).join("\n");
: diff(
StringPrototypeSplit(actualString, "\n"),
StringPrototypeSplit(expectedString, "\n"),
);
const diffMsg = ArrayPrototypeJoin(
buildMessage(diffResult, { stringDiff }),
"\n",
);
message = `Values are not equal:\n${diffMsg}`;
} catch {
message = `\n${red(red(CAN_NOT_DISPLAY))} + \n\n`;
@ -209,7 +272,7 @@ export function assertStrictEquals<T>(
expected: T,
msg?: string,
): asserts actual is T {
if (Object.is(actual, expected)) {
if (ObjectIs(actual, expected)) {
return;
}
@ -222,10 +285,13 @@ export function assertStrictEquals<T>(
const expectedString = format(expected);
if (actualString === expectedString) {
const withOffset = actualString
.split("\n")
.map((l) => ` ${l}`)
.join("\n");
const withOffset = ArrayPrototypeJoin(
ArrayPrototypeMap(
StringPrototypeSplit(actualString, "\n"),
(l: string) => ` ${l}`,
),
"\n",
);
message =
`Values have the same structure but are not reference-equal:\n\n${
red(withOffset)
@ -236,8 +302,14 @@ export function assertStrictEquals<T>(
(typeof expected === "string");
const diffResult = stringDiff
? diffstr(actual as string, expected as string)
: diff(actualString.split("\n"), expectedString.split("\n"));
const diffMsg = buildMessage(diffResult, { stringDiff }).join("\n");
: diff(
StringPrototypeSplit(actualString, "\n"),
StringPrototypeSplit(expectedString, "\n"),
);
const diffMsg = ArrayPrototypeJoin(
buildMessage(diffResult, { stringDiff }),
"\n",
);
message = `Values are not strictly equal:\n${diffMsg}`;
} catch {
message = `\n${CAN_NOT_DISPLAY} + \n\n`;
@ -255,7 +327,7 @@ export function assertNotStrictEquals<T>(
expected: T,
msg?: string,
) {
if (!Object.is(actual, expected)) {
if (!ObjectIs(actual, expected)) {
return;
}
@ -268,10 +340,11 @@ export function assertNotStrictEquals<T>(
* then throw. */
export function assertMatch(
actual: string,
// deno-lint-ignore prefer-primordials
expected: RegExp,
msg?: string,
) {
if (!expected.test(actual)) {
if (!RegExpPrototypeTest(expected, actual)) {
if (!msg) {
msg = `actual: "${actual}" expected to match: "${expected}"`;
}
@ -283,10 +356,11 @@ export function assertMatch(
* then throw. */
export function assertNotMatch(
actual: string,
// deno-lint-ignore prefer-primordials
expected: RegExp,
msg?: string,
) {
if (expected.test(actual)) {
if (RegExpPrototypeTest(expected, actual)) {
if (!msg) {
msg = `actual: "${actual}" expected to not match: "${expected}"`;
}

View file

@ -1,8 +1,15 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// This file is vendored from std/fmt/colors.ts
// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials
import { primordials } from "ext:core/mod.js";
const {
ArrayPrototypeJoin,
MathMax,
MathMin,
MathTrunc,
SafeRegExp,
StringPrototypeReplace,
} = primordials;
// TODO(kt3k): Initialize this at the start of runtime
// based on Deno.noColor
@ -11,6 +18,7 @@ const noColor = false;
interface Code {
open: string;
close: string;
// deno-lint-ignore prefer-primordials
regexp: RegExp;
}
@ -47,9 +55,9 @@ export function getColorEnabled(): boolean {
*/
function code(open: number[], close: number): Code {
return {
open: `\x1b[${open.join(";")}m`,
open: `\x1b[${ArrayPrototypeJoin(open, ";")}m`,
close: `\x1b[${close}m`,
regexp: new RegExp(`\\x1b\\[${close}m`, "g"),
regexp: new SafeRegExp(`\\x1b\\[${close}m`, "g"),
};
}
@ -60,7 +68,9 @@ function code(open: number[], close: number): Code {
*/
function run(str: string, code: Code): string {
return enabled
? `${code.open}${str.replace(code.regexp, code.open)}${code.close}`
? `${code.open}${
StringPrototypeReplace(str, code.regexp, code.open)
}${code.close}`
: str;
}
@ -401,7 +411,7 @@ export function bgBrightWhite(str: string): string {
* @param min number to truncate from
*/
function clampAndTruncate(n: number, max = 255, min = 0): number {
return Math.trunc(Math.max(Math.min(n, max), min));
return MathTrunc(MathMax(MathMin(n, max), min));
}
/**
@ -505,11 +515,11 @@ export function bgRgb24(str: string, color: number | Rgb): string {
}
// https://github.com/chalk/ansi-regex/blob/02fa893d619d3da85411acc8fd4e2eea0e95a9d9/index.js
const ANSI_PATTERN = new RegExp(
[
const ANSI_PATTERN = new SafeRegExp(
ArrayPrototypeJoin([
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))",
].join("|"),
], "|"),
"g",
);
@ -518,5 +528,5 @@ const ANSI_PATTERN = new RegExp(
* @param string to remove ANSI escape codes from
*/
export function stripColor(string: string): string {
return string.replace(ANSI_PATTERN, "");
return StringPrototypeReplace(string, ANSI_PATTERN, "");
}

View file

@ -1,9 +1,7 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// This file was vendored from std/testing/_diff.ts
// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials
import { primordials } from "ext:core/mod.js";
import {
bgGreen,
bgRed,
@ -13,6 +11,30 @@ import {
red,
white,
} from "ext:deno_node/_util/std_fmt_colors.ts";
const {
ArrayFrom,
ArrayPrototypeFilter,
ArrayPrototypeForEach,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePop,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeReverse,
ArrayPrototypeShift,
ArrayPrototypeSlice,
ArrayPrototypeSplice,
ArrayPrototypeSome,
ArrayPrototypeUnshift,
SafeArrayIterator,
SafeRegExp,
StringPrototypeSplit,
StringPrototypeReplace,
StringPrototypeTrim,
MathMin,
ObjectFreeze,
Uint32Array,
} = primordials;
interface FarthestPoint {
y: number;
@ -28,7 +50,7 @@ export enum DiffType {
export interface DiffResult<T> {
type: DiffType;
value: T;
details?: Array<DiffResult<T>>;
details?: DiffResult<T>[];
}
const REMOVED = 1;
@ -38,11 +60,11 @@ const ADDED = 3;
function createCommon<T>(A: T[], B: T[], reverse?: boolean): T[] {
const common = [];
if (A.length === 0 || B.length === 0) return [];
for (let i = 0; i < Math.min(A.length, B.length); i += 1) {
for (let i = 0; i < MathMin(A.length, B.length); i += 1) {
if (
A[reverse ? A.length - i - 1 : i] === B[reverse ? B.length - i - 1 : i]
) {
common.push(A[reverse ? A.length - i - 1 : i]);
ArrayPrototypePush(common, A[reverse ? A.length - i - 1 : i]);
} else {
return common;
}
@ -55,44 +77,56 @@ function createCommon<T>(A: T[], B: T[], reverse?: boolean): T[] {
* @param A Actual value
* @param B Expected value
*/
export function diff<T>(A: T[], B: T[]): Array<DiffResult<T>> {
export function diff<T>(A: T[], B: T[]): DiffResult<T>[] {
const prefixCommon = createCommon(A, B);
const suffixCommon = createCommon(
A.slice(prefixCommon.length),
B.slice(prefixCommon.length),
const suffixCommon = ArrayPrototypeReverse(createCommon(
ArrayPrototypeSlice(A, prefixCommon.length),
ArrayPrototypeSlice(B, prefixCommon.length),
true,
).reverse();
));
A = suffixCommon.length
? A.slice(prefixCommon.length, -suffixCommon.length)
: A.slice(prefixCommon.length);
? ArrayPrototypeSlice(A, prefixCommon.length, -suffixCommon.length)
: ArrayPrototypeSlice(A, prefixCommon.length);
B = suffixCommon.length
? B.slice(prefixCommon.length, -suffixCommon.length)
: B.slice(prefixCommon.length);
? ArrayPrototypeSlice(B, prefixCommon.length, -suffixCommon.length)
: ArrayPrototypeSlice(B, prefixCommon.length);
const swapped = B.length > A.length;
[A, B] = swapped ? [B, A] : [A, B];
if (swapped) {
const temp = A;
A = B;
B = temp;
}
const M = A.length;
const N = B.length;
if (!M && !N && !suffixCommon.length && !prefixCommon.length) return [];
if (!N) {
if (
M === 0 && N === 0 && suffixCommon.length === 0 && prefixCommon.length === 0
) return [];
if (N === 0) {
return [
...prefixCommon.map(
(c): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
...new SafeArrayIterator(
ArrayPrototypeMap(
prefixCommon,
(c: T): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
),
),
...A.map(
(a): DiffResult<typeof a> => ({
...new SafeArrayIterator(
ArrayPrototypeMap(A, (a: T): DiffResult<typeof a> => ({
type: swapped ? DiffType.added : DiffType.removed,
value: a,
}),
})),
),
...suffixCommon.map(
(c): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
...new SafeArrayIterator(
ArrayPrototypeMap(
suffixCommon,
(c: T): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
),
),
];
}
const offset = N;
const delta = M - N;
const size = M + N + 1;
const fp: FarthestPoint[] = Array.from(
const fp: FarthestPoint[] = ArrayFrom(
{ length: size },
() => ({ y: -1, id: -1 }),
);
@ -114,13 +148,13 @@ export function diff<T>(A: T[], B: T[]): Array<DiffResult<T>> {
B: T[],
current: FarthestPoint,
swapped: boolean,
): Array<{
): {
type: DiffType;
value: T;
}> {
}[] {
const M = A.length;
const N = B.length;
const result = [];
const result: DiffResult<T>[] = [];
let a = M - 1;
let b = N - 1;
let j = routes[current.id];
@ -129,19 +163,19 @@ export function diff<T>(A: T[], B: T[]): Array<DiffResult<T>> {
if (!j && !type) break;
const prev = j;
if (type === REMOVED) {
result.unshift({
ArrayPrototypeUnshift(result, {
type: swapped ? DiffType.removed : DiffType.added,
value: B[b],
});
b -= 1;
} else if (type === ADDED) {
result.unshift({
ArrayPrototypeUnshift(result, {
type: swapped ? DiffType.added : DiffType.removed,
value: A[a],
});
a -= 1;
} else {
result.unshift({ type: DiffType.common, value: A[a] });
ArrayPrototypeUnshift(result, { type: DiffType.common, value: A[a] });
a -= 1;
b -= 1;
}
@ -234,16 +268,40 @@ export function diff<T>(A: T[], B: T[]): Array<DiffResult<T>> {
);
}
return [
...prefixCommon.map(
(c): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
...new SafeArrayIterator(
ArrayPrototypeMap(
prefixCommon,
(c: T): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
),
),
...backTrace(A, B, fp[delta + offset], swapped),
...suffixCommon.map(
(c): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
...new SafeArrayIterator(backTrace(A, B, fp[delta + offset], swapped)),
...new SafeArrayIterator(
ArrayPrototypeMap(
suffixCommon,
(c: T): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
),
),
];
}
const ESCAPE_PATTERN = new SafeRegExp(/([\b\f\t\v])/g);
const ESCAPE_MAP = ObjectFreeze({
"\b": "\\b",
"\f": "\\f",
"\t": "\\t",
"\v": "\\v",
});
const LINE_BREAK_GLOBAL_PATTERN = new SafeRegExp(/\r\n|\r|\n/g);
const LINE_BREAK_PATTERN = new SafeRegExp(/(\n|\r\n)/);
const WHITESPACE_PATTERN = new SafeRegExp(/\s+/);
const WHITESPACE_SYMBOL_PATTERN = new SafeRegExp(
/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/,
);
const LATIN_CHARACTER_PATTERN = new SafeRegExp(
/^[a-zA-Z\u{C0}-\u{FF}\u{D8}-\u{F6}\u{F8}-\u{2C6}\u{2C8}-\u{2D7}\u{2DE}-\u{2FF}\u{1E00}-\u{1EFF}]+$/u,
);
/**
* Renders the differences between the actual and expected strings
* Partially inspired from https://github.com/kpdecker/jsdiff
@ -254,44 +312,44 @@ export function diffstr(A: string, B: string) {
function unescape(string: string): string {
// unescape invisible characters.
// ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#escape_sequences
return string
.replaceAll("\b", "\\b")
.replaceAll("\f", "\\f")
.replaceAll("\t", "\\t")
.replaceAll("\v", "\\v")
.replaceAll( // does not remove line breaks
/\r\n|\r|\n/g,
(str) => str === "\r" ? "\\r" : str === "\n" ? "\\n\n" : "\\r\\n\r\n",
);
return StringPrototypeReplace(
StringPrototypeReplace(
string,
ESCAPE_PATTERN,
(c: string) => ESCAPE_MAP[c],
),
LINE_BREAK_GLOBAL_PATTERN, // does not remove line breaks
(str: string) =>
str === "\r" ? "\\r" : str === "\n" ? "\\n\n" : "\\r\\n\r\n",
);
}
function tokenize(string: string, { wordDiff = false } = {}): string[] {
if (wordDiff) {
// Split string on whitespace symbols
const tokens = string.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/);
// Extended Latin character set
const words =
/^[a-zA-Z\u{C0}-\u{FF}\u{D8}-\u{F6}\u{F8}-\u{2C6}\u{2C8}-\u{2D7}\u{2DE}-\u{2FF}\u{1E00}-\u{1EFF}]+$/u;
const tokens = StringPrototypeSplit(string, WHITESPACE_SYMBOL_PATTERN);
// Join boundary splits that we do not consider to be boundaries and merge empty strings surrounded by word chars
for (let i = 0; i < tokens.length - 1; i++) {
if (
!tokens[i + 1] && tokens[i + 2] && words.test(tokens[i]) &&
words.test(tokens[i + 2])
!tokens[i + 1] && tokens[i + 2] &&
LATIN_CHARACTER_PATTERN.test(tokens[i]) &&
LATIN_CHARACTER_PATTERN.test(tokens[i + 2])
) {
tokens[i] += tokens[i + 2];
tokens.splice(i + 1, 2);
ArrayPrototypeSplice(tokens, i + 1, 2);
i--;
}
}
return tokens.filter((token) => token);
return ArrayPrototypeFilter(tokens, (token: string) => token);
} else {
// Split string on new lines symbols
const tokens = [], lines = string.split(/(\n|\r\n)/);
const tokens: string[] = [],
lines: string[] = StringPrototypeSplit(string, LINE_BREAK_PATTERN);
// Ignore final empty token when text ends with a newline
if (!lines[lines.length - 1]) {
lines.pop();
if (lines[lines.length - 1] === "") {
ArrayPrototypePop(lines);
}
// Merge the content and line separators into single tokens
@ -299,7 +357,7 @@ export function diffstr(A: string, B: string) {
if (i % 2) {
tokens[tokens.length - 1] += lines[i];
} else {
tokens.push(lines[i]);
ArrayPrototypePush(tokens, lines[i]);
}
}
return tokens;
@ -310,22 +368,28 @@ export function diffstr(A: string, B: string) {
// and merge "space-diff" if surrounded by word-diff for cleaner displays
function createDetails(
line: DiffResult<string>,
tokens: Array<DiffResult<string>>,
tokens: DiffResult<string>[],
) {
return tokens.filter(({ type }) =>
type === line.type || type === DiffType.common
).map((result, i, t) => {
if (
(result.type === DiffType.common) && (t[i - 1]) &&
(t[i - 1]?.type === t[i + 1]?.type) && /\s+/.test(result.value)
) {
return {
...result,
type: t[i - 1].type,
};
}
return result;
});
return ArrayPrototypeMap(
ArrayPrototypeFilter(
tokens,
({ type }: DiffResult<string>) =>
type === line.type || type === DiffType.common,
),
(result: DiffResult<string>, i: number, t: DiffResult<string>[]) => {
if (
(result.type === DiffType.common) && (t[i - 1]) &&
(t[i - 1]?.type === t[i + 1]?.type) &&
WHITESPACE_PATTERN.test(result.value)
) {
return {
...result,
type: t[i - 1].type,
};
}
return result;
},
);
}
// Compute multi-line diff
@ -334,32 +398,36 @@ export function diffstr(A: string, B: string) {
tokenize(`${unescape(B)}\n`),
);
const added = [], removed = [];
for (const result of diffResult) {
const added: DiffResult<string>[] = [], removed: DiffResult<string>[] = [];
for (let i = 0; i < diffResult.length; ++i) {
const result = diffResult[i];
if (result.type === DiffType.added) {
added.push(result);
ArrayPrototypePush(added, result);
}
if (result.type === DiffType.removed) {
removed.push(result);
ArrayPrototypePush(removed, result);
}
}
// Compute word-diff
const aLines = added.length < removed.length ? added : removed;
const bLines = aLines === removed ? added : removed;
for (const a of aLines) {
let tokens = [] as Array<DiffResult<string>>,
for (let i = 0; i < aLines.length; ++i) {
const a = aLines[i];
let tokens = [] as DiffResult<string>[],
b: undefined | DiffResult<string>;
// Search another diff line with at least one common token
while (bLines.length) {
b = bLines.shift();
while (bLines.length !== 0) {
b = ArrayPrototypeShift(bLines);
tokens = diff(
tokenize(a.value, { wordDiff: true }),
tokenize(b?.value ?? "", { wordDiff: true }),
);
if (
tokens.some(({ type, value }) =>
type === DiffType.common && value.trim().length
ArrayPrototypeSome(
tokens,
({ type, value }) =>
type === DiffType.common && StringPrototypeTrim(value).length,
)
) {
break;
@ -418,26 +486,35 @@ export function buildMessage(
{ stringDiff = false } = {},
): string[] {
const messages: string[] = [], diffMessages: string[] = [];
messages.push("");
messages.push("");
messages.push(
ArrayPrototypePush(messages, "");
ArrayPrototypePush(messages, "");
ArrayPrototypePush(
messages,
` ${gray(bold("[Diff]"))} ${red(bold("Actual"))} / ${
green(bold("Expected"))
}`,
);
messages.push("");
messages.push("");
diffResult.forEach((result: DiffResult<string>) => {
ArrayPrototypePush(messages, "");
ArrayPrototypePush(messages, "");
ArrayPrototypeForEach(diffResult, (result: DiffResult<string>) => {
const c = createColor(result.type);
const line = result.details?.map((detail) =>
detail.type !== DiffType.common
? createColor(detail.type, { background: true })(detail.value)
: detail.value
).join("") ?? result.value;
diffMessages.push(c(`${createSign(result.type)}${line}`));
const line = result.details != null
? ArrayPrototypeJoin(
ArrayPrototypeMap(result.details, (detail) =>
detail.type !== DiffType.common
? createColor(detail.type, { background: true })(detail.value)
: detail.value),
"",
)
: result.value;
ArrayPrototypePush(diffMessages, c(`${createSign(result.type)}${line}`));
});
messages.push(...(stringDiff ? [diffMessages.join("")] : diffMessages));
messages.push("");
ArrayPrototypePushApply(
messages,
stringDiff ? [ArrayPrototypeJoin(diffMessages, "")] : diffMessages,
);
ArrayPrototypePush(messages, "");
return messages;
}