lib,src: refactor assert to load error source from memory · nodejs/node@b200cd8

11

'use strict';

2233

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;

17918-

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

281529-

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

36193720

// Escape control characters but not \n and \t to keep the line breaks and

3821

// indentation intact.

@@ -50,111 +33,7 @@ const meta = [

50335134

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

523553-

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;

16847169-

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

}