node/lib/internal/errors/error_source.js
Chengzhong Wu d35bd2088e lib,src: refactor assert to load error source from memory
The source code is available from V8 API and assert can avoid reading
the source file from the filesystem and parse the file again.

PR-URL: https://github.com/nodejs/node/pull/59751
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
2025-09-11 10:37:45 +00:00

134 lines
4.4 KiB
JavaScript

'use strict';
const {
FunctionPrototypeBind,
StringPrototypeSlice,
} = primordials;
const {
getErrorSourcePositions,
} = internalBinding('errors');
/**
* Get the source location of an error.
*
* The `error.stack` must not have been accessed. The resolution is based on the structured
* error stack data.
* @param {Error|object} error An error object, or an object being invoked with ErrorCaptureStackTrace
* @returns {{sourceLine: string, startColumn: number}|undefined}
*/
function getErrorSourceLocation(error) {
const pos = getErrorSourcePositions(error);
const {
sourceLine,
startColumn,
} = pos;
return { sourceLine, startColumn };
}
const memberAccessTokens = [ '.', '?.', '[', ']' ];
const memberNameTokens = [ 'name', 'string', 'num' ];
let tokenizer;
/**
* Get the first expression in a code string at the startColumn.
* @param {string} code source code line
* @param {number} startColumn which column the error is constructed
* @returns {string}
*/
function getFirstExpression(code, startColumn) {
// Lazy load acorn.
if (tokenizer === undefined) {
const Parser = require('internal/deps/acorn/acorn/dist/acorn').Parser;
tokenizer = FunctionPrototypeBind(Parser.tokenizer, Parser);
}
let lastToken;
let firstMemberAccessNameToken;
let terminatingCol;
let parenLvl = 0;
// Tokenize the line to locate the expression at the startColumn.
// The source line may be an incomplete JavaScript source, so do not parse the source line.
for (const token of tokenizer(code, { ecmaVersion: 'latest' })) {
// Peek before the startColumn.
if (token.start < startColumn) {
// There is a semicolon. This is a statement before the startColumn, so reset the memo.
if (token.type.label === ';') {
firstMemberAccessNameToken = null;
continue;
}
// Try to memo the member access expressions before the startColumn, so that the
// returned source code contains more info:
// assert.ok(value)
// ^ startColumn
// The member expression can also be like
// assert['ok'](value) or assert?.ok(value)
// ^ startColumn ^ startColumn
if (memberAccessTokens.includes(token.type.label) && lastToken?.type.label === 'name') {
// First member access name token must be a 'name'.
firstMemberAccessNameToken ??= lastToken;
} else if (!memberAccessTokens.includes(token.type.label) &&
!memberNameTokens.includes(token.type.label)) {
// Reset the memo if it is not a simple member access.
// For example: assert[(() => 'ok')()](value)
// ^ startColumn
firstMemberAccessNameToken = null;
}
lastToken = token;
continue;
}
// Now after the startColumn, this must be an expression.
if (token.type.label === '(') {
parenLvl++;
continue;
}
if (token.type.label === ')') {
parenLvl--;
if (parenLvl === 0) {
// A matched closing parenthesis found after the startColumn,
// terminate here. Include the token.
// (assert.ok(false), assert.ok(true))
// ^ startColumn
terminatingCol = token.start + 1;
break;
}
continue;
}
if (token.type.label === ';') {
// A semicolon found after the startColumn, terminate here.
// assert.ok(false); assert.ok(true));
// ^ startColumn
terminatingCol = token;
break;
}
// If no semicolon found after the startColumn. The string after the
// startColumn must be the expression.
// assert.ok(false)
// ^ startColumn
}
const start = firstMemberAccessNameToken?.start ?? startColumn;
return StringPrototypeSlice(code, start, terminatingCol);
}
/**
* Get the source expression of an error.
*
* The `error.stack` must not have been accessed, or the source location may be incorrect. The
* resolution is based on the structured error stack data.
* @param {Error|object} error An error object, or an object being invoked with ErrorCaptureStackTrace
* @returns {string|undefined}
*/
function getErrorSourceExpression(error) {
const loc = getErrorSourceLocation(error);
if (loc === undefined) {
return;
}
const { sourceLine, startColumn } = loc;
return getFirstExpression(sourceLine, startColumn);
}
module.exports = {
getErrorSourceLocation,
getErrorSourceExpression,
};