Skip to content

Commit b200cd8

Browse files
legendecastargos
authored andcommitted
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: #59751 Reviewed-By: Marco Ippolito <[email protected]>
1 parent 0d23fd5 commit b200cd8

File tree

5 files changed

+189
-257
lines changed

5 files changed

+189
-257
lines changed

‎lib/internal/assert/utils.js‎

Lines changed: 9 additions & 205 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,21 @@
11
'use strict';
22

33
const{
4-
ArrayPrototypeShift,
54
Error,
65
ErrorCaptureStackTrace,
7-
FunctionPrototypeBind,
8-
RegExpPrototypeSymbolReplace,
9-
SafeMap,
106
StringPrototypeCharCodeAt,
11-
StringPrototypeIncludes,
127
StringPrototypeReplace,
13-
StringPrototypeSlice,
14-
StringPrototypeSplit,
15-
StringPrototypeStartsWith,
168
}=primordials;
179

18-
const{ Buffer }=require('buffer');
1910
const{
2011
isErrorStackTraceLimitWritable,
21-
overrideStackTrace,
2212
}=require('internal/errors');
2313
constAssertionError=require('internal/assert/assertion_error');
24-
const{ openSync, closeSync, readSync }=require('fs');
25-
const{EOL}=require('internal/constants');
26-
const{ BuiltinModule }=require('internal/bootstrap/realm');
2714
const{ isError }=require('internal/util');
2815

29-
consterrorCache=newSafeMap();
30-
const{ fileURLToPath }=require('internal/url');
31-
32-
letparseExpressionAt;
33-
letfindNodeAround;
34-
lettokenizer;
35-
letdecoder;
16+
const{
17+
getErrorSourceExpression,
18+
}=require('internal/errors/error_source');
3619

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

5134
constescapeFn=(str)=>meta[StringPrototypeCharCodeAt(str,0)];
5235

53-
functionfindColumn(fd,column,code){
54-
if(code.length>column+100){
55-
try{
56-
returnparseCode(code,column);
57-
}catch{
58-
// End recursion in case no code could be parsed. The expression should
59-
// have been found after 2500 characters, so stop trying.
60-
if(code.length-column>2500){
61-
// eslint-disable-next-line no-throw-literal
62-
thrownull;
63-
}
64-
}
65-
}
66-
// Read up to 2500 bytes more than necessary in columns. That way we address
67-
// multi byte characters and read enough data to parse the code.
68-
constbytesToRead=column-code.length+2500;
69-
constbuffer=Buffer.allocUnsafe(bytesToRead);
70-
constbytesRead=readSync(fd,buffer,0,bytesToRead);
71-
code+=decoder.write(buffer.slice(0,bytesRead));
72-
// EOF: fast path.
73-
if(bytesRead<bytesToRead){
74-
returnparseCode(code,column);
75-
}
76-
// Read potentially missing code.
77-
returnfindColumn(fd,column,code);
78-
}
79-
80-
functiongetCode(fd,line,column){
81-
letbytesRead=0;
82-
if(line===0){
83-
// Special handle line number one. This is more efficient and simplifies the
84-
// rest of the algorithm. Read more than the regular column number in bytes
85-
// to prevent multiple reads in case multi byte characters are used.
86-
returnfindColumn(fd,column,'');
87-
}
88-
letlines=0;
89-
// Prevent blocking the event loop by limiting the maximum amount of
90-
// data that may be read.
91-
letmaxReads=32;// bytesPerRead * maxReads = 512 KiB
92-
constbytesPerRead=16384;
93-
// Use a single buffer up front that is reused until the call site is found.
94-
letbuffer=Buffer.allocUnsafe(bytesPerRead);
95-
while(maxReads--!==0){
96-
// Only allocate a new buffer in case the needed line is found. All data
97-
// before that can be discarded.
98-
buffer=lines<line ? buffer : Buffer.allocUnsafe(bytesPerRead);
99-
bytesRead=readSync(fd,buffer,0,bytesPerRead);
100-
// Read the buffer until the required code line is found.
101-
for(leti=0;i<bytesRead;i++){
102-
if(buffer[i]===10&&++lines===line){
103-
// If the end of file is reached, directly parse the code and return.
104-
if(bytesRead<bytesPerRead){
105-
returnparseCode(buffer.toString('utf8',i+1,bytesRead),column);
106-
}
107-
// Check if the read code is sufficient or read more until the whole
108-
// expression is read. Make sure multi byte characters are preserved
109-
// properly by using the decoder.
110-
constcode=decoder.write(buffer.slice(i+1,bytesRead));
111-
returnfindColumn(fd,column,code);
112-
}
113-
}
114-
}
115-
}
116-
117-
functionparseCode(code,offset){
118-
// Lazy load acorn.
119-
if(parseExpressionAt===undefined){
120-
constParser=require('internal/deps/acorn/acorn/dist/acorn').Parser;
121-
({ findNodeAround }=require('internal/deps/acorn/acorn-walk/dist/walk'));
122-
123-
parseExpressionAt=FunctionPrototypeBind(Parser.parseExpressionAt,Parser);
124-
tokenizer=FunctionPrototypeBind(Parser.tokenizer,Parser);
125-
}
126-
letnode;
127-
letstart;
128-
// Parse the read code until the correct expression is found.
129-
for(consttokenoftokenizer(code,{ecmaVersion: 'latest'})){
130-
start=token.start;
131-
if(start>offset){
132-
// No matching expression found. This could happen if the assert
133-
// expression is bigger than the provided buffer.
134-
break;
135-
}
136-
try{
137-
node=parseExpressionAt(code,start,{ecmaVersion: 'latest'});
138-
// Find the CallExpression in the tree.
139-
node=findNodeAround(node,offset,'CallExpression');
140-
if(node?.node.end>=offset){
141-
return[
142-
node.node.start,
143-
StringPrototypeReplace(StringPrototypeSlice(code,
144-
node.node.start,node.node.end),
145-
escapeSequencesRegExp,escapeFn),
146-
];
147-
}
148-
// eslint-disable-next-line no-unused-vars
149-
}catch(err){
150-
continue;
151-
}
152-
}
153-
// eslint-disable-next-line no-throw-literal
154-
thrownull;
155-
}
156-
157-
functiongetErrMessage(message,fn){
36+
functiongetErrMessage(fn){
15837
consttmpLimit=Error.stackTraceLimit;
15938
consterrorStackTraceLimitIsWritable=isErrorStackTraceLimitWritable();
16039
// Make sure the limit is set to 1. Otherwise it could fail (<= 0) or it
@@ -166,85 +45,10 @@ function getErrMessage(message, fn){
16645
ErrorCaptureStackTrace(err,fn);
16746
if(errorStackTraceLimitIsWritable)Error.stackTraceLimit=tmpLimit;
16847

169-
overrideStackTrace.set(err,(_,stack)=>stack);
170-
constcall=err.stack[0];
171-
172-
letfilename=call.getFileName();
173-
constline=call.getLineNumber()-1;
174-
letcolumn=call.getColumnNumber()-1;
175-
letidentifier;
176-
177-
if(filename){
178-
identifier=`${filename}${line}${column}`;
179-
180-
// Skip Node.js modules!
181-
if(StringPrototypeStartsWith(filename,'node:')&&
182-
BuiltinModule.exists(StringPrototypeSlice(filename,5))){
183-
errorCache.set(identifier,undefined);
184-
return;
185-
}
186-
}else{
187-
returnmessage;
188-
}
189-
190-
if(errorCache.has(identifier)){
191-
returnerrorCache.get(identifier);
192-
}
193-
194-
letfd;
195-
try{
196-
// Set the stack trace limit to zero. This makes sure unexpected token
197-
// errors are handled faster.
198-
if(errorStackTraceLimitIsWritable)Error.stackTraceLimit=0;
199-
200-
if(decoder===undefined){
201-
const{ StringDecoder }=require('string_decoder');
202-
decoder=newStringDecoder('utf8');
203-
}
204-
205-
// ESM file prop is a file proto. Convert that to path.
206-
// This ensure opensync will not throw ENOENT for ESM files.
207-
constfileProtoPrefix='file://';
208-
if(StringPrototypeStartsWith(filename,fileProtoPrefix)){
209-
filename=fileURLToPath(filename);
210-
}
211-
212-
fd=openSync(filename,'r',0o666);
213-
// Reset column and message.
214-
({0: column,1: message}=getCode(fd,line,column));
215-
// Flush unfinished multi byte characters.
216-
decoder.end();
217-
218-
// Always normalize indentation, otherwise the message could look weird.
219-
if(StringPrototypeIncludes(message,'\n')){
220-
if(EOL==='\r\n'){
221-
message=RegExpPrototypeSymbolReplace(/\r\n/g,message,'\n');
222-
}
223-
constframes=StringPrototypeSplit(message,'\n');
224-
message=ArrayPrototypeShift(frames);
225-
for(leti=0;i<frames.length;i++){
226-
constframe=frames[i];
227-
letpos=0;
228-
while(pos<column&&(frame[pos]===' '||frame[pos]==='\t')){
229-
pos++;
230-
}
231-
message+=`\n ${StringPrototypeSlice(frame,pos)}`;
232-
}
233-
}
234-
message=`The expression evaluated to a falsy value:\n\n ${message}\n`;
235-
// Make sure to always set the cache! No matter if the message is
236-
// undefined or not
237-
errorCache.set(identifier,message);
238-
239-
returnmessage;
240-
}catch{
241-
// Invalidate cache to prevent trying to read this part again.
242-
errorCache.set(identifier,undefined);
243-
}finally{
244-
// Reset limit.
245-
if(errorStackTraceLimitIsWritable)Error.stackTraceLimit=tmpLimit;
246-
if(fd!==undefined)
247-
closeSync(fd);
48+
letsource=getErrorSourceExpression(err);
49+
if(source){
50+
source=StringPrototypeReplace(source,escapeSequencesRegExp,escapeFn);
51+
return`The expression evaluated to a falsy value:\n\n ${source}\n`;
24852
}
24953
}
25054

@@ -257,7 +61,7 @@ function innerOk(fn, argLen, value, message){
25761
message='No value argument passed to `assert.ok()`';
25862
}elseif(message==null){
25963
generatedMessage=true;
260-
message=getErrMessage(message,fn);
64+
message=getErrMessage(fn);
26165
}elseif(isError(message)){
26266
throwmessage;
26367
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
'use strict';
2+
3+
const{
4+
FunctionPrototypeBind,
5+
StringPrototypeSlice,
6+
}=primordials;
7+
8+
const{
9+
getErrorSourcePositions,
10+
}=internalBinding('errors');
11+
12+
/**
13+
* Get the source location of an error.
14+
*
15+
* The `error.stack` must not have been accessed. The resolution is based on the structured
16+
* error stack data.
17+
* @param{Error|object} error An error object, or an object being invoked with ErrorCaptureStackTrace
18+
* @returns{{sourceLine: string, startColumn: number}|undefined}
19+
*/
20+
functiongetErrorSourceLocation(error){
21+
constpos=getErrorSourcePositions(error);
22+
const{
23+
sourceLine,
24+
startColumn,
25+
}=pos;
26+
27+
return{ sourceLine, startColumn };
28+
}
29+
30+
constmemberAccessTokens=['.','?.','[',']'];
31+
constmemberNameTokens=['name','string','num'];
32+
lettokenizer;
33+
/**
34+
* Get the first expression in a code string at the startColumn.
35+
* @param{string} code source code line
36+
* @param{number} startColumn which column the error is constructed
37+
* @returns{string}
38+
*/
39+
functiongetFirstExpression(code,startColumn){
40+
// Lazy load acorn.
41+
if(tokenizer===undefined){
42+
constParser=require('internal/deps/acorn/acorn/dist/acorn').Parser;
43+
tokenizer=FunctionPrototypeBind(Parser.tokenizer,Parser);
44+
}
45+
46+
letlastToken;
47+
letfirstMemberAccessNameToken;
48+
letterminatingCol;
49+
letparenLvl=0;
50+
// Tokenize the line to locate the expression at the startColumn.
51+
// The source line may be an incomplete JavaScript source, so do not parse the source line.
52+
for(consttokenoftokenizer(code,{ecmaVersion: 'latest'})){
53+
// Peek before the startColumn.
54+
if(token.start<startColumn){
55+
// There is a semicolon. This is a statement before the startColumn, so reset the memo.
56+
if(token.type.label===''){
57+
firstMemberAccessNameToken=null;
58+
continue;
59+
}
60+
// Try to memo the member access expressions before the startColumn, so that the
61+
// returned source code contains more info:
62+
// assert.ok(value)
63+
// ^ startColumn
64+
// The member expression can also be like
65+
// assert['ok'](value) or assert?.ok(value)
66+
// ^ startColumn ^ startColumn
67+
if(memberAccessTokens.includes(token.type.label)&&lastToken?.type.label==='name'){
68+
// First member access name token must be a 'name'.
69+
firstMemberAccessNameToken??=lastToken;
70+
}elseif(!memberAccessTokens.includes(token.type.label)&&
71+
!memberNameTokens.includes(token.type.label)){
72+
// Reset the memo if it is not a simple member access.
73+
// For example: assert[(() => 'ok')()](value)
74+
// ^ startColumn
75+
firstMemberAccessNameToken=null;
76+
}
77+
lastToken=token;
78+
continue;
79+
}
80+
// Now after the startColumn, this must be an expression.
81+
if(token.type.label==='('){
82+
parenLvl++;
83+
continue;
84+
}
85+
if(token.type.label===')'){
86+
parenLvl--;
87+
if(parenLvl===0){
88+
// A matched closing parenthesis found after the startColumn,
89+
// terminate here. Include the token.
90+
// (assert.ok(false), assert.ok(true))
91+
// ^ startColumn
92+
terminatingCol=token.start+1;
93+
break;
94+
}
95+
continue;
96+
}
97+
if(token.type.label===''){
98+
// A semicolon found after the startColumn, terminate here.
99+
// assert.ok(false); assert.ok(true));
100+
// ^ startColumn
101+
terminatingCol=token;
102+
break;
103+
}
104+
// If no semicolon found after the startColumn. The string after the
105+
// startColumn must be the expression.
106+
// assert.ok(false)
107+
// ^ startColumn
108+
}
109+
conststart=firstMemberAccessNameToken?.start??startColumn;
110+
returnStringPrototypeSlice(code,start,terminatingCol);
111+
}
112+
113+
/**
114+
* Get the source expression of an error.
115+
*
116+
* The `error.stack` must not have been accessed, or the source location may be incorrect. The
117+
* resolution is based on the structured error stack data.
118+
* @param{Error|object} error An error object, or an object being invoked with ErrorCaptureStackTrace
119+
* @returns{string|undefined}
120+
*/
121+
functiongetErrorSourceExpression(error){
122+
constloc=getErrorSourceLocation(error);
123+
if(loc===undefined){
124+
return;
125+
}
126+
const{ sourceLine, startColumn }=loc;
127+
returngetFirstExpression(sourceLine,startColumn);
128+
}
129+
130+
module.exports={
131+
getErrorSourceLocation,
132+
getErrorSourceExpression,
133+
};

0 commit comments

Comments
(0)