0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 17:34:47 -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 // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE. // 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. // 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"; import { nextTick } from "ext:deno_node/_next_tick.ts";
const {
ArrayPrototypePop,
Error,
FunctionPrototypeApply,
FunctionPrototypeBind,
ObjectDefineProperties,
ObjectGetOwnPropertyDescriptors,
PromisePrototypeThen,
TypeError,
} = primordials;
class NodeFalsyValueRejectionError extends Error { class NodeFalsyValueRejectionError extends Error {
public reason: unknown; public reason: unknown;
@ -98,25 +106,26 @@ function callbackify<ResultT>(
} }
const callbackified = function (this: unknown, ...args: unknown[]) { const callbackified = function (this: unknown, ...args: unknown[]) {
const maybeCb = args.pop(); const maybeCb = ArrayPrototypePop(args);
if (typeof maybeCb !== "function") { if (typeof maybeCb !== "function") {
throw new NodeInvalidArgTypeError("last"); throw new NodeInvalidArgTypeError("last");
} }
const cb = (...args: unknown[]) => { const cb = (...args: unknown[]) => {
maybeCb.apply(this, args); FunctionPrototypeApply(maybeCb, this, args);
}; };
original.apply(this, args).then( PromisePrototypeThen(
FunctionPrototypeApply(this, args),
(ret: unknown) => { (ret: unknown) => {
nextTick(cb.bind(this, null, ret)); nextTick(FunctionPrototypeBind(cb, this, null, ret));
}, },
(rej: unknown) => { (rej: unknown) => {
rej = rej || new NodeFalsyValueRejectionError(rej); 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 // It is possible to manipulate a functions `length` or `name` property. This
// guards against the manipulation. // guards against the manipulation.
if (typeof descriptors.length.value === "number") { if (typeof descriptors.length.value === "number") {
@ -125,7 +134,7 @@ function callbackify<ResultT>(
if (typeof descriptors.name.value === "string") { if (typeof descriptors.name.value === "string") {
descriptors.name.value += "Callbackified"; descriptors.name.value += "Callbackified";
} }
Object.defineProperties(callbackified, descriptors); ObjectDefineProperties(callbackified, descriptors);
return callbackified; return callbackified;
} }

View file

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

View file

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

View file

@ -1,6 +1,7 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. // 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"; 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. // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// vendored from std/assert/mod.ts // vendored from std/assert/mod.ts
// TODO(petamoriken): enable prefer-primordials for node polyfills import { primordials } from "ext:core/mod.js";
// deno-lint-ignore-file prefer-primordials import { URLPrototype } from "ext:deno_url/00_url.js";
import { red } from "ext:deno_node/_util/std_fmt_colors.ts"; import { red } from "ext:deno_node/_util/std_fmt_colors.ts";
import { import {
buildMessage, buildMessage,
diff, diff,
diffstr, diffstr,
} from "ext:deno_node/_util/std_testing_diff.ts"; } 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 /** Converts the input into a string. Objects, Sets and Maps are sorted so as to
* make tests less flaky */ * make tests less flaky */
@ -26,7 +54,7 @@ export function format(v: unknown): string {
// getters should be true in assertEquals. // getters should be true in assertEquals.
getters: true, getters: true,
}) })
: `"${String(v).replace(/(?=["\\])/g, "\\")}"`; : `"${StringPrototypeReplace(String(v), FORMAT_PATTERN, "\\")}"`;
} }
const CAN_NOT_DISPLAY = "[Cannot display]"; const CAN_NOT_DISPLAY = "[Cannot display]";
@ -38,56 +66,75 @@ export class AssertionError extends Error {
} }
} }
function isKeyedCollection(x: unknown): x is Set<unknown> { function isKeyedCollection(
return [Symbol.iterator, "size"].every((k) => k in (x as Set<unknown>)); x: unknown,
): x is { size: number; entries(): Iterable<[unknown, unknown]> } {
return ReflectHas(x, SymbolIterator) && ReflectHas(x, "size");
} }
/** Deep equality comparison used in assertions */ /** Deep equality comparison used in assertions */
export function equal(c: unknown, d: unknown): boolean { export function equal(c: unknown, d: unknown): boolean {
const seen = new Map(); const seen = new SafeMap();
return (function compare(a: unknown, b: unknown): boolean { return (function compare(a: unknown, b: unknown): boolean {
// Have to render RegExp & Date for string comparison // Have to render RegExp & Date for string comparison
// unless it's mistreated as object // unless it's mistreated as object
if ( if (
a && a &&
b && b &&
((a instanceof RegExp && b instanceof RegExp) || ((ObjectPrototypeIsPrototypeOf(RegExpPrototype, a) &&
(a instanceof URL && b instanceof URL)) ObjectPrototypeIsPrototypeOf(RegExpPrototype, b)) ||
(ObjectPrototypeIsPrototypeOf(URLPrototype, a) &&
ObjectPrototypeIsPrototypeOf(URLPrototype, b)))
) { ) {
return String(a) === String(b); return String(a) === String(b);
} }
if (a instanceof Date && b instanceof Date) { if (
const aTime = a.getTime(); ObjectPrototypeIsPrototypeOf(DatePrototype, a) &&
const bTime = b.getTime(); ObjectPrototypeIsPrototypeOf(DatePrototype, b)
) {
const aTime = DatePrototypeGetTime(a);
const bTime = DatePrototypeGetTime(b);
// Check for NaN equality manually since NaN is not // Check for NaN equality manually since NaN is not
// equal to itself. // equal to itself.
if (Number.isNaN(aTime) && Number.isNaN(bTime)) { if (NumberIsNaN(aTime) && NumberIsNaN(bTime)) {
return true; return true;
} }
return aTime === bTime; return aTime === bTime;
} }
if (typeof a === "number" && typeof b === "number") { 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; return true;
} }
if (a && typeof a === "object" && b && typeof b === "object") { if (a && typeof a === "object" && b && typeof b === "object") {
if (a && b && !constructorsEqual(a, b)) { if (a && b && !constructorsEqual(a, b)) {
return false; return false;
} }
if (a instanceof WeakMap || b instanceof WeakMap) { if (
if (!(a instanceof WeakMap && b instanceof WeakMap)) return false; ObjectPrototypeIsPrototypeOf(WeakMapPrototype, a) ||
ObjectPrototypeIsPrototypeOf(WeakMapPrototype, b)
) {
if (
!(ObjectPrototypeIsPrototypeOf(WeakMapPrototype, a) &&
ObjectPrototypeIsPrototypeOf(WeakMapPrototype, b))
) return false;
throw new TypeError("cannot compare WeakMap instances"); throw new TypeError("cannot compare WeakMap instances");
} }
if (a instanceof WeakSet || b instanceof WeakSet) { if (
if (!(a instanceof WeakSet && b instanceof WeakSet)) return false; ObjectPrototypeIsPrototypeOf(WeakSetPrototype, a) ||
ObjectPrototypeIsPrototypeOf(WeakSetPrototype, b)
) {
if (
!(ObjectPrototypeIsPrototypeOf(WeakSetPrototype, a) &&
ObjectPrototypeIsPrototypeOf(WeakSetPrototype, b))
) return false;
throw new TypeError("cannot compare WeakSet instances"); throw new TypeError("cannot compare WeakSet instances");
} }
if (seen.get(a) === b) { if (seen.get(a) === b) {
return true; return true;
} }
if (Object.keys(a || {}).length !== Object.keys(b || {}).length) { if (ObjectKeys(a || {}).length !== ObjectKeys(b || {}).length) {
return false; return false;
} }
seen.set(a, b); seen.set(a, b);
@ -98,7 +145,10 @@ export function equal(c: unknown, d: unknown): boolean {
let unmatchedEntries = a.size; let unmatchedEntries = a.size;
// TODO(petamoriken): use primordials
// deno-lint-ignore prefer-primordials
for (const [aKey, aValue] of a.entries()) { for (const [aKey, aValue] of a.entries()) {
// deno-lint-ignore prefer-primordials
for (const [bKey, bValue] of b.entries()) { for (const [bKey, bValue] of b.entries()) {
/* Given that Map keys can be references, we need /* Given that Map keys can be references, we need
* to ensure that they are also deeply equal */ * to ensure that they are also deeply equal */
@ -111,27 +161,34 @@ export function equal(c: unknown, d: unknown): boolean {
} }
} }
} }
return unmatchedEntries === 0; return unmatchedEntries === 0;
} }
const merged = { ...a, ...b }; const merged = { ...a, ...b };
for ( const keys = ReflectOwnKeys(merged);
const key of [ for (let i = 0; i < keys.length; ++i) {
...Object.getOwnPropertyNames(merged), const key = keys[i];
...Object.getOwnPropertySymbols(merged),
]
) {
type Key = keyof typeof merged; type Key = keyof typeof merged;
if (!compare(a && a[key as Key], b && b[key as Key])) { if (!compare(a && a[key as Key], b && b[key as Key])) {
return false; 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; return false;
} }
} }
if (a instanceof WeakRef || b instanceof WeakRef) {
if (!(a instanceof WeakRef && b instanceof WeakRef)) return false; if (
return compare(a.deref(), b.deref()); ObjectPrototypeIsPrototypeOf(WeakRefPrototype, a) ||
ObjectPrototypeIsPrototypeOf(WeakRefPrototype, b)
) {
if (
!(ObjectPrototypeIsPrototypeOf(WeakRefPrototype, a) &&
ObjectPrototypeIsPrototypeOf(WeakRefPrototype, b))
) return false;
return compare(WeakRefPrototypeDeref(a), WeakRefPrototypeDeref(b));
} }
return true; return true;
} }
@ -166,8 +223,14 @@ export function assertEquals<T>(actual: T, expected: T, msg?: string) {
(typeof expected === "string"); (typeof expected === "string");
const diffResult = stringDiff const diffResult = stringDiff
? diffstr(actual as string, expected as string) ? diffstr(actual as string, expected as string)
: diff(actualString.split("\n"), expectedString.split("\n")); : diff(
const diffMsg = buildMessage(diffResult, { stringDiff }).join("\n"); StringPrototypeSplit(actualString, "\n"),
StringPrototypeSplit(expectedString, "\n"),
);
const diffMsg = ArrayPrototypeJoin(
buildMessage(diffResult, { stringDiff }),
"\n",
);
message = `Values are not equal:\n${diffMsg}`; message = `Values are not equal:\n${diffMsg}`;
} catch { } catch {
message = `\n${red(red(CAN_NOT_DISPLAY))} + \n\n`; message = `\n${red(red(CAN_NOT_DISPLAY))} + \n\n`;
@ -209,7 +272,7 @@ export function assertStrictEquals<T>(
expected: T, expected: T,
msg?: string, msg?: string,
): asserts actual is T { ): asserts actual is T {
if (Object.is(actual, expected)) { if (ObjectIs(actual, expected)) {
return; return;
} }
@ -222,10 +285,13 @@ export function assertStrictEquals<T>(
const expectedString = format(expected); const expectedString = format(expected);
if (actualString === expectedString) { if (actualString === expectedString) {
const withOffset = actualString const withOffset = ArrayPrototypeJoin(
.split("\n") ArrayPrototypeMap(
.map((l) => ` ${l}`) StringPrototypeSplit(actualString, "\n"),
.join("\n"); (l: string) => ` ${l}`,
),
"\n",
);
message = message =
`Values have the same structure but are not reference-equal:\n\n${ `Values have the same structure but are not reference-equal:\n\n${
red(withOffset) red(withOffset)
@ -236,8 +302,14 @@ export function assertStrictEquals<T>(
(typeof expected === "string"); (typeof expected === "string");
const diffResult = stringDiff const diffResult = stringDiff
? diffstr(actual as string, expected as string) ? diffstr(actual as string, expected as string)
: diff(actualString.split("\n"), expectedString.split("\n")); : diff(
const diffMsg = buildMessage(diffResult, { stringDiff }).join("\n"); StringPrototypeSplit(actualString, "\n"),
StringPrototypeSplit(expectedString, "\n"),
);
const diffMsg = ArrayPrototypeJoin(
buildMessage(diffResult, { stringDiff }),
"\n",
);
message = `Values are not strictly equal:\n${diffMsg}`; message = `Values are not strictly equal:\n${diffMsg}`;
} catch { } catch {
message = `\n${CAN_NOT_DISPLAY} + \n\n`; message = `\n${CAN_NOT_DISPLAY} + \n\n`;
@ -255,7 +327,7 @@ export function assertNotStrictEquals<T>(
expected: T, expected: T,
msg?: string, msg?: string,
) { ) {
if (!Object.is(actual, expected)) { if (!ObjectIs(actual, expected)) {
return; return;
} }
@ -268,10 +340,11 @@ export function assertNotStrictEquals<T>(
* then throw. */ * then throw. */
export function assertMatch( export function assertMatch(
actual: string, actual: string,
// deno-lint-ignore prefer-primordials
expected: RegExp, expected: RegExp,
msg?: string, msg?: string,
) { ) {
if (!expected.test(actual)) { if (!RegExpPrototypeTest(expected, actual)) {
if (!msg) { if (!msg) {
msg = `actual: "${actual}" expected to match: "${expected}"`; msg = `actual: "${actual}" expected to match: "${expected}"`;
} }
@ -283,10 +356,11 @@ export function assertMatch(
* then throw. */ * then throw. */
export function assertNotMatch( export function assertNotMatch(
actual: string, actual: string,
// deno-lint-ignore prefer-primordials
expected: RegExp, expected: RegExp,
msg?: string, msg?: string,
) { ) {
if (expected.test(actual)) { if (RegExpPrototypeTest(expected, actual)) {
if (!msg) { if (!msg) {
msg = `actual: "${actual}" expected to not match: "${expected}"`; 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. // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// This file is vendored from std/fmt/colors.ts // This file is vendored from std/fmt/colors.ts
// TODO(petamoriken): enable prefer-primordials for node polyfills import { primordials } from "ext:core/mod.js";
// deno-lint-ignore-file prefer-primordials const {
ArrayPrototypeJoin,
MathMax,
MathMin,
MathTrunc,
SafeRegExp,
StringPrototypeReplace,
} = primordials;
// TODO(kt3k): Initialize this at the start of runtime // TODO(kt3k): Initialize this at the start of runtime
// based on Deno.noColor // based on Deno.noColor
@ -11,6 +18,7 @@ const noColor = false;
interface Code { interface Code {
open: string; open: string;
close: string; close: string;
// deno-lint-ignore prefer-primordials
regexp: RegExp; regexp: RegExp;
} }
@ -47,9 +55,9 @@ export function getColorEnabled(): boolean {
*/ */
function code(open: number[], close: number): Code { function code(open: number[], close: number): Code {
return { return {
open: `\x1b[${open.join(";")}m`, open: `\x1b[${ArrayPrototypeJoin(open, ";")}m`,
close: `\x1b[${close}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 { function run(str: string, code: Code): string {
return enabled return enabled
? `${code.open}${str.replace(code.regexp, code.open)}${code.close}` ? `${code.open}${
StringPrototypeReplace(str, code.regexp, code.open)
}${code.close}`
: str; : str;
} }
@ -401,7 +411,7 @@ export function bgBrightWhite(str: string): string {
* @param min number to truncate from * @param min number to truncate from
*/ */
function clampAndTruncate(n: number, max = 255, min = 0): number { 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 // 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)", "[\\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=><~]))", "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))",
].join("|"), ], "|"),
"g", "g",
); );
@ -518,5 +528,5 @@ const ANSI_PATTERN = new RegExp(
* @param string to remove ANSI escape codes from * @param string to remove ANSI escape codes from
*/ */
export function stripColor(string: string): string { 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. // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// This file was vendored from std/testing/_diff.ts // This file was vendored from std/testing/_diff.ts
// TODO(petamoriken): enable prefer-primordials for node polyfills import { primordials } from "ext:core/mod.js";
// deno-lint-ignore-file prefer-primordials
import { import {
bgGreen, bgGreen,
bgRed, bgRed,
@ -13,6 +11,30 @@ import {
red, red,
white, white,
} from "ext:deno_node/_util/std_fmt_colors.ts"; } 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 { interface FarthestPoint {
y: number; y: number;
@ -28,7 +50,7 @@ export enum DiffType {
export interface DiffResult<T> { export interface DiffResult<T> {
type: DiffType; type: DiffType;
value: T; value: T;
details?: Array<DiffResult<T>>; details?: DiffResult<T>[];
} }
const REMOVED = 1; const REMOVED = 1;
@ -38,11 +60,11 @@ const ADDED = 3;
function createCommon<T>(A: T[], B: T[], reverse?: boolean): T[] { function createCommon<T>(A: T[], B: T[], reverse?: boolean): T[] {
const common = []; const common = [];
if (A.length === 0 || B.length === 0) return []; 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 ( if (
A[reverse ? A.length - i - 1 : i] === B[reverse ? B.length - i - 1 : i] 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 { } else {
return common; return common;
} }
@ -55,44 +77,56 @@ function createCommon<T>(A: T[], B: T[], reverse?: boolean): T[] {
* @param A Actual value * @param A Actual value
* @param B Expected 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 prefixCommon = createCommon(A, B);
const suffixCommon = createCommon( const suffixCommon = ArrayPrototypeReverse(createCommon(
A.slice(prefixCommon.length), ArrayPrototypeSlice(A, prefixCommon.length),
B.slice(prefixCommon.length), ArrayPrototypeSlice(B, prefixCommon.length),
true, true,
).reverse(); ));
A = suffixCommon.length A = suffixCommon.length
? A.slice(prefixCommon.length, -suffixCommon.length) ? ArrayPrototypeSlice(A, prefixCommon.length, -suffixCommon.length)
: A.slice(prefixCommon.length); : ArrayPrototypeSlice(A, prefixCommon.length);
B = suffixCommon.length B = suffixCommon.length
? B.slice(prefixCommon.length, -suffixCommon.length) ? ArrayPrototypeSlice(B, prefixCommon.length, -suffixCommon.length)
: B.slice(prefixCommon.length); : ArrayPrototypeSlice(B, prefixCommon.length);
const swapped = B.length > A.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 M = A.length;
const N = B.length; const N = B.length;
if (!M && !N && !suffixCommon.length && !prefixCommon.length) return []; if (
if (!N) { M === 0 && N === 0 && suffixCommon.length === 0 && prefixCommon.length === 0
) return [];
if (N === 0) {
return [ return [
...prefixCommon.map( ...new SafeArrayIterator(
(c): DiffResult<typeof c> => ({ type: DiffType.common, value: c }), ArrayPrototypeMap(
prefixCommon,
(c: T): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
),
), ),
...A.map( ...new SafeArrayIterator(
(a): DiffResult<typeof a> => ({ ArrayPrototypeMap(A, (a: T): DiffResult<typeof a> => ({
type: swapped ? DiffType.added : DiffType.removed, type: swapped ? DiffType.added : DiffType.removed,
value: a, value: a,
}), })),
), ),
...suffixCommon.map( ...new SafeArrayIterator(
(c): DiffResult<typeof c> => ({ type: DiffType.common, value: c }), ArrayPrototypeMap(
suffixCommon,
(c: T): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
),
), ),
]; ];
} }
const offset = N; const offset = N;
const delta = M - N; const delta = M - N;
const size = M + N + 1; const size = M + N + 1;
const fp: FarthestPoint[] = Array.from( const fp: FarthestPoint[] = ArrayFrom(
{ length: size }, { length: size },
() => ({ y: -1, id: -1 }), () => ({ y: -1, id: -1 }),
); );
@ -114,13 +148,13 @@ export function diff<T>(A: T[], B: T[]): Array<DiffResult<T>> {
B: T[], B: T[],
current: FarthestPoint, current: FarthestPoint,
swapped: boolean, swapped: boolean,
): Array<{ ): {
type: DiffType; type: DiffType;
value: T; value: T;
}> { }[] {
const M = A.length; const M = A.length;
const N = B.length; const N = B.length;
const result = []; const result: DiffResult<T>[] = [];
let a = M - 1; let a = M - 1;
let b = N - 1; let b = N - 1;
let j = routes[current.id]; let j = routes[current.id];
@ -129,19 +163,19 @@ export function diff<T>(A: T[], B: T[]): Array<DiffResult<T>> {
if (!j && !type) break; if (!j && !type) break;
const prev = j; const prev = j;
if (type === REMOVED) { if (type === REMOVED) {
result.unshift({ ArrayPrototypeUnshift(result, {
type: swapped ? DiffType.removed : DiffType.added, type: swapped ? DiffType.removed : DiffType.added,
value: B[b], value: B[b],
}); });
b -= 1; b -= 1;
} else if (type === ADDED) { } else if (type === ADDED) {
result.unshift({ ArrayPrototypeUnshift(result, {
type: swapped ? DiffType.added : DiffType.removed, type: swapped ? DiffType.added : DiffType.removed,
value: A[a], value: A[a],
}); });
a -= 1; a -= 1;
} else { } else {
result.unshift({ type: DiffType.common, value: A[a] }); ArrayPrototypeUnshift(result, { type: DiffType.common, value: A[a] });
a -= 1; a -= 1;
b -= 1; b -= 1;
} }
@ -234,16 +268,40 @@ export function diff<T>(A: T[], B: T[]): Array<DiffResult<T>> {
); );
} }
return [ return [
...prefixCommon.map( ...new SafeArrayIterator(
(c): DiffResult<typeof c> => ({ type: DiffType.common, value: c }), ArrayPrototypeMap(
prefixCommon,
(c: T): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
),
), ),
...backTrace(A, B, fp[delta + offset], swapped), ...new SafeArrayIterator(backTrace(A, B, fp[delta + offset], swapped)),
...suffixCommon.map( ...new SafeArrayIterator(
(c): DiffResult<typeof c> => ({ type: DiffType.common, value: c }), 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 * Renders the differences between the actual and expected strings
* Partially inspired from https://github.com/kpdecker/jsdiff * Partially inspired from https://github.com/kpdecker/jsdiff
@ -254,44 +312,44 @@ export function diffstr(A: string, B: string) {
function unescape(string: string): string { function unescape(string: string): string {
// unescape invisible characters. // unescape invisible characters.
// ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#escape_sequences // ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#escape_sequences
return string return StringPrototypeReplace(
.replaceAll("\b", "\\b") StringPrototypeReplace(
.replaceAll("\f", "\\f") string,
.replaceAll("\t", "\\t") ESCAPE_PATTERN,
.replaceAll("\v", "\\v") (c: string) => ESCAPE_MAP[c],
.replaceAll( // does not remove line breaks ),
/\r\n|\r|\n/g, LINE_BREAK_GLOBAL_PATTERN, // does not remove line breaks
(str) => str === "\r" ? "\\r" : str === "\n" ? "\\n\n" : "\\r\\n\r\n", (str: string) =>
); str === "\r" ? "\\r" : str === "\n" ? "\\n\n" : "\\r\\n\r\n",
);
} }
function tokenize(string: string, { wordDiff = false } = {}): string[] { function tokenize(string: string, { wordDiff = false } = {}): string[] {
if (wordDiff) { if (wordDiff) {
// Split string on whitespace symbols // Split string on whitespace symbols
const tokens = string.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/); const tokens = StringPrototypeSplit(string, WHITESPACE_SYMBOL_PATTERN);
// 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;
// Join boundary splits that we do not consider to be boundaries and merge empty strings surrounded by word chars // 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++) { for (let i = 0; i < tokens.length - 1; i++) {
if ( if (
!tokens[i + 1] && tokens[i + 2] && words.test(tokens[i]) && !tokens[i + 1] && tokens[i + 2] &&
words.test(tokens[i + 2]) LATIN_CHARACTER_PATTERN.test(tokens[i]) &&
LATIN_CHARACTER_PATTERN.test(tokens[i + 2])
) { ) {
tokens[i] += tokens[i + 2]; tokens[i] += tokens[i + 2];
tokens.splice(i + 1, 2); ArrayPrototypeSplice(tokens, i + 1, 2);
i--; i--;
} }
} }
return tokens.filter((token) => token); return ArrayPrototypeFilter(tokens, (token: string) => token);
} else { } else {
// Split string on new lines symbols // 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 // Ignore final empty token when text ends with a newline
if (!lines[lines.length - 1]) { if (lines[lines.length - 1] === "") {
lines.pop(); ArrayPrototypePop(lines);
} }
// Merge the content and line separators into single tokens // Merge the content and line separators into single tokens
@ -299,7 +357,7 @@ export function diffstr(A: string, B: string) {
if (i % 2) { if (i % 2) {
tokens[tokens.length - 1] += lines[i]; tokens[tokens.length - 1] += lines[i];
} else { } else {
tokens.push(lines[i]); ArrayPrototypePush(tokens, lines[i]);
} }
} }
return tokens; 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 // and merge "space-diff" if surrounded by word-diff for cleaner displays
function createDetails( function createDetails(
line: DiffResult<string>, line: DiffResult<string>,
tokens: Array<DiffResult<string>>, tokens: DiffResult<string>[],
) { ) {
return tokens.filter(({ type }) => return ArrayPrototypeMap(
type === line.type || type === DiffType.common ArrayPrototypeFilter(
).map((result, i, t) => { tokens,
if ( ({ type }: DiffResult<string>) =>
(result.type === DiffType.common) && (t[i - 1]) && type === line.type || type === DiffType.common,
(t[i - 1]?.type === t[i + 1]?.type) && /\s+/.test(result.value) ),
) { (result: DiffResult<string>, i: number, t: DiffResult<string>[]) => {
return { if (
...result, (result.type === DiffType.common) && (t[i - 1]) &&
type: t[i - 1].type, (t[i - 1]?.type === t[i + 1]?.type) &&
}; WHITESPACE_PATTERN.test(result.value)
} ) {
return result; return {
}); ...result,
type: t[i - 1].type,
};
}
return result;
},
);
} }
// Compute multi-line diff // Compute multi-line diff
@ -334,32 +398,36 @@ export function diffstr(A: string, B: string) {
tokenize(`${unescape(B)}\n`), tokenize(`${unescape(B)}\n`),
); );
const added = [], removed = []; const added: DiffResult<string>[] = [], removed: DiffResult<string>[] = [];
for (const result of diffResult) { for (let i = 0; i < diffResult.length; ++i) {
const result = diffResult[i];
if (result.type === DiffType.added) { if (result.type === DiffType.added) {
added.push(result); ArrayPrototypePush(added, result);
} }
if (result.type === DiffType.removed) { if (result.type === DiffType.removed) {
removed.push(result); ArrayPrototypePush(removed, result);
} }
} }
// Compute word-diff // Compute word-diff
const aLines = added.length < removed.length ? added : removed; const aLines = added.length < removed.length ? added : removed;
const bLines = aLines === removed ? added : removed; const bLines = aLines === removed ? added : removed;
for (const a of aLines) { for (let i = 0; i < aLines.length; ++i) {
let tokens = [] as Array<DiffResult<string>>, const a = aLines[i];
let tokens = [] as DiffResult<string>[],
b: undefined | DiffResult<string>; b: undefined | DiffResult<string>;
// Search another diff line with at least one common token // Search another diff line with at least one common token
while (bLines.length) { while (bLines.length !== 0) {
b = bLines.shift(); b = ArrayPrototypeShift(bLines);
tokens = diff( tokens = diff(
tokenize(a.value, { wordDiff: true }), tokenize(a.value, { wordDiff: true }),
tokenize(b?.value ?? "", { wordDiff: true }), tokenize(b?.value ?? "", { wordDiff: true }),
); );
if ( if (
tokens.some(({ type, value }) => ArrayPrototypeSome(
type === DiffType.common && value.trim().length tokens,
({ type, value }) =>
type === DiffType.common && StringPrototypeTrim(value).length,
) )
) { ) {
break; break;
@ -418,26 +486,35 @@ export function buildMessage(
{ stringDiff = false } = {}, { stringDiff = false } = {},
): string[] { ): string[] {
const messages: string[] = [], diffMessages: string[] = []; const messages: string[] = [], diffMessages: string[] = [];
messages.push(""); ArrayPrototypePush(messages, "");
messages.push(""); ArrayPrototypePush(messages, "");
messages.push( ArrayPrototypePush(
messages,
` ${gray(bold("[Diff]"))} ${red(bold("Actual"))} / ${ ` ${gray(bold("[Diff]"))} ${red(bold("Actual"))} / ${
green(bold("Expected")) green(bold("Expected"))
}`, }`,
); );
messages.push(""); ArrayPrototypePush(messages, "");
messages.push(""); ArrayPrototypePush(messages, "");
diffResult.forEach((result: DiffResult<string>) => { ArrayPrototypeForEach(diffResult, (result: DiffResult<string>) => {
const c = createColor(result.type); const c = createColor(result.type);
const line = result.details?.map((detail) =>
detail.type !== DiffType.common const line = result.details != null
? createColor(detail.type, { background: true })(detail.value) ? ArrayPrototypeJoin(
: detail.value ArrayPrototypeMap(result.details, (detail) =>
).join("") ?? result.value; detail.type !== DiffType.common
diffMessages.push(c(`${createSign(result.type)}${line}`)); ? createColor(detail.type, { background: true })(detail.value)
: detail.value),
"",
)
: result.value;
ArrayPrototypePush(diffMessages, c(`${createSign(result.type)}${line}`));
}); });
messages.push(...(stringDiff ? [diffMessages.join("")] : diffMessages)); ArrayPrototypePushApply(
messages.push(""); messages,
stringDiff ? [ArrayPrototypeJoin(diffMessages, "")] : diffMessages,
);
ArrayPrototypePush(messages, "");
return messages; return messages;
} }