Skip to content
Closed
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
214 changes: 9 additions & 205 deletions lib/internal/assert/utils.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,21 @@
'use strict';

const {
ArrayPrototypeShift,
Error,
ErrorCaptureStackTrace,
FunctionPrototypeBind,
RegExpPrototypeSymbolReplace,
SafeMap,
StringPrototypeCharCodeAt,
StringPrototypeIncludes,
StringPrototypeReplace,
StringPrototypeSlice,
StringPrototypeSplit,
StringPrototypeStartsWith,
} = primordials;

const { Buffer } = require('buffer');
const {
isErrorStackTraceLimitWritable,
overrideStackTrace,
} = require('internal/errors');
const AssertionError = require('internal/assert/assertion_error');
const { openSync, closeSync, readSync } = require('fs');
const { EOL } = require('internal/constants');
const { BuiltinModule } = require('internal/bootstrap/realm');
const { isError } = require('internal/util');

const errorCache = new SafeMap();
const { fileURLToPath } = require('internal/url');

let parseExpressionAt;
let findNodeAround;
let tokenizer;
let decoder;
const {
getErrorSourceExpression,
} = require('internal/errors/error_source');

// Escape control characters but not \n and \t to keep the line breaks and
// indentation intact.
Expand All @@ -50,111 +33,7 @@ const meta = [

const escapeFn = (str) => meta[StringPrototypeCharCodeAt(str, 0)];

function findColumn(fd, column, code) {
if (code.length > column + 100) {
try {
return parseCode(code, column);
} catch {
// End recursion in case no code could be parsed. The expression should
// have been found after 2500 characters, so stop trying.
if (code.length - column > 2500) {
// eslint-disable-next-line no-throw-literal
throw null;
}
}
}
// Read up to 2500 bytes more than necessary in columns. That way we address
// multi byte characters and read enough data to parse the code.
const bytesToRead = column - code.length + 2500;
const buffer = Buffer.allocUnsafe(bytesToRead);
const bytesRead = readSync(fd, buffer, 0, bytesToRead);
code += decoder.write(buffer.slice(0, bytesRead));
// EOF: fast path.
if (bytesRead < bytesToRead) {
return parseCode(code, column);
}
// Read potentially missing code.
return findColumn(fd, column, code);
}

function getCode(fd, line, column) {
let bytesRead = 0;
if (line === 0) {
// Special handle line number one. This is more efficient and simplifies the
// rest of the algorithm. Read more than the regular column number in bytes
// to prevent multiple reads in case multi byte characters are used.
return findColumn(fd, column, '');
}
let lines = 0;
// Prevent blocking the event loop by limiting the maximum amount of
// data that may be read.
let maxReads = 32; // bytesPerRead * maxReads = 512 KiB
const bytesPerRead = 16384;
// Use a single buffer up front that is reused until the call site is found.
let buffer = Buffer.allocUnsafe(bytesPerRead);
while (maxReads-- !== 0) {
// Only allocate a new buffer in case the needed line is found. All data
// before that can be discarded.
buffer = lines < line ? buffer : Buffer.allocUnsafe(bytesPerRead);
bytesRead = readSync(fd, buffer, 0, bytesPerRead);
// Read the buffer until the required code line is found.
for (let i = 0; i < bytesRead; i++) {
if (buffer[i] === 10 && ++lines === line) {
// If the end of file is reached, directly parse the code and return.
if (bytesRead < bytesPerRead) {
return parseCode(buffer.toString('utf8', i + 1, bytesRead), column);
}
// Check if the read code is sufficient or read more until the whole
// expression is read. Make sure multi byte characters are preserved
// properly by using the decoder.
const code = decoder.write(buffer.slice(i + 1, bytesRead));
return findColumn(fd, column, code);
}
}
}
}

function parseCode(code, offset) {
// Lazy load acorn.
if (parseExpressionAt === undefined) {
const Parser = require('internal/deps/acorn/acorn/dist/acorn').Parser;
({ findNodeAround } = require('internal/deps/acorn/acorn-walk/dist/walk'));

parseExpressionAt = FunctionPrototypeBind(Parser.parseExpressionAt, Parser);
tokenizer = FunctionPrototypeBind(Parser.tokenizer, Parser);
}
let node;
let start;
// Parse the read code until the correct expression is found.
for (const token of tokenizer(code, { ecmaVersion: 'latest' })) {
start = token.start;
if (start > offset) {
// No matching expression found. This could happen if the assert
// expression is bigger than the provided buffer.
break;
}
try {
node = parseExpressionAt(code, start, { ecmaVersion: 'latest' });
// Find the CallExpression in the tree.
node = findNodeAround(node, offset, 'CallExpression');
if (node?.node.end >= offset) {
return [
node.node.start,
StringPrototypeReplace(StringPrototypeSlice(code,
node.node.start, node.node.end),
escapeSequencesRegExp, escapeFn),
];
}
// eslint-disable-next-line no-unused-vars
} catch (err) {
continue;
}
}
// eslint-disable-next-line no-throw-literal
throw null;
}

function getErrMessage(message, fn) {
function getErrMessage(fn) {
const tmpLimit = Error.stackTraceLimit;
const errorStackTraceLimitIsWritable = isErrorStackTraceLimitWritable();
// Make sure the limit is set to 1. Otherwise it could fail (<= 0) or it
Expand All @@ -166,85 +45,10 @@ function getErrMessage(message, fn) {
ErrorCaptureStackTrace(err, fn);
if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = tmpLimit;

overrideStackTrace.set(err, (_, stack) => stack);
const call = err.stack[0];

let filename = call.getFileName();
const line = call.getLineNumber() - 1;
let column = call.getColumnNumber() - 1;
let identifier;

if (filename) {
identifier = `${filename}${line}${column}`;

// Skip Node.js modules!
if (StringPrototypeStartsWith(filename, 'node:') &&
BuiltinModule.exists(StringPrototypeSlice(filename, 5))) {
errorCache.set(identifier, undefined);
return;
}
} else {
return message;
}

if (errorCache.has(identifier)) {
return errorCache.get(identifier);
}

let fd;
try {
// Set the stack trace limit to zero. This makes sure unexpected token
// errors are handled faster.
if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = 0;

if (decoder === undefined) {
const { StringDecoder } = require('string_decoder');
decoder = new StringDecoder('utf8');
}

// ESM file prop is a file proto. Convert that to path.
// This ensure opensync will not throw ENOENT for ESM files.
const fileProtoPrefix = 'file://';
if (StringPrototypeStartsWith(filename, fileProtoPrefix)) {
filename = fileURLToPath(filename);
}

fd = openSync(filename, 'r', 0o666);
// Reset column and message.
({ 0: column, 1: message } = getCode(fd, line, column));
// Flush unfinished multi byte characters.
decoder.end();

// Always normalize indentation, otherwise the message could look weird.
if (StringPrototypeIncludes(message, '\n')) {
if (EOL === '\r\n') {
message = RegExpPrototypeSymbolReplace(/\r\n/g, message, '\n');
}
const frames = StringPrototypeSplit(message, '\n');
message = ArrayPrototypeShift(frames);
for (let i = 0; i < frames.length; i++) {
const frame = frames[i];
let pos = 0;
while (pos < column && (frame[pos] === ' ' || frame[pos] === '\t')) {
pos++;
}
message += `\n ${StringPrototypeSlice(frame, pos)}`;
}
}
message = `The expression evaluated to a falsy value:\n\n ${message}\n`;
// Make sure to always set the cache! No matter if the message is
// undefined or not
errorCache.set(identifier, message);

return message;
} catch {
// Invalidate cache to prevent trying to read this part again.
errorCache.set(identifier, undefined);
} finally {
// Reset limit.
if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = tmpLimit;
if (fd !== undefined)
closeSync(fd);
let source = getErrorSourceExpression(err);
if (source) {
source = StringPrototypeReplace(source, escapeSequencesRegExp, escapeFn);
return `The expression evaluated to a falsy value:\n\n ${source}\n`;
}
}

Expand All @@ -257,7 +61,7 @@ function innerOk(fn, argLen, value, message) {
message = 'No value argument passed to `assert.ok()`';
} else if (message == null) {
generatedMessage = true;
message = getErrMessage(message, fn);
message = getErrMessage(fn);
} else if (isError(message)) {
throw message;
}
Expand Down
Loading
Loading