@@ -34,6 +34,10 @@ const { NativeModule } = require('internal/bootstrap/loaders');
34
34
35
35
let isDeepEqual ;
36
36
let isDeepStrictEqual ;
37
+ let parseExpressionAt ;
38
+ let findNodeAround ;
39
+ let columnOffset = 0 ;
40
+ let decoder ;
37
41
38
42
function lazyLoadComparison ( ) {
39
43
const comparison = require ( 'internal/util/comparisons' ) ;
@@ -113,52 +117,141 @@ function fail(actual, expected, message, operator, stackStartFn) {
113
117
assert . fail = fail ;
114
118
115
119
// The AssertionError is defined in internal/error.
116
- // new assert.AssertionError({ message: message,
117
- // actual: actual,
118
- // expected: expected });
119
120
assert . AssertionError = AssertionError ;
120
121
121
- function getBuffer ( fd , assertLine ) {
122
+ function findColumn ( fd , column , code ) {
123
+ if ( code . length > column + 100 ) {
124
+ try {
125
+ return parseCode ( code , column ) ;
126
+ } catch {
127
+ // End recursion in case no code could be parsed. The expression should
128
+ // have been found after 2500 characters, so stop trying.
129
+ if ( code . length - column > 2500 ) {
130
+ // eslint-disable-next-line no-throw-literal
131
+ throw null ;
132
+ }
133
+ }
134
+ }
135
+ // Read up to 2500 bytes more than necessary in columns. That way we address
136
+ // multi byte characters and read enough data to parse the code.
137
+ const bytesToRead = column - code . length + 2500 ;
138
+ const buffer = Buffer . allocUnsafe ( bytesToRead ) ;
139
+ const bytesRead = readSync ( fd , buffer , 0 , bytesToRead ) ;
140
+ code += decoder . write ( buffer . slice ( 0 , bytesRead ) ) ;
141
+ // EOF: fast path.
142
+ if ( bytesRead < bytesToRead ) {
143
+ return parseCode ( code , column ) ;
144
+ }
145
+ // Read potentially missing code.
146
+ return findColumn ( fd , column , code ) ;
147
+ }
148
+
149
+ function getCode ( fd , line , column ) {
150
+ let bytesRead = 0 ;
151
+ if ( line === 0 ) {
152
+ // Special handle line number one. This is more efficient and simplifies the
153
+ // rest of the algorithm. Read more than the regular column number in bytes
154
+ // to prevent multiple reads in case multi byte characters are used.
155
+ return findColumn ( fd , column , '' ) ;
156
+ }
122
157
let lines = 0 ;
123
158
// Prevent blocking the event loop by limiting the maximum amount of
124
159
// data that may be read.
125
160
let maxReads = 64 ; // bytesPerRead * maxReads = 512 kb
126
- let bytesRead = 0 ;
127
- let startBuffer = 0 ; // Start reading from that char on
128
161
const bytesPerRead = 8192 ;
129
- const buffers = [ ] ;
130
- do {
131
- const buffer = Buffer . allocUnsafe ( bytesPerRead ) ;
162
+ // Use a single buffer up front that is reused until the call site is found.
163
+ let buffer = Buffer . allocUnsafe ( bytesPerRead ) ;
164
+ while ( maxReads -- !== 0 ) {
165
+ // Only allocate a new buffer in case the needed line is found. All data
166
+ // before that can be discarded.
167
+ buffer = lines < line ? buffer : Buffer . allocUnsafe ( bytesPerRead ) ;
132
168
bytesRead = readSync ( fd , buffer , 0 , bytesPerRead ) ;
169
+ // Read the buffer until the required code line is found.
133
170
for ( var i = 0 ; i < bytesRead ; i ++ ) {
134
- if ( buffer [ i ] === 10 ) {
135
- lines ++ ;
136
- if ( lines === assertLine ) {
137
- startBuffer = i + 1 ;
138
- // Read up to 15 more lines to make sure all code gets matched
139
- } else if ( lines === assertLine + 16 ) {
140
- buffers . push ( buffer . slice ( startBuffer , i ) ) ;
141
- return buffers ;
171
+ if ( buffer [ i ] === 10 && ++ lines === line ) {
172
+ // If the end of file is reached, directly parse the code and return.
173
+ if ( bytesRead < bytesPerRead ) {
174
+ return parseCode ( buffer . toString ( 'utf8' , i + 1 , bytesRead ) , column ) ;
142
175
}
176
+ // Check if the read code is sufficient or read more until the whole
177
+ // expression is read. Make sure multi byte characters are preserved
178
+ // properly by using the decoder.
179
+ const code = decoder . write ( buffer . slice ( i + 1 , bytesRead ) ) ;
180
+ return findColumn ( fd , column , code ) ;
143
181
}
144
182
}
145
- if ( lines >= assertLine ) {
146
- buffers . push ( buffer . slice ( startBuffer , bytesRead ) ) ;
147
- // Reset the startBuffer in case we need more than one chunk
148
- startBuffer = 0 ;
183
+ }
184
+ }
185
+
186
+ function parseCode ( code , offset ) {
187
+ // Lazy load acorn.
188
+ if ( parseExpressionAt === undefined ) {
189
+ ( { parseExpressionAt } = require ( 'internal/deps/acorn/dist/acorn' ) ) ;
190
+ ( { findNodeAround } = require ( 'internal/deps/acorn/dist/walk' ) ) ;
191
+ }
192
+ let node ;
193
+ let start = 0 ;
194
+ // Parse the read code until the correct expression is found.
195
+ do {
196
+ try {
197
+ node = parseExpressionAt ( code , start ) ;
198
+ start = node . end + 1 || start ;
199
+ // Find the CallExpression in the tree.
200
+ node = findNodeAround ( node , offset , 'CallExpression' ) ;
201
+ } catch ( err ) {
202
+ // Unexpected token error and the like.
203
+ start += err . raisedAt || 1 ;
204
+ if ( start > offset ) {
205
+ // No matching expression found. This could happen if the assert
206
+ // expression is bigger than the provided buffer.
207
+ // eslint-disable-next-line no-throw-literal
208
+ throw null ;
209
+ }
149
210
}
150
- } while ( -- maxReads !== 0 && bytesRead !== 0 ) ;
151
- return buffers ;
211
+ } while ( node === undefined || node . node . end < offset ) ;
212
+
213
+ return [
214
+ node . node . start ,
215
+ code . slice ( node . node . start , node . node . end )
216
+ . replace ( escapeSequencesRegExp , escapeFn )
217
+ ] ;
152
218
}
153
219
154
- function getErrMessage ( call ) {
220
+ function getErrMessage ( message , fn ) {
221
+ const tmpLimit = Error . stackTraceLimit ;
222
+ // Make sure the limit is set to 1. Otherwise it could fail (<= 0) or it
223
+ // does to much work.
224
+ Error . stackTraceLimit = 1 ;
225
+ // We only need the stack trace. To minimize the overhead use an object
226
+ // instead of an error.
227
+ const err = { } ;
228
+ Error . captureStackTrace ( err , fn ) ;
229
+ Error . stackTraceLimit = tmpLimit ;
230
+
231
+ const tmpPrepare = Error . prepareStackTrace ;
232
+ Error . prepareStackTrace = ( _ , stack ) => stack ;
233
+ const call = err . stack [ 0 ] ;
234
+ Error . prepareStackTrace = tmpPrepare ;
235
+
155
236
const filename = call . getFileName ( ) ;
237
+
156
238
if ( ! filename ) {
157
- return ;
239
+ return message ;
158
240
}
159
241
160
242
const line = call . getLineNumber ( ) - 1 ;
161
- const column = call . getColumnNumber ( ) - 1 ;
243
+ let column = call . getColumnNumber ( ) - 1 ;
244
+
245
+ // Line number one reports the wrong column due to being wrapped in a
246
+ // function. Remove that offset to get the actual call.
247
+ if ( line === 0 ) {
248
+ if ( columnOffset === 0 ) {
249
+ const { wrapper } = require ( 'internal/modules/cjs/loader' ) ;
250
+ columnOffset = wrapper [ 0 ] . length ;
251
+ }
252
+ column -= columnOffset ;
253
+ }
254
+
162
255
const identifier = `${ filename } ${ line } ${ column } ` ;
163
256
164
257
if ( errorCache . has ( identifier ) ) {
@@ -171,57 +264,49 @@ function getErrMessage(call) {
171
264
return ;
172
265
}
173
266
174
- let fd , message ;
267
+ let fd ;
175
268
try {
269
+ // Set the stack trace limit to zero. This makes sure unexpected token
270
+ // errors are handled faster.
271
+ Error . stackTraceLimit = 0 ;
272
+
273
+ if ( decoder === undefined ) {
274
+ const { StringDecoder } = require ( 'string_decoder' ) ;
275
+ decoder = new StringDecoder ( 'utf8' ) ;
276
+ }
277
+
176
278
fd = openSync ( filename , 'r' , 0o666 ) ;
177
- const buffers = getBuffer ( fd , line ) ;
178
- const code = Buffer . concat ( buffers ) . toString ( 'utf8' ) ;
179
- // Lazy load acorn.
180
- const { parseExpressionAt } = require ( 'internal/deps/acorn/dist/acorn' ) ;
181
- const nodes = parseExpressionAt ( code , column ) ;
182
- // Node type should be "CallExpression" and some times
183
- // "SequenceExpression".
184
- const node = nodes . type === 'CallExpression' ? nodes : nodes . expressions [ 0 ] ;
185
- const name = node . callee . name ;
186
- // Calling `ok` with .apply or .call is uncommon but we use a simple
187
- // safeguard nevertheless.
188
- if ( name !== 'apply' && name !== 'call' ) {
189
- // Only use `assert` and `assert.ok` to reference the "real API" and
190
- // not user defined function names.
191
- const ok = name === 'ok' ? '.ok' : '' ;
192
- const args = node . arguments ;
193
- message = code
194
- . slice ( args [ 0 ] . start , args [ args . length - 1 ] . end )
195
- . replace ( escapeSequencesRegExp , escapeFn ) ;
279
+ // Reset column and message.
280
+ [ column , message ] = getCode ( fd , line , column ) ;
281
+ // Flush unfinished multi byte characters.
282
+ decoder . end ( ) ;
283
+ // Always normalize indentation, otherwise the message could look weird.
284
+ if ( message . indexOf ( '\n' ) !== - 1 ) {
196
285
if ( EOL === '\r\n' ) {
197
286
message = message . replace ( / \r \n / g, '\n' ) ;
198
287
}
199
- // Always normalize indentation, otherwise the message could look weird.
200
- if ( message . indexOf ( '\n' ) !== - 1 ) {
201
- const tmp = message . split ( '\n' ) ;
202
- message = tmp [ 0 ] ;
203
- for ( var i = 1 ; i < tmp . length ; i ++ ) {
204
- let pos = 0 ;
205
- while ( pos < column &&
206
- ( tmp [ i ] [ pos ] === ' ' || tmp [ i ] [ pos ] === '\t' ) ) {
207
- pos ++ ;
208
- }
209
- message += `\n ${ tmp [ i ] . slice ( pos ) } ` ;
288
+ const frames = message . split ( '\n' ) ;
289
+ message = frames . shift ( ) ;
290
+ for ( const frame of frames ) {
291
+ let pos = 0 ;
292
+ while ( pos < column && ( frame [ pos ] === ' ' || frame [ pos ] === '\t' ) ) {
293
+ pos ++ ;
210
294
}
295
+ message += `\n ${ frame . slice ( pos ) } ` ;
211
296
}
212
- message = 'The expression evaluated to a falsy value:' +
213
- `\n\n assert${ ok } (${ message } )\n` ;
214
297
}
298
+ message = `The expression evaluated to a falsy value:\n\n ${ message } \n` ;
215
299
// Make sure to always set the cache! No matter if the message is
216
300
// undefined or not
217
301
errorCache . set ( identifier , message ) ;
218
302
219
303
return message ;
220
-
221
304
} catch ( e ) {
222
305
// Invalidate cache to prevent trying to read this part again.
223
306
errorCache . set ( identifier , undefined ) ;
224
307
} finally {
308
+ // Reset limit.
309
+ Error . stackTraceLimit = tmpLimit ;
225
310
if ( fd !== undefined )
226
311
closeSync ( fd ) ;
227
312
}
@@ -235,25 +320,8 @@ function innerOk(fn, argLen, value, message) {
235
320
generatedMessage = true ;
236
321
message = 'No value argument passed to `assert.ok()`' ;
237
322
} else if ( message == null ) {
238
- // Use the call as error message if possible.
239
- // This does not work with e.g. the repl.
240
- // eslint-disable-next-line no-restricted-syntax
241
- const err = new Error ( ) ;
242
- // Make sure the limit is set to 1. Otherwise it could fail (<= 0) or it
243
- // does to much work.
244
- const tmpLimit = Error . stackTraceLimit ;
245
- Error . stackTraceLimit = 1 ;
246
- Error . captureStackTrace ( err , fn ) ;
247
- Error . stackTraceLimit = tmpLimit ;
248
-
249
- const tmpPrepare = Error . prepareStackTrace ;
250
- Error . prepareStackTrace = ( _ , stack ) => stack ;
251
- const call = err . stack [ 0 ] ;
252
- Error . prepareStackTrace = tmpPrepare ;
253
-
254
- // Make sure it would be "null" in case that is used.
255
- message = getErrMessage ( call ) || message ;
256
323
generatedMessage = true ;
324
+ message = getErrMessage ( message , fn ) ;
257
325
} else if ( message instanceof Error ) {
258
326
throw message ;
259
327
}
0 commit comments