Skip to content
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

assert: fix single assert first line #21626

Closed
wants to merge 1 commit into from
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
224 changes: 146 additions & 78 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ const { NativeModule } = require('internal/bootstrap/loaders');

let isDeepEqual;
let isDeepStrictEqual;
let parseExpressionAt;
let findNodeAround;
let columnOffset = 0;
let decoder;

function lazyLoadComparison() {
const comparison = require('internal/util/comparisons');
Expand Down Expand Up @@ -114,52 +118,141 @@ function fail(actual, expected, message, operator, stackStartFn) {
assert.fail = fail;

// The AssertionError is defined in internal/error.
// new assert.AssertionError({ message: message,
// actual: actual,
// expected: expected });
assert.AssertionError = AssertionError;

function getBuffer(fd, assertLine) {
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 = 64; // bytesPerRead * maxReads = 512 kb
let bytesRead = 0;
let startBuffer = 0; // Start reading from that char on
const bytesPerRead = 8192;
const buffers = [];
do {
const buffer = Buffer.allocUnsafe(bytesPerRead);
// 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 (var i = 0; i < bytesRead; i++) {
if (buffer[i] === 10) {
lines++;
if (lines === assertLine) {
startBuffer = i + 1;
// Read up to 15 more lines to make sure all code gets matched
} else if (lines === assertLine + 16) {
buffers.push(buffer.slice(startBuffer, i));
return buffers;
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);
}
}
if (lines >= assertLine) {
buffers.push(buffer.slice(startBuffer, bytesRead));
// Reset the startBuffer in case we need more than one chunk
startBuffer = 0;
}
}

function parseCode(code, offset) {
// Lazy load acorn.
if (parseExpressionAt === undefined) {
({ parseExpressionAt } = require('internal/deps/acorn/dist/acorn'));
({ findNodeAround } = require('internal/deps/acorn/dist/walk'));
}
let node;
let start = 0;
// Parse the read code until the correct expression is found.
do {
try {
node = parseExpressionAt(code, start);
start = node.end + 1 || start;
// Find the CallExpression in the tree.
node = findNodeAround(node, offset, 'CallExpression');
} catch (err) {
// Unexpected token error and the like.
start += err.raisedAt || 1;
if (start > offset) {
// No matching expression found. This could happen if the assert
// expression is bigger than the provided buffer.
// eslint-disable-next-line no-throw-literal
throw null;
}
}
} while (--maxReads !== 0 && bytesRead !== 0);
return buffers;
} while (node === undefined || node.node.end < offset);

return [
node.node.start,
code.slice(node.node.start, node.node.end)
.replace(escapeSequencesRegExp, escapeFn)
];
}

function getErrMessage(call) {
function getErrMessage(message, fn) {
const tmpLimit = Error.stackTraceLimit;
// Make sure the limit is set to 1. Otherwise it could fail (<= 0) or it
// does to much work.
Error.stackTraceLimit = 1;
// We only need the stack trace. To minimize the overhead use an object
// instead of an error.
const err = {};
Error.captureStackTrace(err, fn);
Error.stackTraceLimit = tmpLimit;

const tmpPrepare = Error.prepareStackTrace;
Error.prepareStackTrace = (_, stack) => stack;
const call = err.stack[0];
Error.prepareStackTrace = tmpPrepare;

const filename = call.getFileName();

if (!filename) {
return;
return message;
}

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

// Line number one reports the wrong column due to being wrapped in a
// function. Remove that offset to get the actual call.
if (line === 0) {
if (columnOffset === 0) {
const { wrapper } = require('internal/modules/cjs/loader');
columnOffset = wrapper[0].length;
}
column -= columnOffset;
}

const identifier = `${filename}${line}${column}`;

if (errorCache.has(identifier)) {
Expand All @@ -172,57 +265,49 @@ function getErrMessage(call) {
return;
}

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

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

fd = openSync(filename, 'r', 0o666);
const buffers = getBuffer(fd, line);
const code = Buffer.concat(buffers).toString('utf8');
// Lazy load acorn.
const { parseExpressionAt } = require('internal/deps/acorn/dist/acorn');
const nodes = parseExpressionAt(code, column);
// Node type should be "CallExpression" and some times
// "SequenceExpression".
const node = nodes.type === 'CallExpression' ? nodes : nodes.expressions[0];
const name = node.callee.name;
// Calling `ok` with .apply or .call is uncommon but we use a simple
// safeguard nevertheless.
if (name !== 'apply' && name !== 'call') {
// Only use `assert` and `assert.ok` to reference the "real API" and
// not user defined function names.
const ok = name === 'ok' ? '.ok' : '';
const args = node.arguments;
message = code
.slice(args[0].start, args[args.length - 1].end)
.replace(escapeSequencesRegExp, escapeFn);
// Reset column and message.
[column, message] = getCode(fd, line, column);
// Flush unfinished multi byte characters.
decoder.end();
// Always normalize indentation, otherwise the message could look weird.
if (message.indexOf('\n') !== -1) {
if (EOL === '\r\n') {
message = message.replace(/\r\n/g, '\n');
}
// Always normalize indentation, otherwise the message could look weird.
if (message.indexOf('\n') !== -1) {
const tmp = message.split('\n');
message = tmp[0];
for (var i = 1; i < tmp.length; i++) {
let pos = 0;
while (pos < column &&
(tmp[i][pos] === ' ' || tmp[i][pos] === '\t')) {
pos++;
}
message += `\n ${tmp[i].slice(pos)}`;
const frames = message.split('\n');
message = frames.shift();
for (const frame of frames) {
let pos = 0;
while (pos < column && (frame[pos] === ' ' || frame[pos] === '\t')) {
pos++;
}
message += `\n ${frame.slice(pos)}`;
}
message = 'The expression evaluated to a falsy value:' +
`\n\n assert${ok}(${message})\n`;
}
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 (e) {
// Invalidate cache to prevent trying to read this part again.
errorCache.set(identifier, undefined);
} finally {
// Reset limit.
Error.stackTraceLimit = tmpLimit;
if (fd !== undefined)
closeSync(fd);
}
Expand All @@ -236,25 +321,8 @@ function innerOk(fn, argLen, value, message) {
generatedMessage = true;
message = 'No value argument passed to `assert.ok()`';
} else if (message == null) {
// Use the call as error message if possible.
// This does not work with e.g. the repl.
// eslint-disable-next-line no-restricted-syntax
const err = new Error();
// Make sure the limit is set to 1. Otherwise it could fail (<= 0) or it
// does to much work.
const tmpLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 1;
Error.captureStackTrace(err, fn);
Error.stackTraceLimit = tmpLimit;

const tmpPrepare = Error.prepareStackTrace;
Error.prepareStackTrace = (_, stack) => stack;
const call = err.stack[0];
Error.prepareStackTrace = tmpPrepare;

// Make sure it would be "null" in case that is used.
message = getErrMessage(call) || message;
generatedMessage = true;
message = getErrMessage(message, fn);
} else if (message instanceof Error) {
throw message;
}
Expand Down
2 changes: 2 additions & 0 deletions test/fixtures/assert-first-line.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
'use strict'; const ässört = require('assert'); ässört(true); ässört.ok(''); ässört(null);
// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(false);
1 change: 1 addition & 0 deletions test/fixtures/assert-long-line.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading