| 'use strict'; |
| |
| const { |
| ArrayPrototypeShift, |
| Error, |
| ErrorCaptureStackTrace, |
| FunctionPrototypeBind, |
| RegExpPrototypeSymbolReplace, |
| SafeMap, |
| StringPrototypeCharCodeAt, |
| StringPrototypeIncludes, |
| StringPrototypeIndexOf, |
| StringPrototypeReplace, |
| StringPrototypeSlice, |
| StringPrototypeSplit, |
| StringPrototypeStartsWith, |
| } = primordials; |
| |
| const { Buffer } = require('buffer'); |
| const { |
| isErrorStackTraceLimitWritable, |
| overrideStackTrace, |
| } = require('internal/errors'); |
| const AssertionError = require('internal/assert/assertion_error'); |
| const { openSync, closeSync, readSync } = require('fs'); |
| const { EOL } = require('internal/constants'); |
| const { BuiltinModule } = require('internal/bootstrap/realm'); |
| const { isError } = require('internal/util'); |
| |
| const errorCache = new SafeMap(); |
| const { fileURLToPath } = require('internal/url'); |
| |
| let parseExpressionAt; |
| let findNodeAround; |
| let tokenizer; |
| let decoder; |
| |
| // Escape control characters but not \n and \t to keep the line breaks and |
| // indentation intact. |
| // eslint-disable-next-line no-control-regex |
| const escapeSequencesRegExp = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g; |
| const meta = [ |
| '\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004', |
| '\\u0005', '\\u0006', '\\u0007', '\\b', '', |
| '', '\\u000b', '\\f', '', '\\u000e', |
| '\\u000f', '\\u0010', '\\u0011', '\\u0012', '\\u0013', |
| '\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018', |
| '\\u0019', '\\u001a', '\\u001b', '\\u001c', '\\u001d', |
| '\\u001e', '\\u001f', |
| ]; |
| |
| const escapeFn = (str) => meta[StringPrototypeCharCodeAt(str, 0)]; |
| |
| function findColumn(fd, column, code) { |
| if (code.length > column + 100) { |
| try { |
| return parseCode(code, column); |
| } catch { |
| // End recursion in case no code could be parsed. The expression should |
| // have been found after 2500 characters, so stop trying. |
| if (code.length - column > 2500) { |
| // eslint-disable-next-line no-throw-literal |
| throw null; |
| } |
| } |
| } |
| // Read up to 2500 bytes more than necessary in columns. That way we address |
| // multi byte characters and read enough data to parse the code. |
| const bytesToRead = column - code.length + 2500; |
| const buffer = Buffer.allocUnsafe(bytesToRead); |
| const bytesRead = readSync(fd, buffer, 0, bytesToRead); |
| code += decoder.write(buffer.slice(0, bytesRead)); |
| // EOF: fast path. |
| if (bytesRead < bytesToRead) { |
| return parseCode(code, column); |
| } |
| // Read potentially missing code. |
| return findColumn(fd, column, code); |
| } |
| |
| function getCode(fd, line, column) { |
| let bytesRead = 0; |
| if (line === 0) { |
| // Special handle line number one. This is more efficient and simplifies the |
| // rest of the algorithm. Read more than the regular column number in bytes |
| // to prevent multiple reads in case multi byte characters are used. |
| return findColumn(fd, column, ''); |
| } |
| let lines = 0; |
| // Prevent blocking the event loop by limiting the maximum amount of |
| // data that may be read. |
| let maxReads = 32; // bytesPerRead * maxReads = 512 KiB |
| const bytesPerRead = 16384; |
| // Use a single buffer up front that is reused until the call site is found. |
| let buffer = Buffer.allocUnsafe(bytesPerRead); |
| while (maxReads-- !== 0) { |
| // Only allocate a new buffer in case the needed line is found. All data |
| // before that can be discarded. |
| buffer = lines < line ? buffer : Buffer.allocUnsafe(bytesPerRead); |
| bytesRead = readSync(fd, buffer, 0, bytesPerRead); |
| // Read the buffer until the required code line is found. |
| for (let i = 0; i < bytesRead; i++) { |
| if (buffer[i] === 10 && ++lines === line) { |
| // If the end of file is reached, directly parse the code and return. |
| if (bytesRead < bytesPerRead) { |
| return parseCode(buffer.toString('utf8', i + 1, bytesRead), column); |
| } |
| // Check if the read code is sufficient or read more until the whole |
| // expression is read. Make sure multi byte characters are preserved |
| // properly by using the decoder. |
| const code = decoder.write(buffer.slice(i + 1, bytesRead)); |
| return findColumn(fd, column, code); |
| } |
| } |
| } |
| } |
| |
| function parseCode(code, offset) { |
| // Lazy load acorn. |
| if (parseExpressionAt === undefined) { |
| const Parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; |
| ({ findNodeAround } = require('internal/deps/acorn/acorn-walk/dist/walk')); |
| |
| parseExpressionAt = FunctionPrototypeBind(Parser.parseExpressionAt, Parser); |
| tokenizer = FunctionPrototypeBind(Parser.tokenizer, Parser); |
| } |
| let node; |
| let start; |
| // Parse the read code until the correct expression is found. |
| for (const token of tokenizer(code, { ecmaVersion: 'latest' })) { |
| start = token.start; |
| if (start > offset) { |
| // No matching expression found. This could happen if the assert |
| // expression is bigger than the provided buffer. |
| break; |
| } |
| try { |
| node = parseExpressionAt(code, start, { ecmaVersion: 'latest' }); |
| // Find the CallExpression in the tree. |
| node = findNodeAround(node, offset, 'CallExpression'); |
| if (node?.node.end >= offset) { |
| return [ |
| node.node.start, |
| StringPrototypeReplace(StringPrototypeSlice(code, |
| node.node.start, node.node.end), |
| escapeSequencesRegExp, escapeFn), |
| ]; |
| } |
| // eslint-disable-next-line no-unused-vars |
| } catch (err) { |
| continue; |
| } |
| } |
| // eslint-disable-next-line no-throw-literal |
| throw null; |
| } |
| |
| function getErrMessage(message, fn) { |
| const tmpLimit = Error.stackTraceLimit; |
| const errorStackTraceLimitIsWritable = isErrorStackTraceLimitWritable(); |
| // Make sure the limit is set to 1. Otherwise it could fail (<= 0) or it |
| // does to much work. |
| if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = 1; |
| // We only need the stack trace. To minimize the overhead use an object |
| // instead of an error. |
| const err = {}; |
| ErrorCaptureStackTrace(err, fn); |
| if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = tmpLimit; |
| |
| overrideStackTrace.set(err, (_, stack) => stack); |
| const call = err.stack[0]; |
| |
| let filename = call.getFileName(); |
| const line = call.getLineNumber() - 1; |
| let column = call.getColumnNumber() - 1; |
| let identifier; |
| let code; |
| |
| if (filename) { |
| identifier = `${filename}${line}${column}`; |
| |
| // Skip Node.js modules! |
| if (StringPrototypeStartsWith(filename, 'node:') && |
| BuiltinModule.exists(StringPrototypeSlice(filename, 5))) { |
| errorCache.set(identifier, undefined); |
| return; |
| } |
| } else { |
| return message; |
| } |
| |
| if (errorCache.has(identifier)) { |
| return errorCache.get(identifier); |
| } |
| |
| let fd; |
| try { |
| // Set the stack trace limit to zero. This makes sure unexpected token |
| // errors are handled faster. |
| if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = 0; |
| |
| if (filename) { |
| if (decoder === undefined) { |
| const { StringDecoder } = require('string_decoder'); |
| decoder = new StringDecoder('utf8'); |
| } |
| |
| // ESM file prop is a file proto. Convert that to path. |
| // This ensure opensync will not throw ENOENT for ESM files. |
| const fileProtoPrefix = 'file://'; |
| if (StringPrototypeStartsWith(filename, fileProtoPrefix)) { |
| filename = fileURLToPath(filename); |
| } |
| |
| fd = openSync(filename, 'r', 0o666); |
| // Reset column and message. |
| ({ 0: column, 1: message } = getCode(fd, line, column)); |
| // Flush unfinished multi byte characters. |
| decoder.end(); |
| } else { |
| for (let i = 0; i < line; i++) { |
| code = StringPrototypeSlice(code, |
| StringPrototypeIndexOf(code, '\n') + 1); |
| } |
| ({ 0: column, 1: message } = parseCode(code, column)); |
| } |
| // Always normalize indentation, otherwise the message could look weird. |
| if (StringPrototypeIncludes(message, '\n')) { |
| if (EOL === '\r\n') { |
| message = RegExpPrototypeSymbolReplace(/\r\n/g, message, '\n'); |
| } |
| const frames = StringPrototypeSplit(message, '\n'); |
| message = ArrayPrototypeShift(frames); |
| for (const frame of frames) { |
| let pos = 0; |
| while (pos < column && (frame[pos] === ' ' || frame[pos] === '\t')) { |
| pos++; |
| } |
| message += `\n ${StringPrototypeSlice(frame, pos)}`; |
| } |
| } |
| message = `The expression evaluated to a falsy value:\n\n ${message}\n`; |
| // Make sure to always set the cache! No matter if the message is |
| // undefined or not |
| errorCache.set(identifier, message); |
| |
| return message; |
| } catch { |
| // Invalidate cache to prevent trying to read this part again. |
| errorCache.set(identifier, undefined); |
| } finally { |
| // Reset limit. |
| if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = tmpLimit; |
| if (fd !== undefined) |
| closeSync(fd); |
| } |
| } |
| |
| function innerOk(fn, argLen, value, message) { |
| if (!value) { |
| let generatedMessage = false; |
| |
| if (argLen === 0) { |
| generatedMessage = true; |
| message = 'No value argument passed to `assert.ok()`'; |
| } else if (message == null) { |
| generatedMessage = true; |
| message = getErrMessage(message, fn); |
| } else if (isError(message)) { |
| throw message; |
| } |
| |
| const err = new AssertionError({ |
| actual: value, |
| expected: true, |
| message, |
| operator: '==', |
| stackStartFn: fn, |
| }); |
| err.generatedMessage = generatedMessage; |
| throw err; |
| } |
| } |
| |
| module.exports = { |
| innerOk, |
| }; |