1 | /* eslint-disable no-param-reassign*/
|
2 | import TokenTranslator from "./token-translator.js";
|
3 | import { normalizeOptions } from "./options.js";
|
4 |
|
5 |
|
6 | const STATE = Symbol("espree's internal state");
|
7 | const ESPRIMA_FINISH_NODE = Symbol("espree's esprimaFinishNode");
|
8 |
|
9 |
|
10 | /**
|
11 | * Converts an Acorn comment to a Esprima comment.
|
12 | * @param {boolean} block True if it's a block comment, false if not.
|
13 | * @param {string} text The text of the comment.
|
14 | * @param {int} start The index at which the comment starts.
|
15 | * @param {int} end The index at which the comment ends.
|
16 | * @param {Location} startLoc The location at which the comment starts.
|
17 | * @param {Location} endLoc The location at which the comment ends.
|
18 | * @returns {Object} The comment object.
|
19 | * @private
|
20 | */
|
21 | function convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc) {
|
22 | const comment = {
|
23 | type: block ? "Block" : "Line",
|
24 | value: text
|
25 | };
|
26 |
|
27 | if (typeof start === "number") {
|
28 | comment.start = start;
|
29 | comment.end = end;
|
30 | comment.range = [start, end];
|
31 | }
|
32 |
|
33 | if (typeof startLoc === "object") {
|
34 | comment.loc = {
|
35 | start: startLoc,
|
36 | end: endLoc
|
37 | };
|
38 | }
|
39 |
|
40 | return comment;
|
41 | }
|
42 |
|
43 | export default () => Parser => {
|
44 | const tokTypes = Object.assign({}, Parser.acorn.tokTypes);
|
45 |
|
46 | if (Parser.acornJsx) {
|
47 | Object.assign(tokTypes, Parser.acornJsx.tokTypes);
|
48 | }
|
49 |
|
50 | return class Espree extends Parser {
|
51 | constructor(opts, code) {
|
52 | if (typeof opts !== "object" || opts === null) {
|
53 | opts = {};
|
54 | }
|
55 | if (typeof code !== "string" && !(code instanceof String)) {
|
56 | code = String(code);
|
57 | }
|
58 |
|
59 | // save original source type in case of commonjs
|
60 | const originalSourceType = opts.sourceType;
|
61 | const options = normalizeOptions(opts);
|
62 | const ecmaFeatures = options.ecmaFeatures || {};
|
63 | const tokenTranslator =
|
64 | options.tokens === true
|
65 | ? new TokenTranslator(tokTypes, code)
|
66 | : null;
|
67 |
|
68 | // Initialize acorn parser.
|
69 | super({
|
70 |
|
71 | // do not use spread, because we don't want to pass any unknown options to acorn
|
72 | ecmaVersion: options.ecmaVersion,
|
73 | sourceType: options.sourceType,
|
74 | ranges: options.ranges,
|
75 | locations: options.locations,
|
76 | allowReserved: options.allowReserved,
|
77 |
|
78 | // Truthy value is true for backward compatibility.
|
79 | allowReturnOutsideFunction: options.allowReturnOutsideFunction,
|
80 |
|
81 | // Collect tokens
|
82 | onToken: token => {
|
83 | if (tokenTranslator) {
|
84 |
|
85 | // Use `tokens`, `ecmaVersion`, and `jsxAttrValueToken` in the state.
|
86 | tokenTranslator.onToken(token, this[STATE]);
|
87 | }
|
88 | if (token.type !== tokTypes.eof) {
|
89 | this[STATE].lastToken = token;
|
90 | }
|
91 | },
|
92 |
|
93 | // Collect comments
|
94 | onComment: (block, text, start, end, startLoc, endLoc) => {
|
95 | if (this[STATE].comments) {
|
96 | const comment = convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc);
|
97 |
|
98 | this[STATE].comments.push(comment);
|
99 | }
|
100 | }
|
101 | }, code);
|
102 |
|
103 | /*
|
104 | * Data that is unique to Espree and is not represented internally in
|
105 | * Acorn. We put all of this data into a symbol property as a way to
|
106 | * avoid potential naming conflicts with future versions of Acorn.
|
107 | */
|
108 | this[STATE] = {
|
109 | originalSourceType: originalSourceType || options.sourceType,
|
110 | tokens: tokenTranslator ? [] : null,
|
111 | comments: options.comment === true ? [] : null,
|
112 | impliedStrict: ecmaFeatures.impliedStrict === true && this.options.ecmaVersion >= 5,
|
113 | ecmaVersion: this.options.ecmaVersion,
|
114 | jsxAttrValueToken: false,
|
115 | lastToken: null,
|
116 | templateElements: []
|
117 | };
|
118 | }
|
119 |
|
120 | tokenize() {
|
121 | do {
|
122 | this.next();
|
123 | } while (this.type !== tokTypes.eof);
|
124 |
|
125 | // Consume the final eof token
|
126 | this.next();
|
127 |
|
128 | const extra = this[STATE];
|
129 | const tokens = extra.tokens;
|
130 |
|
131 | if (extra.comments) {
|
132 | tokens.comments = extra.comments;
|
133 | }
|
134 |
|
135 | return tokens;
|
136 | }
|
137 |
|
138 | finishNode(...args) {
|
139 | const result = super.finishNode(...args);
|
140 |
|
141 | return this[ESPRIMA_FINISH_NODE](result);
|
142 | }
|
143 |
|
144 | finishNodeAt(...args) {
|
145 | const result = super.finishNodeAt(...args);
|
146 |
|
147 | return this[ESPRIMA_FINISH_NODE](result);
|
148 | }
|
149 |
|
150 | parse() {
|
151 | const extra = this[STATE];
|
152 | const program = super.parse();
|
153 |
|
154 | program.sourceType = extra.originalSourceType;
|
155 |
|
156 | if (extra.comments) {
|
157 | program.comments = extra.comments;
|
158 | }
|
159 | if (extra.tokens) {
|
160 | program.tokens = extra.tokens;
|
161 | }
|
162 |
|
163 | /*
|
164 | * Adjust opening and closing position of program to match Esprima.
|
165 | * Acorn always starts programs at range 0 whereas Esprima starts at the
|
166 | * first AST node's start (the only real difference is when there's leading
|
167 | * whitespace or leading comments). Acorn also counts trailing whitespace
|
168 | * as part of the program whereas Esprima only counts up to the last token.
|
169 | */
|
170 | if (program.body.length) {
|
171 | const [firstNode] = program.body;
|
172 |
|
173 | if (program.range) {
|
174 | program.range[0] = firstNode.range[0];
|
175 | }
|
176 | if (program.loc) {
|
177 | program.loc.start = firstNode.loc.start;
|
178 | }
|
179 | program.start = firstNode.start;
|
180 | }
|
181 | if (extra.lastToken) {
|
182 | if (program.range) {
|
183 | program.range[1] = extra.lastToken.range[1];
|
184 | }
|
185 | if (program.loc) {
|
186 | program.loc.end = extra.lastToken.loc.end;
|
187 | }
|
188 | program.end = extra.lastToken.end;
|
189 | }
|
190 |
|
191 |
|
192 | /*
|
193 | * https://github.com/eslint/espree/issues/349
|
194 | * Ensure that template elements have correct range information.
|
195 | * This is one location where Acorn produces a different value
|
196 | * for its start and end properties vs. the values present in the
|
197 | * range property. In order to avoid confusion, we set the start
|
198 | * and end properties to the values that are present in range.
|
199 | * This is done here, instead of in finishNode(), because Acorn
|
200 | * uses the values of start and end internally while parsing, making
|
201 | * it dangerous to change those values while parsing is ongoing.
|
202 | * By waiting until the end of parsing, we can safely change these
|
203 | * values without affect any other part of the process.
|
204 | */
|
205 | this[STATE].templateElements.forEach(templateElement => {
|
206 | const startOffset = -1;
|
207 | const endOffset = templateElement.tail ? 1 : 2;
|
208 |
|
209 | templateElement.start += startOffset;
|
210 | templateElement.end += endOffset;
|
211 |
|
212 | if (templateElement.range) {
|
213 | templateElement.range[0] += startOffset;
|
214 | templateElement.range[1] += endOffset;
|
215 | }
|
216 |
|
217 | if (templateElement.loc) {
|
218 | templateElement.loc.start.column += startOffset;
|
219 | templateElement.loc.end.column += endOffset;
|
220 | }
|
221 | });
|
222 |
|
223 | return program;
|
224 | }
|
225 |
|
226 | parseTopLevel(node) {
|
227 | if (this[STATE].impliedStrict) {
|
228 | this.strict = true;
|
229 | }
|
230 | return super.parseTopLevel(node);
|
231 | }
|
232 |
|
233 | /**
|
234 | * Overwrites the default raise method to throw Esprima-style errors.
|
235 | * @param {int} pos The position of the error.
|
236 | * @param {string} message The error message.
|
237 | * @throws {SyntaxError} A syntax error.
|
238 | * @returns {void}
|
239 | */
|
240 | raise(pos, message) {
|
241 | const loc = Parser.acorn.getLineInfo(this.input, pos);
|
242 | const err = new SyntaxError(message);
|
243 |
|
244 | err.index = pos;
|
245 | err.lineNumber = loc.line;
|
246 | err.column = loc.column + 1; // acorn uses 0-based columns
|
247 | throw err;
|
248 | }
|
249 |
|
250 | /**
|
251 | * Overwrites the default raise method to throw Esprima-style errors.
|
252 | * @param {int} pos The position of the error.
|
253 | * @param {string} message The error message.
|
254 | * @throws {SyntaxError} A syntax error.
|
255 | * @returns {void}
|
256 | */
|
257 | raiseRecoverable(pos, message) {
|
258 | this.raise(pos, message);
|
259 | }
|
260 |
|
261 | /**
|
262 | * Overwrites the default unexpected method to throw Esprima-style errors.
|
263 | * @param {int} pos The position of the error.
|
264 | * @throws {SyntaxError} A syntax error.
|
265 | * @returns {void}
|
266 | */
|
267 | unexpected(pos) {
|
268 | let message = "Unexpected token";
|
269 |
|
270 | if (pos !== null && pos !== void 0) {
|
271 | this.pos = pos;
|
272 |
|
273 | if (this.options.locations) {
|
274 | while (this.pos < this.lineStart) {
|
275 | this.lineStart = this.input.lastIndexOf("\n", this.lineStart - 2) + 1;
|
276 | --this.curLine;
|
277 | }
|
278 | }
|
279 |
|
280 | this.nextToken();
|
281 | }
|
282 |
|
283 | if (this.end > this.start) {
|
284 | message += ` ${this.input.slice(this.start, this.end)}`;
|
285 | }
|
286 |
|
287 | this.raise(this.start, message);
|
288 | }
|
289 |
|
290 | /*
|
291 | * Esprima-FB represents JSX strings as tokens called "JSXText", but Acorn-JSX
|
292 | * uses regular tt.string without any distinction between this and regular JS
|
293 | * strings. As such, we intercept an attempt to read a JSX string and set a flag
|
294 | * on extra so that when tokens are converted, the next token will be switched
|
295 | * to JSXText via onToken.
|
296 | */
|
297 | jsx_readString(quote) { // eslint-disable-line camelcase
|
298 | const result = super.jsx_readString(quote);
|
299 |
|
300 | if (this.type === tokTypes.string) {
|
301 | this[STATE].jsxAttrValueToken = true;
|
302 | }
|
303 | return result;
|
304 | }
|
305 |
|
306 | /**
|
307 | * Performs last-minute Esprima-specific compatibility checks and fixes.
|
308 | * @param {ASTNode} result The node to check.
|
309 | * @returns {ASTNode} The finished node.
|
310 | */
|
311 | [ESPRIMA_FINISH_NODE](result) {
|
312 |
|
313 | // Acorn doesn't count the opening and closing backticks as part of templates
|
314 | // so we have to adjust ranges/locations appropriately.
|
315 | if (result.type === "TemplateElement") {
|
316 |
|
317 | // save template element references to fix start/end later
|
318 | this[STATE].templateElements.push(result);
|
319 | }
|
320 |
|
321 | if (result.type.includes("Function") && !result.generator) {
|
322 | result.generator = false;
|
323 | }
|
324 |
|
325 | return result;
|
326 | }
|
327 | };
|
328 | };
|