Skip to content

assert: refactor to use more primordials #38995

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 30 additions & 21 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,18 @@ const {
ObjectKeys,
ObjectPrototypeIsPrototypeOf,
ReflectApply,
RegExpPrototypeTest,
RegExpPrototypeExec,
SafeMap,
String,
StringPrototypeCharCodeAt,
StringPrototypeIncludes,
StringPrototypeIndexOf,
StringPrototypeReplace,
StringPrototypeSlice,
StringPrototypeSplit,
StringPrototypeStartsWith,
SafeRegExpPrototypeSymbolReplace,
SafeRegExpPrototypeTest,
makeSafeStringObject,
} = primordials;

const { Buffer } = require('buffer');
Expand Down Expand Up @@ -283,9 +285,11 @@ function parseCode(code, offset) {

return [
node.node.start,
StringPrototypeReplace(StringPrototypeSlice(code,
node.node.start, node.node.end),
escapeSequencesRegExp, escapeFn),
SafeRegExpPrototypeSymbolReplace(
escapeSequencesRegExp,
StringPrototypeSlice(code, node.node.start, node.node.end),
escapeFn
),
];
}

Expand Down Expand Up @@ -357,17 +361,21 @@ function getErrMessage(message, fn) {
}
// Always normalize indentation, otherwise the message could look weird.
if (StringPrototypeIncludes(message, '\n')) {
if (EOL === '\r\n') {
message = StringPrototypeReplace(message, /\r\n/g, '\n');
}
const frames = StringPrototypeSplit(message, '\n');
message = ArrayPrototypeShift(frames);
for (const frame of frames) {
let pos = 0;
while (pos < column && (frame[pos] === ' ' || frame[pos] === '\t')) {
const splitter = EOL === '\r\n' ? /\r\n/g : /\n/g;
const splitterLength = EOL === '\r\n' ? 2 : 1;
const str = message;
let previousIndex = RegExpPrototypeExec(splitter, str).index;
message = StringPrototypeSlice(message, 0, previousIndex);
while (previousIndex != null) {
const lineEndIndex = RegExpPrototypeExec(splitter, str)?.index;
previousIndex += splitterLength;
let pos = previousIndex;
while (pos < previousIndex + column &&
(str[pos] === ' ' || str[pos] === '\t')) {
pos++;
}
message += `\n ${StringPrototypeSlice(frame, pos)}`;
message += `\n ${StringPrototypeSlice(str, pos, lineEndIndex)}`;
previousIndex = lineEndIndex;
}
}
message = `The expression evaluated to a falsy value:\n\n ${message}\n`;
Expand Down Expand Up @@ -619,7 +627,7 @@ class Comparison {
if (actual !== undefined &&
typeof actual[key] === 'string' &&
isRegExp(obj[key]) &&
RegExpPrototypeTest(obj[key], actual[key])) {
SafeRegExpPrototypeTest(obj[key], actual[key])) {
this[key] = actual[key];
} else {
this[key] = obj[key];
Expand Down Expand Up @@ -665,7 +673,7 @@ function expectedException(actual, expected, message, fn) {
// Handle regular expressions.
if (isRegExp(expected)) {
const str = String(actual);
if (RegExpPrototypeTest(expected, str))
if (SafeRegExpPrototypeTest(expected, str))
return;

if (!message) {
Expand Down Expand Up @@ -700,7 +708,7 @@ function expectedException(actual, expected, message, fn) {
for (const key of keys) {
if (typeof actual[key] === 'string' &&
isRegExp(expected[key]) &&
RegExpPrototypeTest(expected[key], actual[key])) {
SafeRegExpPrototypeTest(expected[key], actual[key])) {
continue;
}
compareExceptionKey(actual, expected, key, message, keys, fn);
Expand Down Expand Up @@ -864,7 +872,7 @@ function hasMatchingError(actual, expected) {
if (typeof expected !== 'function') {
if (isRegExp(expected)) {
const str = String(actual);
return RegExpPrototypeTest(expected, str);
return SafeRegExpPrototypeTest(expected, str);
}
throw new ERR_INVALID_ARG_TYPE(
'expected', ['Function', 'RegExp'], expected
Expand Down Expand Up @@ -978,10 +986,11 @@ assert.ifError = function ifError(err) {
// This will remove any duplicated frames from the error frames taken
// from within `ifError` and add the original error frames to the newly
// created ones.
const tmp2 = StringPrototypeSplit(origStack, '\n');
const LINE_RETURN = makeSafeStringObject('\n');
const tmp2 = StringPrototypeSplit(origStack, LINE_RETURN);
ArrayPrototypeShift(tmp2);
// Filter all frames existing in err.stack.
let tmp1 = StringPrototypeSplit(newErr.stack, '\n');
let tmp1 = StringPrototypeSplit(newErr.stack, LINE_RETURN);
for (const errFrame of tmp2) {
// Find the first occurrence of the frame.
const pos = ArrayPrototypeIndexOf(tmp1, errFrame);
Expand All @@ -1007,7 +1016,7 @@ function internalMatch(string, regexp, message, fn) {
}
const match = fn.name === 'match';
if (typeof string !== 'string' ||
RegExpPrototypeTest(regexp, string) !== match) {
SafeRegExpPrototypeTest(regexp, string) !== match) {
if (message instanceof Error) {
throw message;
}
Expand Down
14 changes: 9 additions & 5 deletions lib/internal/assert/assertion_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const {
StringPrototypeRepeat,
StringPrototypeSlice,
StringPrototypeSplit,
makeSafeStringObject,
} = primordials;

const { inspect } = require('internal/util/inspect');
Expand Down Expand Up @@ -80,14 +81,16 @@ function inspectValue(val) {
);
}

const SAFE_LINE_RETURN = makeSafeStringObject('\n');
function createErrDiff(actual, expected, operator) {
let other = '';
let res = '';
let end = '';
let skipped = false;
const actualInspected = inspectValue(actual);
const actualLines = StringPrototypeSplit(actualInspected, '\n');
const expectedLines = StringPrototypeSplit(inspectValue(expected), '\n');
const actualLines = StringPrototypeSplit(actualInspected, SAFE_LINE_RETURN);
const expectedLines = StringPrototypeSplit(inspectValue(expected),
SAFE_LINE_RETURN);

let i = 0;
let indicator = '';
Expand Down Expand Up @@ -164,7 +167,7 @@ function createErrDiff(actual, expected, operator) {
// E.g., assert.deepStrictEqual({ a: Symbol() }, { a: Symbol() })
if (maxLines === 0) {
// We have to get the result again. The lines were all removed before.
const actualLines = StringPrototypeSplit(actualInspected, '\n');
const actualLines = StringPrototypeSplit(actualInspected, SAFE_LINE_RETURN);

// Only remove lines in case it makes sense to collapse those.
// TODO: Accept env to always show the full error.
Expand Down Expand Up @@ -315,7 +318,7 @@ function createErrDiff(actual, expected, operator) {
}

function addEllipsis(string) {
const lines = StringPrototypeSplit(string, '\n', 11);
const lines = StringPrototypeSplit(string, SAFE_LINE_RETURN, 11);
if (lines.length > 10) {
lines.length = 10;
return `${ArrayPrototypeJoin(lines, '\n')}\n...`;
Expand Down Expand Up @@ -380,7 +383,8 @@ class AssertionError extends Error {
// In case the objects are equal but the operator requires unequal, show
// the first object and say A equals B
let base = kReadableOperator[operator];
const res = StringPrototypeSplit(inspectValue(actual), '\n');
const res = StringPrototypeSplit(inspectValue(actual),
SAFE_LINE_RETURN);

// In case "actual" is an object or a function, it should not be
// reference equal.
Expand Down
190 changes: 190 additions & 0 deletions lib/internal/per_context/primordials.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ function copyPrototype(src, dest, prefix) {
copyPrototype(original.prototype, primordials, `${name}Prototype`);
});

const { String } = primordials;

// Create copies of abstract intrinsic objects that are not directly exposed
// on the global object.
// Refs: https://tc39.es/ecma262/#sec-%typedarray%-intrinsic-object
Expand All @@ -248,20 +250,208 @@ function copyPrototype(src, dest, prefix) {

const {
ArrayPrototypeForEach,
Error,
FinalizationRegistry,
FunctionPrototypeCall,
Map,
MathMax,
MathMin,
ObjectCreate,
ObjectDefineProperties,
ObjectFreeze,
ObjectSetPrototypeOf,
Promise,
PromisePrototypeThen,
RegExp,
RegExpPrototype,
RegExpPrototypeExec,
RegExpPrototypeGetUnicode,
RegExpPrototypeGetSticky,
RegExpPrototypeSymbolMatch,
RegExpPrototypeSymbolReplace,
RegExpPrototypeSymbolSearch,
Set,
StringPrototypeReplace,
StringPrototypeReplaceAll,
StringPrototypeSlice,
StringPrototypeSplit,
SymbolIterator,
SymbolMatch,
SymbolMatchAll,
SymbolReplace,
SymbolSearch,
SymbolSplit,
WeakMap,
WeakRef,
WeakSet,
} = primordials;

{
const undefinedDesc = ObjectCreate(null);
undefinedDesc.value = undefined;
const safeTextTransformProperties = {
[SymbolMatch]: undefinedDesc,
[SymbolMatchAll]: undefinedDesc,
[SymbolReplace]: undefinedDesc,
[SymbolSearch]: undefinedDesc,
[SymbolSplit]: undefinedDesc,
exec: undefinedDesc,
};
const defineRegExpGetter = (name) => {
safeTextTransformProperties[name] =
ReflectGetOwnPropertyDescriptor(RegExpPrototype, name);
};
defineRegExpGetter('flags');
defineRegExpGetter('global');
defineRegExpGetter('ignoreCase');
defineRegExpGetter('multiline');
defineRegExpGetter('dotAll');
defineRegExpGetter('unicode');
defineRegExpGetter('sticky');
ObjectFreeze(safeTextTransformProperties);

const toSafeRegex = (stringOrRegex) =>
ObjectDefineProperties(stringOrRegex, safeTextTransformProperties);
const toSafeString = (string) => toSafeRegex(new String(string));
primordials.makeSafeStringObject = toSafeString;

/**
* @param {string} thisString
* @param {string} needle
* @returns {string[]}
*/
primordials.SafeStringPrototypeMatch = (thisString, needle) =>
RegExpPrototypeSymbolMatch(toSafeRegex(new RegExp(needle)), thisString);

/**
* @param {string} thisStr
* @param {string} needle
* @returns {string[]}
*/
primordials.SafeStringPrototypeMatchAll = (thisStr, needle) =>
RegExpPrototypeSymbolMatch(toSafeRegex(new RegExp(needle, 'g')), thisStr);

/**
* @param {string} thisStr
* @param {string} searchVal
* @param {string | (substring: string, ...args: any[]) => string} replaceVal
* @returns {string}
*/
primordials.SafeStringPrototypeReplace = (thisStr, searchVal, replaceVal) =>
StringPrototypeReplace(thisStr, toSafeString(searchVal), replaceVal);

/**
* @param {string} thisStr
* @param {string} searchVal
* @param {string | (substring: string, ...args: any[]) => string} replaceVal
* @returns {string}
*/
primordials.SafeStringPrototypeReplaceAll =
(thisStr, searchVal, replaceVal) =>
StringPrototypeReplaceAll(thisStr, toSafeString(searchVal), replaceVal);

/**
* @param {string} thisString
* @param {string} needle
* @returns {number}
*/
primordials.SafeStringPrototypeSearch = (thisString, needle) =>
RegExpPrototypeSymbolSearch(toSafeRegex(new RegExp(needle)), thisString);

/**
* @param {string} thisString
* @param {string} splitter
* @param {number} [limit]
* @returns {string}
*/
primordials.SafeStringPrototypeSplit = (thisString, splitter, limit) =>
StringPrototypeSplit(thisString, toSafeString(splitter), limit);

/**
* @param {RegExp} thisRegex
* @param {string} string
* @returns {string[]}
*/
primordials.SafeRegExpPrototypeSymbolMatch = (thisRegex, string) =>
RegExpPrototypeSymbolMatch(toSafeRegex(thisRegex), string);

/**
* @param {RegExp} thisRegex
* @param {string} string
* @param {string | (substring: string, ...args: any[]) => string} replaceVal
* @returns {string}
*/
primordials.SafeRegExpPrototypeSymbolReplace =
(thisRegex, string, replaceVal) =>
RegExpPrototypeSymbolReplace(toSafeRegex(thisRegex), string, replaceVal);

/**
* @param {RegExp} thisRegex
* @param {string} string
* @returns {number}
*/
primordials.SafeRegExpPrototypeSymbolSearch = (thisRegex, string) =>
RegExpPrototypeSymbolSearch(toSafeRegex(thisRegex), string);

/**
* Re-implementation of `RegExp.prototype [ @@split ]` that do not rely on
* user-mutable methods.
* @ref https://tc39.es/ecma262/#sec-regexp.prototype-@@split
* @param {RegExp} splitter
* @param {string} string
* @param {number} [limit]
* @returns {string[]}
*/
primordials.SafeRegExpPrototypeSymbolSplit = (splitter, string, limit) => {
if (!RegExpPrototypeGetSticky(splitter))
// eslint-disable-next-line no-restricted-syntax
throw new Error('You must use sticky flag (y).');
if (RegExpPrototypeGetUnicode(splitter))
// eslint-disable-next-line no-restricted-syntax
throw new Error('Unicode matching regex not supported');
const S = string;
const A = [];
let lengthA = 0;
if (limit === 0) return A;
const size = S.length;
if (size === 0) {
const z = RegExpPrototypeExec(splitter, S);
if (z === null) A[0] = S;
return A;
}

let p = 0;
let q = p;
while (q < size) {
splitter.lastIndex = q;
const z = RegExpPrototypeExec(splitter, S);
if (z === null) q++;
else {
const e = MathMin(splitter.lastIndex, size);
if (e === p) q++;
else {
const T = StringPrototypeSlice(S, p, q);
A[lengthA++] = T;
if (lengthA === limit) return A;
p = e;
const numberOfCaptures = MathMax(z.length - 1, 0);
for (let i = 1; i <= numberOfCaptures; i++) {
const nextCapture = z[i];
A[lengthA++] = nextCapture;
if (lengthA === limit) return A;
}
q = p;
}
}
}
A[lengthA] = StringPrototypeSlice(S, p, size);
return A;
};

primordials.SafeRegExpPrototypeTest = (thisRegex, string) =>
RegExpPrototypeExec(thisRegex, string) !== null;
}

// Because these functions are used by `makeSafe`, which is exposed
// on the `primordials` object, it's important to use const references
// to the primordials that they use:
Expand Down
Loading