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