3 /* eslint-disable no-param-reassign*/
4 const TokenTranslator = require("./token-translator");
5 const { normalizeOptions } = require("./options");
7 const STATE = Symbol("espree's internal state");
8 const ESPRIMA_FINISH_NODE = Symbol("espree's esprimaFinishNode");
12 * Converts an Acorn comment to a Esprima comment.
13 * @param {boolean} block True if it's a block comment, false if not.
14 * @param {string} text The text of the comment.
15 * @param {int} start The index at which the comment starts.
16 * @param {int} end The index at which the comment ends.
17 * @param {Location} startLoc The location at which the comment starts.
18 * @param {Location} endLoc The location at which the comment ends.
19 * @returns {Object} The comment object.
22 function convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc) {
24 type: block ? "Block" : "Line",
28 if (typeof start === "number") {
29 comment.start = start;
31 comment.range = [start, end];
34 if (typeof startLoc === "object") {
44 module.exports = () => Parser => {
45 const tokTypes = Object.assign({}, Parser.acorn.tokTypes);
47 if (Parser.acornJsx) {
48 Object.assign(tokTypes, Parser.acornJsx.tokTypes);
51 return class Espree extends Parser {
52 constructor(opts, code) {
53 if (typeof opts !== "object" || opts === null) {
56 if (typeof code !== "string" && !(code instanceof String)) {
60 const options = normalizeOptions(opts);
61 const ecmaFeatures = options.ecmaFeatures || {};
62 const tokenTranslator =
63 options.tokens === true
64 ? new TokenTranslator(tokTypes, code)
67 // Initialize acorn parser.
70 // TODO: use {...options} when spread is supported(Node.js >= 8.3.0).
71 ecmaVersion: options.ecmaVersion,
72 sourceType: options.sourceType,
73 ranges: options.ranges,
74 locations: options.locations,
76 // Truthy value is true for backward compatibility.
77 allowReturnOutsideFunction: Boolean(ecmaFeatures.globalReturn),
81 if (tokenTranslator) {
83 // Use `tokens`, `ecmaVersion`, and `jsxAttrValueToken` in the state.
84 tokenTranslator.onToken(token, this[STATE]);
86 if (token.type !== tokTypes.eof) {
87 this[STATE].lastToken = token;
92 onComment: (block, text, start, end, startLoc, endLoc) => {
93 if (this[STATE].comments) {
94 const comment = convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc);
96 this[STATE].comments.push(comment);
101 // Initialize internal state.
103 tokens: tokenTranslator ? [] : null,
104 comments: options.comment === true ? [] : null,
105 impliedStrict: ecmaFeatures.impliedStrict === true && this.options.ecmaVersion >= 5,
106 ecmaVersion: this.options.ecmaVersion,
107 jsxAttrValueToken: false,
115 } while (this.type !== tokTypes.eof);
117 // Consume the final eof token
120 const extra = this[STATE];
121 const tokens = extra.tokens;
123 if (extra.comments) {
124 tokens.comments = extra.comments;
130 finishNode(...args) {
131 const result = super.finishNode(...args);
133 return this[ESPRIMA_FINISH_NODE](result);
136 finishNodeAt(...args) {
137 const result = super.finishNodeAt(...args);
139 return this[ESPRIMA_FINISH_NODE](result);
143 const extra = this[STATE];
144 const program = super.parse();
146 program.sourceType = this.options.sourceType;
148 if (extra.comments) {
149 program.comments = extra.comments;
152 program.tokens = extra.tokens;
156 * Adjust opening and closing position of program to match Esprima.
157 * Acorn always starts programs at range 0 whereas Esprima starts at the
158 * first AST node's start (the only real difference is when there's leading
159 * whitespace or leading comments). Acorn also counts trailing whitespace
160 * as part of the program whereas Esprima only counts up to the last token.
163 program.range[0] = program.body.length ? program.body[0].range[0] : program.range[0];
164 program.range[1] = extra.lastToken ? extra.lastToken.range[1] : program.range[1];
167 program.loc.start = program.body.length ? program.body[0].loc.start : program.loc.start;
168 program.loc.end = extra.lastToken ? extra.lastToken.loc.end : program.loc.end;
174 parseTopLevel(node) {
175 if (this[STATE].impliedStrict) {
178 return super.parseTopLevel(node);
182 * Overwrites the default raise method to throw Esprima-style errors.
183 * @param {int} pos The position of the error.
184 * @param {string} message The error message.
185 * @throws {SyntaxError} A syntax error.
188 raise(pos, message) {
189 const loc = Parser.acorn.getLineInfo(this.input, pos);
190 const err = new SyntaxError(message);
193 err.lineNumber = loc.line;
194 err.column = loc.column + 1; // acorn uses 0-based columns
199 * Overwrites the default raise method to throw Esprima-style errors.
200 * @param {int} pos The position of the error.
201 * @param {string} message The error message.
202 * @throws {SyntaxError} A syntax error.
205 raiseRecoverable(pos, message) {
206 this.raise(pos, message);
210 * Overwrites the default unexpected method to throw Esprima-style errors.
211 * @param {int} pos The position of the error.
212 * @throws {SyntaxError} A syntax error.
216 let message = "Unexpected token";
218 if (pos !== null && pos !== void 0) {
221 if (this.options.locations) {
222 while (this.pos < this.lineStart) {
223 this.lineStart = this.input.lastIndexOf("\n", this.lineStart - 2) + 1;
231 if (this.end > this.start) {
232 message += ` ${this.input.slice(this.start, this.end)}`;
235 this.raise(this.start, message);
239 * Esprima-FB represents JSX strings as tokens called "JSXText", but Acorn-JSX
240 * uses regular tt.string without any distinction between this and regular JS
241 * strings. As such, we intercept an attempt to read a JSX string and set a flag
242 * on extra so that when tokens are converted, the next token will be switched
243 * to JSXText via onToken.
245 jsx_readString(quote) { // eslint-disable-line camelcase
246 const result = super.jsx_readString(quote);
248 if (this.type === tokTypes.string) {
249 this[STATE].jsxAttrValueToken = true;
255 * Performs last-minute Esprima-specific compatibility checks and fixes.
256 * @param {ASTNode} result The node to check.
257 * @returns {ASTNode} The finished node.
259 [ESPRIMA_FINISH_NODE](result) {
261 // Acorn doesn't count the opening and closing backticks as part of templates
262 // so we have to adjust ranges/locations appropriately.
263 if (result.type === "TemplateElement") {
265 // additional adjustment needed if ${ is the last token
266 const terminalDollarBraceL = this.input.slice(result.end, result.end + 2) === "${";
270 result.range[1] += (terminalDollarBraceL ? 2 : 1);
274 result.loc.start.column--;
275 result.loc.end.column += (terminalDollarBraceL ? 2 : 1);
279 if (result.type.indexOf("Function") > -1 && !result.generator) {
280 result.generator = false;