diff --git a/ext/node/polyfills/_util/_util_callbackify.ts b/ext/node/polyfills/_util/_util_callbackify.ts index c60122b56c..4962ead8f3 100644 --- a/ext/node/polyfills/_util/_util_callbackify.ts +++ b/ext/node/polyfills/_util/_util_callbackify.ts @@ -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( } 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( if (typeof descriptors.name.value === "string") { descriptors.name.value += "Callbackified"; } - Object.defineProperties(callbackified, descriptors); + ObjectDefineProperties(callbackified, descriptors); return callbackified; } diff --git a/ext/node/polyfills/_util/asserts.ts b/ext/node/polyfills/_util/asserts.ts index ad85ecbf2e..7fb56049c0 100644 --- a/ext/node/polyfills/_util/asserts.ts +++ b/ext/node/polyfills/_util/asserts.ts @@ -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 { diff --git a/ext/node/polyfills/_util/async.ts b/ext/node/polyfills/_util/async.ts index 5eeda2a377..d0e6e04e88 100644 --- a/ext/node/polyfills/_util/async.ts +++ b/ext/node/polyfills/_util/async.ts @@ -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 { 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); diff --git a/ext/node/polyfills/_util/os.ts b/ext/node/polyfills/_util/os.ts index ff5e2efd54..49fde7613c 100644 --- a/ext/node/polyfills/_util/os.ts +++ b/ext/node/polyfills/_util/os.ts @@ -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"; diff --git a/ext/node/polyfills/_util/std_asserts.ts b/ext/node/polyfills/_util/std_asserts.ts index 4258a74957..061b0515d6 100644 --- a/ext/node/polyfills/_util/std_asserts.ts +++ b/ext/node/polyfills/_util/std_asserts.ts @@ -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 { - return [Symbol.iterator, "size"].every((k) => k in (x as Set)); +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(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( 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( 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( (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( expected: T, msg?: string, ) { - if (!Object.is(actual, expected)) { + if (!ObjectIs(actual, expected)) { return; } @@ -268,10 +340,11 @@ export function assertNotStrictEquals( * 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}"`; } diff --git a/ext/node/polyfills/_util/std_fmt_colors.ts b/ext/node/polyfills/_util/std_fmt_colors.ts index 5632c05c1a..df18526dab 100644 --- a/ext/node/polyfills/_util/std_fmt_colors.ts +++ b/ext/node/polyfills/_util/std_fmt_colors.ts @@ -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, ""); } diff --git a/ext/node/polyfills/_util/std_testing_diff.ts b/ext/node/polyfills/_util/std_testing_diff.ts index bcd6192554..635ce1f291 100644 --- a/ext/node/polyfills/_util/std_testing_diff.ts +++ b/ext/node/polyfills/_util/std_testing_diff.ts @@ -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 { type: DiffType; value: T; - details?: Array>; + details?: DiffResult[]; } const REMOVED = 1; @@ -38,11 +60,11 @@ const ADDED = 3; function createCommon(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(A: T[], B: T[], reverse?: boolean): T[] { * @param A Actual value * @param B Expected value */ -export function diff(A: T[], B: T[]): Array> { +export function diff(A: T[], B: T[]): DiffResult[] { 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 => ({ type: DiffType.common, value: c }), + ...new SafeArrayIterator( + ArrayPrototypeMap( + prefixCommon, + (c: T): DiffResult => ({ type: DiffType.common, value: c }), + ), ), - ...A.map( - (a): DiffResult => ({ + ...new SafeArrayIterator( + ArrayPrototypeMap(A, (a: T): DiffResult => ({ type: swapped ? DiffType.added : DiffType.removed, value: a, - }), + })), ), - ...suffixCommon.map( - (c): DiffResult => ({ type: DiffType.common, value: c }), + ...new SafeArrayIterator( + ArrayPrototypeMap( + suffixCommon, + (c: T): DiffResult => ({ 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(A: T[], B: T[]): Array> { B: T[], current: FarthestPoint, swapped: boolean, - ): Array<{ + ): { type: DiffType; value: T; - }> { + }[] { const M = A.length; const N = B.length; - const result = []; + const result: DiffResult[] = []; let a = M - 1; let b = N - 1; let j = routes[current.id]; @@ -129,19 +163,19 @@ export function diff(A: T[], B: T[]): Array> { 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(A: T[], B: T[]): Array> { ); } return [ - ...prefixCommon.map( - (c): DiffResult => ({ type: DiffType.common, value: c }), + ...new SafeArrayIterator( + ArrayPrototypeMap( + prefixCommon, + (c: T): DiffResult => ({ type: DiffType.common, value: c }), + ), ), - ...backTrace(A, B, fp[delta + offset], swapped), - ...suffixCommon.map( - (c): DiffResult => ({ type: DiffType.common, value: c }), + ...new SafeArrayIterator(backTrace(A, B, fp[delta + offset], swapped)), + ...new SafeArrayIterator( + ArrayPrototypeMap( + suffixCommon, + (c: T): DiffResult => ({ 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, - tokens: Array>, + tokens: DiffResult[], ) { - 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) => + type === line.type || type === DiffType.common, + ), + (result: DiffResult, i: number, t: DiffResult[]) => { + 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[] = [], removed: DiffResult[] = []; + 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>, + for (let i = 0; i < aLines.length; ++i) { + const a = aLines[i]; + let tokens = [] as DiffResult[], b: undefined | DiffResult; // 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) => { + ArrayPrototypePush(messages, ""); + ArrayPrototypePush(messages, ""); + ArrayPrototypeForEach(diffResult, (result: DiffResult) => { 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; }