UNPKG

11.6 kBJavaScriptView Raw
1/* eslint-disable no-param-reassign*/
2import TokenTranslator from "./token-translator.js";
3import { normalizeOptions } from "./options.js";
4
5
6const STATE = Symbol("espree's internal state");
7const 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 */
21function 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
43export 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};