File size: 13,294 Bytes
443c22e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
/* eslint no-param-reassign: 0 -- stylistic choice */

import TokenTranslator from "./token-translator.js";
import { normalizeOptions } from "./options.js";

/**
 * @import {
 *   CommentType,
 *   EspreeParserCtor,
 *   EsprimaNode,
 *   AcornJsxParserCtorEnhanced,
 *   TokTypes
 * } from "./types.js";
 * @import {
 *   Options,
 *   EspreeToken as EsprimaToken,
 *   EspreeTokens as EsprimaTokens,
 *   EspreeComment as EsprimaComment
 * } from "../espree.js";
 * @import { NormalizedEcmaVersion } from "./options.js";
 * @import * as acorn from "acorn";
 */

/**
 * @typedef {{
 *   originalSourceType: "script" | "module" | "commonjs" | undefined
 *   tokens: EsprimaToken[] | null,
 *   comments: EsprimaComment[] | null,
 *   impliedStrict: boolean,
 *   ecmaVersion: NormalizedEcmaVersion,
 *   jsxAttrValueToken: boolean,
 *   lastToken: acorn.Token | null,
 *   templateElements: acorn.TemplateElement[]
 * }} State
 */

/**
 * @typedef {{
 *   sourceType?: "script"|"module"|"commonjs";
 *   comments?: EsprimaComment[];
 *   tokens?: EsprimaToken[];
 *   body: acorn.Node[];
 * } & acorn.Program} EsprimaProgramNode
 */

// ----------------------------------------------------------------------------
// Types exported from file
// ----------------------------------------------------------------------------
/**
 * @typedef {{
 *   index?: number;
 *   lineNumber?: number;
 *   column?: number;
 * } & SyntaxError} EnhancedSyntaxError
 */

// We add `jsxAttrValueToken` ourselves.
/**
 * @typedef {{
 *   jsxAttrValueToken?: acorn.TokenType;
 * } & TokTypes} EnhancedTokTypes
 */

const STATE = Symbol("espree's internal state");
const ESPRIMA_FINISH_NODE = Symbol("espree's esprimaFinishNode");

/**
 * Converts an Acorn comment to a Esprima comment.
 * @param {boolean} block True if it's a block comment, false if not.
 * @param {string} text The text of the comment.
 * @param {number} start The index at which the comment starts.
 * @param {number} end The index at which the comment ends.
 * @param {acorn.Position | undefined} startLoc The location at which the comment starts.
 * @param {acorn.Position | undefined} endLoc The location at which the comment ends.
 * @param {string} code The source code being parsed.
 * @returns {EsprimaComment} The comment object.
 * @private
 */
function convertAcornCommentToEsprimaComment(
	block,
	text,
	start,
	end,
	startLoc,
	endLoc,
	code,
) {
	/** @type {CommentType} */
	let type;

	if (block) {
		type = "Block";
	} else if (code.slice(start, start + 2) === "#!") {
		type = "Hashbang";
	} else {
		type = "Line";
	}

	/**
	 * @type {{
	 *   type: CommentType,
	 *   value: string,
	 *   start?: number,
	 *   end?: number,
	 *   range?: [number, number],
	 *   loc?: {
	 *     start: acorn.Position | undefined,
	 *     end: acorn.Position | undefined
	 *   }
	 * }}
	 */
	const comment = {
		type,
		value: text,
	};

	if (typeof start === "number") {
		comment.start = start;
		comment.end = end;
		comment.range = [start, end];
	}

	if (typeof startLoc === "object") {
		comment.loc = {
			start: startLoc,
			end: endLoc,
		};
	}

	return comment;
}

// eslint-disable-next-line arrow-body-style -- For TS
export default () => {
	/**
	 * Returns the Espree parser.
	 * @param {AcornJsxParserCtorEnhanced} Parser The Acorn parser. The `acorn` property is missing from acorn's
	 *   TypeScript but is present statically on the class.
	 * @returns {EspreeParserCtor} The Espree Parser constructor.
	 */
	return Parser => {
		const tokTypes = /** @type {EnhancedTokTypes} */ (
			Object.assign({}, Parser.acorn.tokTypes)
		);

		if (Parser.acornJsx) {
			Object.assign(tokTypes, Parser.acornJsx.tokTypes);
		}

		return class Espree extends Parser {
			/**
			 * @param {Options | null | undefined} opts The parser options
			 * @param {string | object} code The code which will be converted to a string.
			 */
			constructor(opts, code) {
				if (typeof opts !== "object" || opts === null) {
					opts = {};
				}
				if (typeof code !== "string" && !(code instanceof String)) {
					code = String(code);
				}

				// save original source type in case of commonjs
				const originalSourceType = opts.sourceType;
				const options = normalizeOptions(opts);
				const ecmaFeatures = options.ecmaFeatures || {};
				const tokenTranslator =
					options.tokens === true
						? new TokenTranslator(
								tokTypes,

								// @ts-expect-error Appears to be a TS bug since the type is indeed string|String
								code,
							)
						: null;

				/**
				 * Data that is unique to Espree and is not represented internally
				 * in Acorn.
				 *
				 * For ES2023 hashbangs, Espree will call `onComment()` during the
				 * constructor, so we must define state before having access to
				 * `this`.
				 * @type {State}
				 */
				const state = {
					originalSourceType:
						originalSourceType || options.sourceType,
					tokens: tokenTranslator ? [] : null,
					comments: options.comment === true ? [] : null,
					impliedStrict:
						ecmaFeatures.impliedStrict === true &&
						options.ecmaVersion >= 5,
					ecmaVersion: options.ecmaVersion,
					jsxAttrValueToken: false,
					lastToken: null,
					templateElements: [],
				};

				// Initialize acorn parser.
				super(
					{
						// do not use spread, because we don't want to pass any unknown options to acorn
						ecmaVersion: options.ecmaVersion,
						sourceType: options.sourceType,
						ranges: options.ranges,
						locations: options.locations,
						allowReserved: options.allowReserved,

						// Truthy value is true for backward compatibility.
						allowReturnOutsideFunction:
							options.allowReturnOutsideFunction,

						// Collect tokens
						onToken(token) {
							if (tokenTranslator) {
								// Use `tokens`, `ecmaVersion`, and `jsxAttrValueToken` in the state.
								tokenTranslator.onToken(
									token,

									/**
									 * @type {Omit<State, "tokens"> & {
									 *   tokens: EsprimaToken[]
									 * }}
									 */
									(state),
								);
							}
							if (token.type !== tokTypes.eof) {
								state.lastToken = token;
							}
						},

						// Collect comments
						onComment(block, text, start, end, startLoc, endLoc) {
							if (state.comments) {
								const comment =
									convertAcornCommentToEsprimaComment(
										block,
										text,
										start,
										end,
										startLoc,
										endLoc,

										// @ts-expect-error Appears to be a TS bug
										//   since the type is indeed string|String
										code,
									);

								state.comments.push(comment);
							}
						},
					},
					// @ts-expect-error Appears to be a TS bug
					//   since the type is indeed string|String
					code,
				);

				/*
				 * We put all of this data into a symbol property as a way to avoid
				 * potential naming conflicts with future versions of Acorn.
				 */
				this[STATE] = state;
			}

			/**
			 * Returns Espree tokens.
			 * @returns {EsprimaTokens} The Esprima-compatible tokens
			 */
			tokenize() {
				do {
					this.next();
				} while (this.type !== tokTypes.eof);

				// Consume the final eof token
				this.next();

				const extra = this[STATE];
				const tokens = /** @type {EsprimaTokens} */ (extra.tokens);

				if (extra.comments) {
					tokens.comments = extra.comments;
				}

				return tokens;
			}

			/**
			 * Calls parent.
			 * @param {acorn.Node} node The node
			 * @param {string} type The type
			 * @returns {acorn.Node} The altered Node
			 */
			finishNode(node, type) {
				const result = super.finishNode(node, type);

				return this[ESPRIMA_FINISH_NODE](result);
			}

			/**
			 * Calls parent.
			 * @param {acorn.Node} node The node
			 * @param {string} type The type
			 * @param {number} pos The position
			 * @param {acorn.Position} loc The location
			 * @returns {acorn.Node} The altered Node
			 */
			finishNodeAt(node, type, pos, loc) {
				const result = super.finishNodeAt(node, type, pos, loc);

				return this[ESPRIMA_FINISH_NODE](result);
			}

			/**
			 * Parses.
			 * @returns {EsprimaProgramNode} The program Node
			 */
			parse() {
				const extra = this[STATE];
				const prog = super.parse();

				const program = /** @type {EsprimaProgramNode} */ (prog);

				// @ts-expect-error TS bug? We've already converted to `EsprimaProgramNode`
				program.sourceType = extra.originalSourceType;

				if (extra.comments) {
					program.comments = extra.comments;
				}
				if (extra.tokens) {
					program.tokens = extra.tokens;
				}

				/*
				 * https://github.com/eslint/espree/issues/349
				 * Ensure that template elements have correct range information.
				 * This is one location where Acorn produces a different value
				 * for its start and end properties vs. the values present in the
				 * range property. In order to avoid confusion, we set the start
				 * and end properties to the values that are present in range.
				 * This is done here, instead of in finishNode(), because Acorn
				 * uses the values of start and end internally while parsing, making
				 * it dangerous to change those values while parsing is ongoing.
				 * By waiting until the end of parsing, we can safely change these
				 * values without affect any other part of the process.
				 */
				this[STATE].templateElements.forEach(templateElement => {
					const startOffset = -1;
					const endOffset = templateElement.tail ? 1 : 2;

					templateElement.start += startOffset;
					templateElement.end += endOffset;

					if (templateElement.range) {
						templateElement.range[0] += startOffset;
						templateElement.range[1] += endOffset;
					}

					if (templateElement.loc) {
						templateElement.loc.start.column += startOffset;
						templateElement.loc.end.column += endOffset;
					}
				});

				return program;
			}

			/**
			 * Parses top level.
			 * @param {acorn.Node} node AST Node
			 * @returns {acorn.Node} The changed node
			 */
			parseTopLevel(node) {
				if (this[STATE].impliedStrict) {
					this.strict = true;
				}
				return super.parseTopLevel(node);
			}

			/**
			 * Overwrites the default raise method to throw Esprima-style errors.
			 * @param {number} pos The position of the error.
			 * @param {string} message The error message.
			 * @throws {EnhancedSyntaxError} A syntax error.
			 * @returns {void}
			 */
			raise(pos, message) {
				const loc = Parser.acorn.getLineInfo(this.input, pos);
				const err = /** @type {EnhancedSyntaxError} */ (
					new SyntaxError(message)
				);

				err.index = pos;
				err.lineNumber = loc.line;
				err.column = loc.column + 1; // acorn uses 0-based columns
				throw err;
			}

			/**
			 * Overwrites the default raise method to throw Esprima-style errors.
			 * @param {number} pos The position of the error.
			 * @param {string} message The error message.
			 * @throws {SyntaxError} A syntax error.
			 * @returns {void}
			 */
			raiseRecoverable(pos, message) {
				this.raise(pos, message);
			}

			/**
			 * Overwrites the default unexpected method to throw Esprima-style errors.
			 * @param {number} pos The position of the error.
			 * @throws {SyntaxError} A syntax error.
			 * @returns {void}
			 */
			unexpected(pos) {
				let message = "Unexpected token";

				if (pos !== null && pos !== void 0) {
					this.pos = pos;

					if (this.options.locations) {
						while (this.pos < this.lineStart) {
							this.lineStart =
								this.input.lastIndexOf(
									"\n",
									this.lineStart - 2,
								) + 1;
							--this.curLine;
						}
					}

					this.nextToken();
				}

				if (this.end > this.start) {
					message += ` ${this.input.slice(this.start, this.end)}`;
				}

				this.raise(this.start, message);
			}

			/**
			 * Esprima-FB represents JSX strings as tokens called "JSXText", but Acorn-JSX
			 * uses regular tt.string without any distinction between this and regular JS
			 * strings. As such, we intercept an attempt to read a JSX string and set a flag
			 * on extra so that when tokens are converted, the next token will be switched
			 * to JSXText via onToken.
			 * @param {number} quote A character code
			 * @returns {void}
			 */ // eslint-disable-next-line camelcase -- required by API
			jsx_readString(quote) {
				const result = super.jsx_readString(quote);

				if (this.type === tokTypes.string) {
					this[STATE].jsxAttrValueToken = true;
				}
				return result;
			}

			/**
			 * Performs last-minute Esprima-specific compatibility checks and fixes.
			 * @param {acorn.Node} result The node to check.
			 * @returns {EsprimaNode} The finished node.
			 */
			[ESPRIMA_FINISH_NODE](result) {
				// Acorn doesn't count the opening and closing backticks as part of templates
				// so we have to adjust ranges/locations appropriately.
				if (result.type === "TemplateElement") {
					// save template element references to fix start/end later
					this[STATE].templateElements.push(
						/** @type {acorn.TemplateElement} */
						(result),
					);
				}

				if (
					result.type.includes("Function") &&
					!("generator" in result)
				) {
					/**
					 * @type {acorn.FunctionDeclaration|acorn.FunctionExpression|
					 *   acorn.ArrowFunctionExpression}
					 */
					(result).generator = false;
				}

				return result;
			}
		};
	};
};