@@ -34,6 +34,10 @@ const { NativeModule } = require('internal/bootstrap/loaders');
3434
3535let isDeepEqual ;
3636let isDeepStrictEqual ;
37+ let parseExpressionAt ;
38+ let findNodeAround ;
39+ let columnOffset = 0 ;
40+ let decoder ;
3741
3842function lazyLoadComparison ( ) {
3943 const comparison = require ( 'internal/util/comparisons' ) ;
@@ -113,52 +117,141 @@ function fail(actual, expected, message, operator, stackStartFn) {
113117assert . fail = fail ;
114118
115119// The AssertionError is defined in internal/error.
116- // new assert.AssertionError({ message: message,
117- // actual: actual,
118- // expected: expected });
119120assert . AssertionError = AssertionError ;
120121
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+ }
122157 let lines = 0 ;
123158 // Prevent blocking the event loop by limiting the maximum amount of
124159 // data that may be read.
125160 let maxReads = 64 ; // bytesPerRead * maxReads = 512 kb
126- let bytesRead = 0 ;
127- let startBuffer = 0 ; // Start reading from that char on
128161 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 ) ;
132168 bytesRead = readSync ( fd , buffer , 0 , bytesPerRead ) ;
169+ // Read the buffer until the required code line is found.
133170 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 ) ;
142175 }
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 ) ;
143181 }
144182 }
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+ }
149210 }
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+ ] ;
152218}
153219
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+
155236 const filename = call . getFileName ( ) ;
237+
156238 if ( ! filename ) {
157- return ;
239+ return message ;
158240 }
159241
160242 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+
162255 const identifier = `${ filename } ${ line } ${ column } ` ;
163256
164257 if ( errorCache . has ( identifier ) ) {
@@ -171,57 +264,49 @@ function getErrMessage(call) {
171264 return ;
172265 }
173266
174- let fd , message ;
267+ let fd ;
175268 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+
176278 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 ) {
196285 if ( EOL === '\r\n' ) {
197286 message = message . replace ( / \r \n / g, '\n' ) ;
198287 }
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 ++ ;
210294 }
295+ message += `\n ${ frame . slice ( pos ) } ` ;
211296 }
212- message = 'The expression evaluated to a falsy value:' +
213- `\n\n assert${ ok } (${ message } )\n` ;
214297 }
298+ message = `The expression evaluated to a falsy value:\n\n ${ message } \n` ;
215299 // Make sure to always set the cache! No matter if the message is
216300 // undefined or not
217301 errorCache . set ( identifier , message ) ;
218302
219303 return message ;
220-
221304 } catch ( e ) {
222305 // Invalidate cache to prevent trying to read this part again.
223306 errorCache . set ( identifier , undefined ) ;
224307 } finally {
308+ // Reset limit.
309+ Error . stackTraceLimit = tmpLimit ;
225310 if ( fd !== undefined )
226311 closeSync ( fd ) ;
227312 }
@@ -235,25 +320,8 @@ function innerOk(fn, argLen, value, message) {
235320 generatedMessage = true ;
236321 message = 'No value argument passed to `assert.ok()`' ;
237322 } 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 ;
256323 generatedMessage = true ;
324+ message = getErrMessage ( message , fn ) ;
257325 } else if ( message instanceof Error ) {
258326 throw message ;
259327 }
0 commit comments