GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — lib,src: refactor assert to load error source from memory · nodejs/node@b200cd8

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • lib/internal
    • assert
      • utils.js
    • errors
      • error_source.js
  • src
    • node_errors.cc
  • test/parallel
    • test-assert-builtins-not-read-from-filesystem.js
    • test-assert.js
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎lib/internal/assert/utils.js‎

Copy file name to clipboardExpand all lines: lib/internal/assert/utils.js
+9-205Lines changed: 9 additions & 205 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,21 @@
11
'use strict';
22

33
const {
4-
ArrayPrototypeShift,
54
Error,
65
ErrorCaptureStackTrace,
7-
FunctionPrototypeBind,
8-
RegExpPrototypeSymbolReplace,
9-
SafeMap,
106
StringPrototypeCharCodeAt,
11-
StringPrototypeIncludes,
127
StringPrototypeReplace,
13-
StringPrototypeSlice,
14-
StringPrototypeSplit,
15-
StringPrototypeStartsWith,
168
} = primordials;
179

18-
const { Buffer } = require('buffer');
1910
const {
2011
isErrorStackTraceLimitWritable,
21-
overrideStackTrace,
2212
} = require('internal/errors');
2313
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');
2714
const { isError } = require('internal/util');
2815

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');
3619

3720
// Escape control characters but not \n and \t to keep the line breaks and
3821
// indentation intact.
@@ -50,111 +33,7 @@ const meta = [
5033

5134
const escapeFn = (str) => meta[StringPrototypeCharCodeAt(str, 0)];
5235

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) {
15837
const tmpLimit = Error.stackTraceLimit;
15938
const errorStackTraceLimitIsWritable = isErrorStackTraceLimitWritable();
16039
// Make sure the limit is set to 1. Otherwise it could fail (<= 0) or it
@@ -166,85 +45,10 @@ function getErrMessage(message, fn) {
16645
ErrorCaptureStackTrace(err, fn);
16746
if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = tmpLimit;
16847

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`;
24852
}
24953
}
25054

@@ -257,7 +61,7 @@ function innerOk(fn, argLen, value, message) {
25761
message = 'No value argument passed to `assert.ok()`';
25862
} else if (message == null) {
25963
generatedMessage = true;
260-
message = getErrMessage(message, fn);
64+
message = getErrMessage(fn);
26165
} else if (isError(message)) {
26266
throw message;
26367
}
Collapse file

‎lib/internal/errors/error_source.js‎

Copy file name to clipboard
+133Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff 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+
};

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.