UNPKG

20.5 kBJavaScriptView Raw
1/**
2 * @fileoverview Main Espree file that converts Acorn into Esprima output.
3 *
4 * This file contains code from the following MIT-licensed projects:
5 * 1. Acorn
6 * 2. Babylon
7 * 3. Babel-ESLint
8 *
9 * This file also contains code from Esprima, which is BSD licensed.
10 *
11 * Acorn is Copyright 2012-2015 Acorn Contributors (https://github.com/marijnh/acorn/blob/master/AUTHORS)
12 * Babylon is Copyright 2014-2015 various contributors (https://github.com/babel/babel/blob/master/packages/babylon/AUTHORS)
13 * Babel-ESLint is Copyright 2014-2015 Sebastian McKenzie <sebmck@gmail.com>
14 *
15 * Redistribution and use in source and binary forms, with or without
16 * modification, are permitted provided that the following conditions are met:
17 *
18 * * Redistributions of source code must retain the above copyright
19 * notice, this list of conditions and the following disclaimer.
20 * * Redistributions in binary form must reproduce the above copyright
21 * notice, this list of conditions and the following disclaimer in the
22 * documentation and/or other materials provided with the distribution.
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 * ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
28 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
30 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
31 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
32 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
33 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 *
35 * Esprima is Copyright (c) jQuery Foundation, Inc. and Contributors, All Rights Reserved.
36 *
37 * Redistribution and use in source and binary forms, with or without
38 * modification, are permitted provided that the following conditions are met:
39 *
40 * * Redistributions of source code must retain the above copyright
41 * notice, this list of conditions and the following disclaimer.
42 * * Redistributions in binary form must reproduce the above copyright
43 * notice, this list of conditions and the following disclaimer in the
44 * documentation and/or other materials provided with the distribution.
45 *
46 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
47 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
48 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
49 * ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
50 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
51 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
52 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
53 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
54 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
55 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
56 */
57/* eslint no-undefined:0, no-use-before-define: 0 */
58
59"use strict";
60
61var astNodeTypes = require("./lib/ast-node-types"),
62 commentAttachment = require("./lib/comment-attachment"),
63 TokenTranslator = require("./lib/token-translator"),
64 acornJSX = require("acorn-jsx/inject"),
65 rawAcorn = require("acorn");
66
67
68var acorn = acornJSX(rawAcorn);
69var DEFAULT_ECMA_VERSION = 5;
70var lookahead,
71 extra,
72 lastToken;
73
74/**
75 * Resets the extra object to its default.
76 * @returns {void}
77 * @private
78 */
79function resetExtra() {
80 extra = {
81 tokens: null,
82 range: false,
83 loc: false,
84 comment: false,
85 comments: [],
86 tolerant: false,
87 errors: [],
88 strict: false,
89 ecmaFeatures: {},
90 ecmaVersion: DEFAULT_ECMA_VERSION,
91 isModule: false
92 };
93}
94
95
96
97var tt = acorn.tokTypes,
98 getLineInfo = acorn.getLineInfo;
99
100// custom type for JSX attribute values
101tt.jsxAttrValueToken = {};
102
103/**
104 * Normalize ECMAScript version from the initial config
105 * @param {number} ecmaVersion ECMAScript version from the initial config
106 * @returns {number} normalized ECMAScript version
107 */
108function normalizeEcmaVersion(ecmaVersion) {
109 if (typeof ecmaVersion === "number") {
110 var version = ecmaVersion;
111
112 // Calculate ECMAScript edition number from official year version starting with
113 // ES2015, which corresponds with ES6 (or a difference of 2009).
114 if (version >= 2015) {
115 version -= 2009;
116 }
117
118 switch (version) {
119 case 3:
120 case 5:
121 case 6:
122 case 7:
123 case 8:
124 case 9:
125 case 10:
126 return version;
127
128 default:
129 throw new Error("Invalid ecmaVersion.");
130 }
131 } else {
132 return DEFAULT_ECMA_VERSION;
133 }
134}
135
136/**
137 * Determines if a node is valid given the set of ecmaFeatures.
138 * @param {ASTNode} node The node to check.
139 * @returns {boolean} True if the node is allowed, false if not.
140 * @private
141 */
142function isValidNode(node) {
143 switch (node.type) {
144 case "ImportDeclaration":
145 case "ExportNamedDeclaration":
146 case "ExportDefaultDeclaration":
147 case "ExportAllDeclaration":
148 return extra.isModule;
149
150 default:
151 return true;
152 }
153}
154
155/**
156 * Performs last-minute Esprima-specific compatibility checks and fixes.
157 * @param {ASTNode} result The node to check.
158 * @returns {ASTNode} The finished node.
159 * @private
160 * @this acorn.Parser
161 */
162function esprimaFinishNode(result) {
163 // ensure that parsed node was allowed through ecmaFeatures
164 if (!isValidNode(result)) {
165 this.unexpected(result.start);
166 }
167
168 // Acorn doesn't count the opening and closing backticks as part of templates
169 // so we have to adjust ranges/locations appropriately.
170 if (result.type === "TemplateElement") {
171
172 // additional adjustment needed if ${ is the last token
173 var terminalDollarBraceL = this.input.slice(result.end, result.end + 2) === "${";
174
175 if (result.range) {
176 result.range[0]--;
177 result.range[1] += (terminalDollarBraceL ? 2 : 1);
178 }
179
180 if (result.loc) {
181 result.loc.start.column--;
182 result.loc.end.column += (terminalDollarBraceL ? 2 : 1);
183 }
184 }
185
186 if (extra.attachComment) {
187 commentAttachment.processComment(result);
188 }
189
190 if (result.type.indexOf("Function") > -1 && !result.generator) {
191 result.generator = false;
192 }
193
194 return result;
195}
196
197/**
198 * Determines if a token is valid given the set of ecmaFeatures.
199 * @param {acorn.Parser} parser The parser to check.
200 * @returns {boolean} True if the token is allowed, false if not.
201 * @private
202 */
203function isValidToken(parser) {
204 var ecma = extra.ecmaFeatures;
205 var type = parser.type;
206
207 switch (type) {
208 case tt.jsxName:
209 case tt.jsxText:
210 case tt.jsxTagStart:
211 case tt.jsxTagEnd:
212 return ecma.jsx;
213
214 // https://github.com/ternjs/acorn/issues/363
215 case tt.regexp:
216 if (extra.ecmaVersion < 6 && parser.value.flags && parser.value.flags.indexOf("y") > -1) {
217 return false;
218 }
219
220 return true;
221
222 default:
223 return true;
224 }
225}
226
227/**
228 * Injects esprimaFinishNode into the finishNode process.
229 * @param {Function} finishNode Original finishNode function.
230 * @returns {ASTNode} The finished node.
231 * @private
232 */
233function wrapFinishNode(finishNode) {
234 return /** @this acorn.Parser */ function(node, type, pos, loc) {
235 var result = finishNode.call(this, node, type, pos, loc);
236 return esprimaFinishNode.call(this, result);
237 };
238}
239
240acorn.plugins.espree = function(instance) {
241
242 instance.extend("finishNode", wrapFinishNode);
243
244 instance.extend("finishNodeAt", wrapFinishNode);
245
246 instance.extend("next", function(next) {
247 return /** @this acorn.Parser */ function() {
248 if (!isValidToken(this)) {
249 this.unexpected();
250 }
251 return next.call(this);
252 };
253 });
254
255 instance.extend("parseTopLevel", function(parseTopLevel) {
256 return /** @this acorn.Parser */ function(node) {
257 if (extra.ecmaFeatures.impliedStrict && this.options.ecmaVersion >= 5) {
258 this.strict = true;
259 }
260 return parseTopLevel.call(this, node);
261 };
262 });
263
264 /**
265 * Overwrites the default raise method to throw Esprima-style errors.
266 * @param {int} pos The position of the error.
267 * @param {string} message The error message.
268 * @throws {SyntaxError} A syntax error.
269 * @returns {void}
270 */
271 instance.raise = instance.raiseRecoverable = function(pos, message) {
272 var loc = getLineInfo(this.input, pos);
273 var err = new SyntaxError(message);
274 err.index = pos;
275 err.lineNumber = loc.line;
276 err.column = loc.column + 1; // acorn uses 0-based columns
277 throw err;
278 };
279
280 /**
281 * Overwrites the default unexpected method to throw Esprima-style errors.
282 * @param {int} pos The position of the error.
283 * @throws {SyntaxError} A syntax error.
284 * @returns {void}
285 */
286 instance.unexpected = function(pos) {
287 var message = "Unexpected token";
288
289 if (pos !== null && pos !== undefined) {
290 this.pos = pos;
291
292 if (this.options.locations) {
293 while (this.pos < this.lineStart) {
294 this.lineStart = this.input.lastIndexOf("\n", this.lineStart - 2) + 1;
295 --this.curLine;
296 }
297 }
298
299 this.nextToken();
300 }
301
302 if (this.end > this.start) {
303 message += " " + this.input.slice(this.start, this.end);
304 }
305
306 this.raise(this.start, message);
307 };
308
309 /*
310 * Esprima-FB represents JSX strings as tokens called "JSXText", but Acorn-JSX
311 * uses regular tt.string without any distinction between this and regular JS
312 * strings. As such, we intercept an attempt to read a JSX string and set a flag
313 * on extra so that when tokens are converted, the next token will be switched
314 * to JSXText via onToken.
315 */
316 instance.extend("jsx_readString", function(jsxReadString) {
317 return /** @this acorn.Parser */ function(quote) {
318 var result = jsxReadString.call(this, quote);
319 if (this.type === tt.string) {
320 extra.jsxAttrValueToken = true;
321 }
322
323 return result;
324 };
325 });
326};
327
328//------------------------------------------------------------------------------
329// Tokenizer
330//------------------------------------------------------------------------------
331
332/**
333 * Tokenizes the given code.
334 * @param {string} code The code to tokenize.
335 * @param {Object} options Options defining how to tokenize.
336 * @returns {Token[]} An array of tokens.
337 * @throws {SyntaxError} If the input code is invalid.
338 * @private
339 */
340function tokenize(code, options) {
341 var toString,
342 tokens,
343 impliedStrict,
344 translator = new TokenTranslator(tt, code);
345
346 toString = String;
347 if (typeof code !== "string" && !(code instanceof String)) {
348 code = toString(code);
349 }
350
351 lookahead = null;
352
353 // Options matching.
354 options = Object.assign({}, options);
355
356 var acornOptions = {
357 ecmaVersion: DEFAULT_ECMA_VERSION,
358 plugins: {
359 espree: true
360 }
361 };
362
363 resetExtra();
364
365 // Of course we collect tokens here.
366 options.tokens = true;
367 extra.tokens = [];
368
369 extra.range = (typeof options.range === "boolean") && options.range;
370 acornOptions.ranges = extra.range;
371
372 extra.loc = (typeof options.loc === "boolean") && options.loc;
373 acornOptions.locations = extra.loc;
374
375 extra.comment = typeof options.comment === "boolean" && options.comment;
376
377 if (extra.comment) {
378 acornOptions.onComment = function() {
379 var comment = convertAcornCommentToEsprimaComment.apply(this, arguments);
380 extra.comments.push(comment);
381 };
382 }
383
384 extra.tolerant = typeof options.tolerant === "boolean" && options.tolerant;
385
386 acornOptions.ecmaVersion = extra.ecmaVersion = normalizeEcmaVersion(options.ecmaVersion);
387
388 // apply parsing flags
389 if (options.ecmaFeatures && typeof options.ecmaFeatures === "object") {
390 extra.ecmaFeatures = Object.assign({}, options.ecmaFeatures);
391 impliedStrict = extra.ecmaFeatures.impliedStrict;
392 extra.ecmaFeatures.impliedStrict = typeof impliedStrict === "boolean" && impliedStrict;
393 }
394
395 try {
396 var tokenizer = acorn.tokenizer(code, acornOptions);
397 while ((lookahead = tokenizer.getToken()).type !== tt.eof) {
398 translator.onToken(lookahead, extra);
399 }
400
401 // filterTokenLocation();
402 tokens = extra.tokens;
403
404 if (extra.comment) {
405 tokens.comments = extra.comments;
406 }
407 if (extra.tolerant) {
408 tokens.errors = extra.errors;
409 }
410 } catch (e) {
411 throw e;
412 }
413 return tokens;
414}
415
416//------------------------------------------------------------------------------
417// Parser
418//------------------------------------------------------------------------------
419
420
421
422/**
423 * Converts an Acorn comment to a Esprima comment.
424 * @param {boolean} block True if it's a block comment, false if not.
425 * @param {string} text The text of the comment.
426 * @param {int} start The index at which the comment starts.
427 * @param {int} end The index at which the comment ends.
428 * @param {Location} startLoc The location at which the comment starts.
429 * @param {Location} endLoc The location at which the comment ends.
430 * @returns {Object} The comment object.
431 * @private
432 */
433function convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc) {
434 var comment = {
435 type: block ? "Block" : "Line",
436 value: text
437 };
438
439 if (typeof start === "number") {
440 comment.start = start;
441 comment.end = end;
442 comment.range = [start, end];
443 }
444
445 if (typeof startLoc === "object") {
446 comment.loc = {
447 start: startLoc,
448 end: endLoc
449 };
450 }
451
452 return comment;
453}
454
455/**
456 * Parses the given code.
457 * @param {string} code The code to tokenize.
458 * @param {Object} options Options defining how to tokenize.
459 * @returns {ASTNode} The "Program" AST node.
460 * @throws {SyntaxError} If the input code is invalid.
461 * @private
462 */
463function parse(code, options) {
464 var program,
465 toString = String,
466 translator,
467 impliedStrict,
468 acornOptions = {
469 ecmaVersion: DEFAULT_ECMA_VERSION,
470 plugins: {
471 espree: true
472 }
473 };
474
475 lastToken = null;
476
477 if (typeof code !== "string" && !(code instanceof String)) {
478 code = toString(code);
479 }
480
481 resetExtra();
482 commentAttachment.reset();
483
484 if (typeof options !== "undefined") {
485 extra.range = (typeof options.range === "boolean") && options.range;
486 extra.loc = (typeof options.loc === "boolean") && options.loc;
487 extra.attachComment = (typeof options.attachComment === "boolean") && options.attachComment;
488
489 if (extra.loc && options.source !== null && options.source !== undefined) {
490 extra.source = toString(options.source);
491 }
492
493 if (typeof options.tokens === "boolean" && options.tokens) {
494 extra.tokens = [];
495 translator = new TokenTranslator(tt, code);
496 }
497 if (typeof options.comment === "boolean" && options.comment) {
498 extra.comment = true;
499 extra.comments = [];
500 }
501 if (typeof options.tolerant === "boolean" && options.tolerant) {
502 extra.errors = [];
503 }
504 if (extra.attachComment) {
505 extra.range = true;
506 extra.comments = [];
507 commentAttachment.reset();
508 }
509
510 acornOptions.ecmaVersion = extra.ecmaVersion = normalizeEcmaVersion(options.ecmaVersion);
511
512 if (options.sourceType === "module") {
513 extra.isModule = true;
514
515 // modules must be in 6 at least
516 if (acornOptions.ecmaVersion < 6) {
517 acornOptions.ecmaVersion = 6;
518 extra.ecmaVersion = 6;
519 }
520
521 acornOptions.sourceType = "module";
522 }
523
524 // apply parsing flags after sourceType to allow overriding
525 if (options.ecmaFeatures && typeof options.ecmaFeatures === "object") {
526 extra.ecmaFeatures = Object.assign({}, options.ecmaFeatures);
527 impliedStrict = extra.ecmaFeatures.impliedStrict;
528 extra.ecmaFeatures.impliedStrict = typeof impliedStrict === "boolean" && impliedStrict;
529 if (options.ecmaFeatures.globalReturn) {
530 acornOptions.allowReturnOutsideFunction = true;
531 }
532 }
533
534
535 acornOptions.onToken = function(token) {
536 if (extra.tokens) {
537 translator.onToken(token, extra);
538 }
539 if (token.type !== tt.eof) {
540 lastToken = token;
541 }
542 };
543
544 if (extra.attachComment || extra.comment) {
545 acornOptions.onComment = function() {
546 var comment = convertAcornCommentToEsprimaComment.apply(this, arguments);
547 extra.comments.push(comment);
548
549 if (extra.attachComment) {
550 commentAttachment.addComment(comment);
551 }
552 };
553 }
554
555 if (extra.range) {
556 acornOptions.ranges = true;
557 }
558
559 if (extra.loc) {
560 acornOptions.locations = true;
561 }
562
563 if (extra.ecmaFeatures.jsx) {
564 // Should process jsx plugin before espree plugin.
565 acornOptions.plugins = {
566 jsx: true,
567 espree: true
568 };
569 }
570 }
571
572 program = acorn.parse(code, acornOptions);
573 program.sourceType = extra.isModule ? "module" : "script";
574
575 if (extra.comment || extra.attachComment) {
576 program.comments = extra.comments;
577 }
578
579 if (extra.tokens) {
580 program.tokens = extra.tokens;
581 }
582
583 /*
584 * Adjust opening and closing position of program to match Esprima.
585 * Acorn always starts programs at range 0 whereas Esprima starts at the
586 * first AST node's start (the only real difference is when there's leading
587 * whitespace or leading comments). Acorn also counts trailing whitespace
588 * as part of the program whereas Esprima only counts up to the last token.
589 */
590 if (program.range) {
591 program.range[0] = program.body.length ? program.body[0].range[0] : program.range[0];
592 program.range[1] = lastToken ? lastToken.range[1] : program.range[1];
593 }
594
595 if (program.loc) {
596 program.loc.start = program.body.length ? program.body[0].loc.start : program.loc.start;
597 program.loc.end = lastToken ? lastToken.loc.end : program.loc.end;
598 }
599
600 return program;
601}
602
603//------------------------------------------------------------------------------
604// Public
605//------------------------------------------------------------------------------
606
607exports.version = require("./package.json").version;
608
609exports.tokenize = tokenize;
610
611exports.parse = parse;
612
613// Deep copy.
614/* istanbul ignore next */
615exports.Syntax = (function() {
616 var name, types = {};
617
618 if (typeof Object.create === "function") {
619 types = Object.create(null);
620 }
621
622 for (name in astNodeTypes) {
623 if (astNodeTypes.hasOwnProperty(name)) {
624 types[name] = astNodeTypes[name];
625 }
626 }
627
628 if (typeof Object.freeze === "function") {
629 Object.freeze(types);
630 }
631
632 return types;
633}());
634
635/* istanbul ignore next */
636exports.VisitorKeys = (function() {
637 var visitorKeys = require("./lib/visitor-keys");
638 var name,
639 keys = {};
640
641 if (typeof Object.create === "function") {
642 keys = Object.create(null);
643 }
644
645 for (name in visitorKeys) {
646 if (visitorKeys.hasOwnProperty(name)) {
647 keys[name] = visitorKeys[name];
648 }
649 }
650
651 if (typeof Object.freeze === "function") {
652 Object.freeze(keys);
653 }
654
655 return keys;
656}());