-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
Expand file tree
/
Copy patherror_source.js
More file actions
245 lines (225 loc) Β· 7.72 KB
/
error_source.js
File metadata and controls
245 lines (225 loc) Β· 7.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
'use strict';
const {
FunctionPrototypeBind,
MathMax,
MathMin,
NumberIsFinite,
RegExpPrototypeSymbolReplace,
StringPrototypeRepeat,
StringPrototypeSlice,
} = primordials;
const {
getErrorSourcePositions,
} = internalBinding('errors');
const {
getSourceMapsSupport,
findSourceMap,
getSourceLine,
} = require('internal/source_map/source_map_cache');
const kSourceLineMaxLength = 120;
const kSourceLineContext = 40;
const kLineEllipsis = '...';
function createSourceUnderline(sourceLine, startColumn, underlineLength) {
const prefix = RegExpPrototypeSymbolReplace(
/[^\t]/g, StringPrototypeSlice(sourceLine, 0, startColumn), ' ');
return prefix + StringPrototypeRepeat('^', underlineLength);
}
function clipSourceLine(sourceLine, startColumn, underlineLength) {
if (sourceLine.length <= kSourceLineMaxLength) {
return {
sourceLine,
startColumn,
underlineLength,
};
}
const targetEnd = startColumn + underlineLength;
const windowStart = MathMax(0, startColumn - kSourceLineContext);
const windowEnd = MathMin(
sourceLine.length,
windowStart + kSourceLineMaxLength,
targetEnd + kSourceLineContext,
);
const leftEllipsis = windowStart > 0 ? kLineEllipsis : '';
const rightEllipsis = windowEnd < sourceLine.length ? kLineEllipsis : '';
const clippedLine = leftEllipsis +
StringPrototypeSlice(sourceLine, windowStart, windowEnd) +
rightEllipsis;
const clippedStartColumn = leftEllipsis.length + startColumn - windowStart;
const clippedUnderlineLength = MathMax(
1,
MathMin(underlineLength, windowEnd - startColumn),
);
return {
sourceLine: clippedLine,
startColumn: clippedStartColumn,
underlineLength: clippedUnderlineLength,
};
}
/**
* Format a source line with a caret underline for an error message.
* @param {string} filename The file containing the source line.
* @param {number} lineNumber The 1-based line number.
* @param {string} sourceLine The source line text.
* @param {number} startColumn The 0-based underline start column.
* @param {number} underlineLength The underline length.
* @returns {string|undefined}
*/
function getErrorSourceMessage(filename, lineNumber, sourceLine, startColumn, underlineLength) {
if (typeof sourceLine !== 'string' ||
!NumberIsFinite(lineNumber) ||
!NumberIsFinite(startColumn) ||
!NumberIsFinite(underlineLength)) {
return;
}
startColumn = MathMax(0, startColumn);
underlineLength = MathMax(1, underlineLength);
const clipped = clipSourceLine(sourceLine, startColumn, underlineLength);
const arrow = createSourceUnderline(
clipped.sourceLine,
clipped.startColumn,
clipped.underlineLength,
);
return `${filename}:${lineNumber}\n${clipped.sourceLine}\n${arrow}\n`;
}
/**
* Get the source location of an error. If source map is enabled, resolve the source location
* based on the source map.
*
* The `error.stack` must not have been accessed. The resolution is based on the structured
* error stack data.
* @param {Error|object} error An error object, or an object being invoked with ErrorCaptureStackTrace
* @returns {{sourceLine: string, startColumn: number}|undefined}
*/
function getErrorSourceLocation(error) {
const pos = getErrorSourcePositions(error);
const {
sourceLine,
scriptResourceName,
lineNumber,
startColumn,
} = pos;
// Source map is not enabled. Return the source line directly.
if (!getSourceMapsSupport().enabled) {
return { sourceLine, startColumn };
}
const sm = findSourceMap(scriptResourceName);
if (sm === undefined) {
return;
}
const {
originalLine,
originalColumn,
originalSource,
} = sm.findEntry(lineNumber - 1, startColumn);
const originalSourceLine = getSourceLine(sm, originalSource, originalLine, originalColumn);
if (!originalSourceLine) {
return;
}
return {
sourceLine: originalSourceLine,
startColumn: originalColumn,
};
}
const memberAccessTokens = [ '.', '?.', '[', ']' ];
const memberNameTokens = [ 'name', 'string', 'num' ];
let tokenizer;
/**
* Get the first expression in a code string at the startColumn.
* @param {string} code source code line
* @param {number} startColumn which column the error is constructed
* @returns {string}
*/
function getFirstExpression(code, startColumn) {
// Lazy load acorn.
if (tokenizer === undefined) {
const Parser = require('internal/deps/acorn/acorn/dist/acorn').Parser;
tokenizer = FunctionPrototypeBind(Parser.tokenizer, Parser);
}
let lastToken;
let firstMemberAccessNameToken;
let terminatingCol;
let parenLvl = 0;
// Tokenize the line to locate the expression at the startColumn.
// The source line may be an incomplete JavaScript source, so do not parse the source line.
for (const token of tokenizer(code, { ecmaVersion: 'latest' })) {
// Peek before the startColumn.
if (token.start < startColumn) {
// There is a semicolon. This is a statement before the startColumn, so reset the memo.
if (token.type.label === ';') {
firstMemberAccessNameToken = null;
continue;
}
// Try to memo the member access expressions before the startColumn, so that the
// returned source code contains more info:
// assert.ok(value)
// ^ startColumn
// The member expression can also be like
// assert['ok'](value) or assert?.ok(value)
// ^ startColumn ^ startColumn
if (memberAccessTokens.includes(token.type.label) && lastToken?.type.label === 'name') {
// First member access name token must be a 'name'.
firstMemberAccessNameToken ??= lastToken;
} else if (!memberAccessTokens.includes(token.type.label) &&
!memberNameTokens.includes(token.type.label)) {
// Reset the memo if it is not a simple member access.
// For example: assert[(() => 'ok')()](value)
// ^ startColumn
firstMemberAccessNameToken = null;
}
lastToken = token;
continue;
}
// Now after the startColumn, this must be an expression.
if (token.type.label === '(') {
parenLvl++;
continue;
}
if (token.type.label === ')') {
parenLvl--;
if (parenLvl === 0) {
// A matched closing parenthesis found after the startColumn,
// terminate here. Include the token.
// (assert.ok(false), assert.ok(true))
// ^ startColumn
terminatingCol = token.start + 1;
break;
}
continue;
}
if (token.type.label === ';') {
// A semicolon found after the startColumn, terminate here.
// assert.ok(false); assert.ok(true));
// ^ startColumn
terminatingCol = token;
break;
}
// If no semicolon found after the startColumn. The string after the
// startColumn must be the expression.
// assert.ok(false)
// ^ startColumn
}
const start = firstMemberAccessNameToken?.start ?? startColumn;
return StringPrototypeSlice(code, start, terminatingCol);
}
/**
* Get the source expression of an error. If source map is enabled, resolve the source location
* based on the source map.
*
* The `error.stack` must not have been accessed, or the source location may be incorrect. The
* resolution is based on the structured error stack data.
* @param {Error|object} error An error object, or an object being invoked with ErrorCaptureStackTrace
* @returns {string|undefined}
*/
function getErrorSourceExpression(error) {
const loc = getErrorSourceLocation(error);
if (loc === undefined) {
return;
}
const { sourceLine, startColumn } = loc;
return getFirstExpression(sourceLine, startColumn);
}
module.exports = {
getErrorSourceLocation,
getErrorSourceExpression,
getErrorSourceMessage,
};