| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent b632a1d commit d2a7057
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,38 +1,21 @@ | |||
| 1 | 1 | 'use strict'; | |
| 2 | 2 | ||
| 3 | 3 | const { | |
| 4 | - ArrayPrototypeShift, | ||
| 5 | 4 | Error, | |
| 6 | 5 | ErrorCaptureStackTrace, | |
| 7 | - FunctionPrototypeBind, | ||
| 8 | - RegExpPrototypeSymbolReplace, | ||
| 9 | - SafeMap, | ||
| 10 | 6 | StringPrototypeCharCodeAt, | |
| 11 | - StringPrototypeIncludes, | ||
| 12 | 7 | StringPrototypeReplace, | |
| 13 | - StringPrototypeSlice, | ||
| 14 | - StringPrototypeSplit, | ||
| 15 | - StringPrototypeStartsWith, | ||
| 16 | 8 | } = primordials; | |
| 17 | 9 | ||
| 18 | - const { Buffer } = require('buffer'); | ||
| 19 | 10 | const { | |
| 20 | 11 | isErrorStackTraceLimitWritable, | |
| 21 | - overrideStackTrace, | ||
| 22 | 12 | } = require('internal/errors'); | |
| 23 | 13 | const AssertionError = 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'); | ||
| 27 | 14 | const { isError } = require('internal/util'); | |
| 28 | 15 | ||
| 29 | - const errorCache = new SafeMap(); | ||
| 30 | - const { fileURLToPath } = require('internal/url'); | ||
| 31 | - | ||
| 32 | - let parseExpressionAt; | ||
| 33 | - let findNodeAround; | ||
| 34 | - let tokenizer; | ||
| 35 | - let decoder; | ||
| 16 | + const { | ||
| 17 | + getErrorSourceExpression, | ||
| 18 | + } = require('internal/errors/error_source'); | ||
| 36 | 19 | ||
| 37 | 20 | // Escape control characters but not \n and \t to keep the line breaks and | |
| 38 | 21 | // indentation intact. | |
@@ -50,111 +33,7 @@ const meta = [ | |||
| 50 | 33 | ||
| 51 | 34 | const escapeFn = (str) => meta[StringPrototypeCharCodeAt(str, 0)]; | |
| 52 | 35 | ||
| 53 | - function findColumn(fd, column, code) { | ||
| 54 | - if (code.length > column + 100) { | ||
| 55 | - try { | ||
| 56 | - return parseCode(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 | - throw null; | ||
| 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 | - const bytesToRead = column - code.length + 2500; | ||
| 69 | - const buffer = Buffer.allocUnsafe(bytesToRead); | ||
| 70 | - const bytesRead = readSync(fd, buffer, 0, bytesToRead); | ||
| 71 | - code += decoder.write(buffer.slice(0, bytesRead)); | ||
| 72 | - // EOF: fast path. | ||
| 73 | - if (bytesRead < bytesToRead) { | ||
| 74 | - return parseCode(code, column); | ||
| 75 | - } | ||
| 76 | - // Read potentially missing code. | ||
| 77 | - return findColumn(fd, column, code); | ||
| 78 | - } | ||
| 79 | - | ||
| 80 | - function getCode(fd, line, column) { | ||
| 81 | - let bytesRead = 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 | - return findColumn(fd, column, ''); | ||
| 87 | - } | ||
| 88 | - let lines = 0; | ||
| 89 | - // Prevent blocking the event loop by limiting the maximum amount of | ||
| 90 | - // data that may be read. | ||
| 91 | - let maxReads = 32; // bytesPerRead * maxReads = 512 KiB | ||
| 92 | - const bytesPerRead = 16384; | ||
| 93 | - // Use a single buffer up front that is reused until the call site is found. | ||
| 94 | - let buffer = 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 (let i = 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 | - return parseCode(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 | - const code = decoder.write(buffer.slice(i + 1, bytesRead)); | ||
| 111 | - return findColumn(fd, column, code); | ||
| 112 | - } | ||
| 113 | - } | ||
| 114 | - } | ||
| 115 | - } | ||
| 116 | - | ||
| 117 | - function parseCode(code, offset) { | ||
| 118 | - // Lazy load acorn. | ||
| 119 | - if (parseExpressionAt === undefined) { | ||
| 120 | - const Parser = 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 | - let node; | ||
| 127 | - let start; | ||
| 128 | - // Parse the read code until the correct expression is found. | ||
| 129 | - for (const token of tokenizer(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 | - throw null; | ||
| 155 | - } | ||
| 156 | - | ||
| 157 | - function getErrMessage(message, fn) { | ||
| 36 | + function getErrMessage(fn) { | ||
| 158 | 37 | const tmpLimit = Error.stackTraceLimit; | |
| 159 | 38 | const errorStackTraceLimitIsWritable = isErrorStackTraceLimitWritable(); | |
| 160 | 39 | // Make sure the limit is set to 1. Otherwise it could fail (<= 0) or it | |
@@ -166,85 +45,10 @@ function getErrMessage(message, fn) { | |||
| 166 | 45 | ErrorCaptureStackTrace(err, fn); | |
| 167 | 46 | if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = tmpLimit; | |
| 168 | 47 | ||
| 169 | - overrideStackTrace.set(err, (_, stack) => stack); | ||
| 170 | - const call = err.stack[0]; | ||
| 171 | - | ||
| 172 | - let filename = call.getFileName(); | ||
| 173 | - const line = call.getLineNumber() - 1; | ||
| 174 | - let column = call.getColumnNumber() - 1; | ||
| 175 | - let identifier; | ||
| 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 | - return message; | ||
| 188 | - } | ||
| 189 | - | ||
| 190 | - if (errorCache.has(identifier)) { | ||
| 191 | - return errorCache.get(identifier); | ||
| 192 | - } | ||
| 193 | - | ||
| 194 | - let fd; | ||
| 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 = new StringDecoder('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 | - const fileProtoPrefix = '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 | - const frames = StringPrototypeSplit(message, '\n'); | ||
| 224 | - message = ArrayPrototypeShift(frames); | ||
| 225 | - for (let i = 0; i < frames.length; i++) { | ||
| 226 | - const frame = frames[i]; | ||
| 227 | - let pos = 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 | - return message; | ||
| 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 | + let source = getErrorSourceExpression(err); | ||
| 49 | + if (source) { | ||
| 50 | + source = StringPrototypeReplace(source, escapeSequencesRegExp, escapeFn); | ||
| 51 | + return `The expression evaluated to a falsy value:\n\n ${source}\n`; | ||
| 248 | 52 | } | |
| 249 | 53 | } | |
| 250 | 54 | ||
@@ -257,7 +61,7 @@ function innerOk(fn, argLen, value, message) { | |||
| 257 | 61 | message = 'No value argument passed to `assert.ok()`'; | |
| 258 | 62 | } else if (message == null) { | |
| 259 | 63 | generatedMessage = true; | |
| 260 | - message = getErrMessage(message, fn); | ||
| 64 | + message = getErrMessage(fn); | ||
| 261 | 65 | } else if (isError(message)) { | |
| 262 | 66 | throw message; | |
| 263 | 67 | } | |
| Original file line number | Diff line number | Diff 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 | + function getErrorSourceLocation(error) { | ||
| 21 | + const pos = getErrorSourcePositions(error); | ||
| 22 | + const { | ||
| 23 | + sourceLine, | ||
| 24 | + startColumn, | ||
| 25 | + } = pos; | ||
| 26 | + | ||
| 27 | + return { sourceLine, startColumn }; | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + const memberAccessTokens = [ '.', '?.', '[', ']' ]; | ||
| 31 | + const memberNameTokens = [ 'name', 'string', 'num' ]; | ||
| 32 | + let tokenizer; | ||
| 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 | + function getFirstExpression(code, startColumn) { | ||
| 40 | + // Lazy load acorn. | ||
| 41 | + if (tokenizer === undefined) { | ||
| 42 | + const Parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; | ||
| 43 | + tokenizer = FunctionPrototypeBind(Parser.tokenizer, Parser); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + let lastToken; | ||
| 47 | + let firstMemberAccessNameToken; | ||
| 48 | + let terminatingCol; | ||
| 49 | + let parenLvl = 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 (const token of tokenizer(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 | + } else if (!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 | + const start = firstMemberAccessNameToken?.start ?? startColumn; | ||
| 110 | + return StringPrototypeSlice(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 | + function getErrorSourceExpression(error) { | ||
| 122 | + const loc = getErrorSourceLocation(error); | ||
| 123 | + if (loc === undefined) { | ||
| 124 | + return; | ||
| 125 | + } | ||
| 126 | + const { sourceLine, startColumn } = loc; | ||
| 127 | + return getFirstExpression(sourceLine, startColumn); | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + module.exports = { | ||
| 131 | + getErrorSourceLocation, | ||
| 132 | + getErrorSourceExpression, | ||
| 133 | + }; | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments