|
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 | } |
|