/** * @fileoverview Main Espree file that converts Acorn into Esprima output. * * This file contains code from the following MIT-licensed projects: * 1. Acorn * 2. Babylon * 3. Babel-ESLint * * This file also contains code from Esprima, which is BSD licensed. * * Acorn is Copyright 2012-2015 Acorn Contributors (https://github.com/marijnh/acorn/blob/master/AUTHORS) * Babylon is Copyright 2014-2015 various contributors (https://github.com/babel/babel/blob/master/packages/babylon/AUTHORS) * Babel-ESLint is Copyright 2014-2015 Sebastian McKenzie * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * Esprima is Copyright (c) jQuery Foundation, Inc. and Contributors, All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* eslint no-undefined:0, no-use-before-define: 0 */ "use strict"; var astNodeTypes = require("./lib/ast-node-types"), commentAttachment = require("./lib/comment-attachment"), TokenTranslator = require("./lib/token-translator"), acornJSX = require("acorn-jsx/inject"), rawAcorn = require("acorn"); var acorn = acornJSX(rawAcorn); var DEFAULT_ECMA_VERSION = 5; var lookahead, extra, lastToken; /** * Object.assign polyfill for Node < 4 * @param {Object} target The target object * @param {...Object} sources Sources for the object * @returns {Object} `target` after being mutated */ var assign = Object.assign || function assign(target) { for (var argIndex = 1; argIndex < arguments.length; argIndex++) { if (arguments[argIndex] !== null && typeof arguments[argIndex] === "object") { var keys = Object.keys(arguments[argIndex]); for (var keyIndex = 0; keyIndex < keys.length; keyIndex++) { target[keys[keyIndex]] = arguments[argIndex][keys[keyIndex]]; } } } return target; }; /** * Resets the extra object to its default. * @returns {void} * @private */ function resetExtra() { extra = { tokens: null, range: false, loc: false, comment: false, comments: [], tolerant: false, errors: [], strict: false, ecmaFeatures: {}, ecmaVersion: DEFAULT_ECMA_VERSION, isModule: false }; } var tt = acorn.tokTypes, getLineInfo = acorn.getLineInfo; // custom type for JSX attribute values tt.jsxAttrValueToken = {}; /** * Normalize ECMAScript version from the initial config * @param {number} ecmaVersion ECMAScript version from the initial config * @returns {number} normalized ECMAScript version */ function normalizeEcmaVersion(ecmaVersion) { if (typeof ecmaVersion === "number") { var version = ecmaVersion; // Calculate ECMAScript edition number from official year version starting with // ES2015, which corresponds with ES6 (or a difference of 2009). if (version >= 2015) { version -= 2009; } switch (version) { case 3: case 5: case 6: case 7: case 8: return version; default: throw new Error("Invalid ecmaVersion."); } } else { return DEFAULT_ECMA_VERSION; } } /** * Determines if a node is valid given the set of ecmaFeatures. * @param {ASTNode} node The node to check. * @returns {boolean} True if the node is allowed, false if not. * @private */ function isValidNode(node) { var ecma = extra.ecmaFeatures; switch (node.type) { case "ExperimentalSpreadProperty": case "ExperimentalRestProperty": return ecma.experimentalObjectRestSpread; case "ImportDeclaration": case "ExportNamedDeclaration": case "ExportDefaultDeclaration": case "ExportAllDeclaration": return extra.isModule; default: return true; } } /** * Performs last-minute Esprima-specific compatibility checks and fixes. * @param {ASTNode} result The node to check. * @returns {ASTNode} The finished node. * @private * @this acorn.Parser */ function esprimaFinishNode(result) { // ensure that parsed node was allowed through ecmaFeatures if (!isValidNode(result)) { this.unexpected(result.start); } // https://github.com/marijnh/acorn/issues/323 if (result.type === "TryStatement") { delete result.guardedHandlers; } else if (result.type === "CatchClause") { delete result.guard; } // 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") { // additional adjustment needed if ${ is the last token var terminalDollarBraceL = this.input.slice(result.end, result.end + 2) === "${"; if (result.range) { result.range[0]--; result.range[1] += (terminalDollarBraceL ? 2 : 1); } if (result.loc) { result.loc.start.column--; result.loc.end.column += (terminalDollarBraceL ? 2 : 1); } } // Acorn uses undefined instead of null, which affects serialization if (result.type === "Literal" && result.value === undefined) { result.value = null; } if (extra.attachComment) { commentAttachment.processComment(result); } if (result.type.indexOf("Function") > -1 && !result.generator) { result.generator = false; } return result; } /** * Determines if a token is valid given the set of ecmaFeatures. * @param {acorn.Parser} parser The parser to check. * @returns {boolean} True if the token is allowed, false if not. * @private */ function isValidToken(parser) { var ecma = extra.ecmaFeatures; var type = parser.type; switch (type) { case tt.jsxName: case tt.jsxText: case tt.jsxTagStart: case tt.jsxTagEnd: return ecma.jsx; // https://github.com/ternjs/acorn/issues/363 case tt.regexp: if (extra.ecmaVersion < 6 && parser.value.flags && parser.value.flags.indexOf("y") > -1) { return false; } return true; default: return true; } } /** * Injects esprimaFinishNode into the finishNode process. * @param {Function} finishNode Original finishNode function. * @returns {ASTNode} The finished node. * @private */ function wrapFinishNode(finishNode) { return /** @this acorn.Parser */ function(node, type, pos, loc) { var result = finishNode.call(this, node, type, pos, loc); return esprimaFinishNode.call(this, result); }; } acorn.plugins.espree = function(instance) { instance.extend("finishNode", wrapFinishNode); instance.extend("finishNodeAt", wrapFinishNode); instance.extend("next", function(next) { return /** @this acorn.Parser */ function() { if (!isValidToken(this)) { this.unexpected(); } return next.call(this); }; }); // needed for experimental object rest/spread instance.extend("checkLVal", function(checkLVal) { return /** @this acorn.Parser */ function(expr, isBinding, checkClashes) { if (extra.ecmaFeatures.experimentalObjectRestSpread && expr.type === "ObjectPattern") { for (var i = 0; i < expr.properties.length; i++) { if (expr.properties[i].type.indexOf("Experimental") === -1) { this.checkLVal(expr.properties[i].value, isBinding, checkClashes); } } return undefined; } return checkLVal.call(this, expr, isBinding, checkClashes); }; }); instance.extend("parseTopLevel", function(parseTopLevel) { return /** @this acorn.Parser */ function(node) { if (extra.ecmaFeatures.impliedStrict && this.options.ecmaVersion >= 5) { this.strict = true; } return parseTopLevel.call(this, node); }; }); instance.extend("toAssignable", function(toAssignable) { return /** @this acorn.Parser */ function(node, isBinding) { if (extra.ecmaFeatures.experimentalObjectRestSpread && node.type === "ObjectExpression" ) { node.type = "ObjectPattern"; for (var i = 0; i < node.properties.length; i++) { var prop = node.properties[i]; if (prop.type === "ExperimentalSpreadProperty") { prop.type = "ExperimentalRestProperty"; } else if (prop.kind !== "init") { this.raise(prop.key.start, "Object pattern can't contain getter or setter"); } else { this.toAssignable(prop.value, isBinding); } } return node; } else { return toAssignable.call(this, node, isBinding); } }; }); /** * Method to parse an object rest or object spread. * @returns {ASTNode} The node representing object rest or object spread. * @this acorn.Parser */ instance.parseObjectRest = function() { var node = this.startNode(); this.next(); node.argument = this.parseIdent(); return this.finishNode(node, "ExperimentalRestProperty"); }; /** * Method to parse an object with object rest or object spread. * @param {boolean} isPattern True if the object is a destructuring pattern. * @param {Object} refShorthandDefaultPos ? * @returns {ASTNode} The node representing object rest or object spread. * @this acorn.Parser */ instance.parseObj = function(isPattern, refShorthandDefaultPos) { var node = this.startNode(), first = true, hasRestProperty = false, propHash = {}; node.properties = []; this.next(); while (!this.eat(tt.braceR)) { if (!first) { this.expect(tt.comma); if (this.afterTrailingComma(tt.braceR)) { if (hasRestProperty) { this.raise(node.properties[node.properties.length - 1].end, "Unexpected trailing comma after rest property"); } break; } } else { first = false; } var prop = this.startNode(), isGenerator, isAsync, startPos, startLoc; if (extra.ecmaFeatures.experimentalObjectRestSpread && this.type === tt.ellipsis) { if (isPattern) { prop = this.parseObjectRest(); hasRestProperty = true; } else { prop = this.parseSpread(); prop.type = "ExperimentalSpreadProperty"; } node.properties.push(prop); continue; } if (this.options.ecmaVersion >= 6) { prop.method = false; prop.shorthand = false; if (isPattern || refShorthandDefaultPos) { startPos = this.start; startLoc = this.startLoc; } if (!isPattern) { isGenerator = this.eat(tt.star); } } // grab the property name or "async" this.parsePropertyName(prop, refShorthandDefaultPos); if (this.options.ecmaVersion >= 8 && !isPattern && !isGenerator && !prop.computed && prop.key.type === "Identifier" && prop.key.name === "async" && this.type !== tt.parenL && this.type !== tt.colon && !this.canInsertSemicolon() ) { this.parsePropertyName(prop, refShorthandDefaultPos); isAsync = true; } else { isAsync = false; } this.parsePropertyValue(prop, isPattern, isGenerator, isAsync, startPos, startLoc, refShorthandDefaultPos); this.checkPropClash(prop, propHash); node.properties.push(this.finishNode(prop, "Property")); } return this.finishNode(node, isPattern ? "ObjectPattern" : "ObjectExpression"); }; /** * Overwrites the default raise method to throw Esprima-style errors. * @param {int} pos The position of the error. * @param {string} message The error message. * @throws {SyntaxError} A syntax error. * @returns {void} */ instance.raise = instance.raiseRecoverable = function(pos, message) { var loc = getLineInfo(this.input, pos); var err = 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 unexpected method to throw Esprima-style errors. * @param {int} pos The position of the error. * @throws {SyntaxError} A syntax error. * @returns {void} */ instance.unexpected = function(pos) { var message = "Unexpected token"; if (pos !== null && pos !== undefined) { 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. */ instance.extend("jsx_readString", function(jsxReadString) { return /** @this acorn.Parser */ function(quote) { var result = jsxReadString.call(this, quote); if (this.type === tt.string) { extra.jsxAttrValueToken = true; } return result; }; }); }; //------------------------------------------------------------------------------ // Tokenizer //------------------------------------------------------------------------------ /** * Tokenizes the given code. * @param {string} code The code to tokenize. * @param {Object} options Options defining how to tokenize. * @returns {Token[]} An array of tokens. * @throws {SyntaxError} If the input code is invalid. * @private */ function tokenize(code, options) { var toString, tokens, impliedStrict, translator = new TokenTranslator(tt, code); toString = String; if (typeof code !== "string" && !(code instanceof String)) { code = toString(code); } lookahead = null; // Options matching. options = assign({}, options); var acornOptions = { ecmaVersion: DEFAULT_ECMA_VERSION, plugins: { espree: true } }; resetExtra(); // Of course we collect tokens here. options.tokens = true; extra.tokens = []; extra.range = (typeof options.range === "boolean") && options.range; acornOptions.ranges = extra.range; extra.loc = (typeof options.loc === "boolean") && options.loc; acornOptions.locations = extra.loc; extra.comment = typeof options.comment === "boolean" && options.comment; if (extra.comment) { acornOptions.onComment = function() { var comment = convertAcornCommentToEsprimaComment.apply(this, arguments); extra.comments.push(comment); }; } extra.tolerant = typeof options.tolerant === "boolean" && options.tolerant; acornOptions.ecmaVersion = extra.ecmaVersion = normalizeEcmaVersion(options.ecmaVersion); // apply parsing flags if (options.ecmaFeatures && typeof options.ecmaFeatures === "object") { extra.ecmaFeatures = assign({}, options.ecmaFeatures); impliedStrict = extra.ecmaFeatures.impliedStrict; extra.ecmaFeatures.impliedStrict = typeof impliedStrict === "boolean" && impliedStrict; } try { var tokenizer = acorn.tokenizer(code, acornOptions); while ((lookahead = tokenizer.getToken()).type !== tt.eof) { translator.onToken(lookahead, extra); } // filterTokenLocation(); tokens = extra.tokens; if (extra.comment) { tokens.comments = extra.comments; } if (extra.tolerant) { tokens.errors = extra.errors; } } catch (e) { throw e; } return tokens; } //------------------------------------------------------------------------------ // Parser //------------------------------------------------------------------------------ /** * 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 {int} start The index at which the comment starts. * @param {int} end The index at which the comment ends. * @param {Location} startLoc The location at which the comment starts. * @param {Location} endLoc The location at which the comment ends. * @returns {Object} The comment object. * @private */ function convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc) { var comment = { type: block ? "Block" : "Line", 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; } /** * Parses the given code. * @param {string} code The code to tokenize. * @param {Object} options Options defining how to tokenize. * @returns {ASTNode} The "Program" AST node. * @throws {SyntaxError} If the input code is invalid. * @private */ function parse(code, options) { var program, toString = String, translator, impliedStrict, acornOptions = { ecmaVersion: DEFAULT_ECMA_VERSION, plugins: { espree: true } }; lastToken = null; if (typeof code !== "string" && !(code instanceof String)) { code = toString(code); } resetExtra(); commentAttachment.reset(); if (typeof options !== "undefined") { extra.range = (typeof options.range === "boolean") && options.range; extra.loc = (typeof options.loc === "boolean") && options.loc; extra.attachComment = (typeof options.attachComment === "boolean") && options.attachComment; if (extra.loc && options.source !== null && options.source !== undefined) { extra.source = toString(options.source); } if (typeof options.tokens === "boolean" && options.tokens) { extra.tokens = []; translator = new TokenTranslator(tt, code); } if (typeof options.comment === "boolean" && options.comment) { extra.comment = true; extra.comments = []; } if (typeof options.tolerant === "boolean" && options.tolerant) { extra.errors = []; } if (extra.attachComment) { extra.range = true; extra.comments = []; commentAttachment.reset(); } acornOptions.ecmaVersion = extra.ecmaVersion = normalizeEcmaVersion(options.ecmaVersion); if (options.sourceType === "module") { extra.isModule = true; // modules must be in 6 at least if (acornOptions.ecmaVersion < 6) { acornOptions.ecmaVersion = 6; extra.ecmaVersion = 6; } acornOptions.sourceType = "module"; } // apply parsing flags after sourceType to allow overriding if (options.ecmaFeatures && typeof options.ecmaFeatures === "object") { extra.ecmaFeatures = assign({}, options.ecmaFeatures); impliedStrict = extra.ecmaFeatures.impliedStrict; extra.ecmaFeatures.impliedStrict = typeof impliedStrict === "boolean" && impliedStrict; if (options.ecmaFeatures.globalReturn) { acornOptions.allowReturnOutsideFunction = true; } } acornOptions.onToken = function(token) { if (extra.tokens) { translator.onToken(token, extra); } if (token.type !== tt.eof) { lastToken = token; } }; if (extra.attachComment || extra.comment) { acornOptions.onComment = function() { var comment = convertAcornCommentToEsprimaComment.apply(this, arguments); extra.comments.push(comment); if (extra.attachComment) { commentAttachment.addComment(comment); } }; } if (extra.range) { acornOptions.ranges = true; } if (extra.loc) { acornOptions.locations = true; } if (extra.ecmaFeatures.jsx) { // Should process jsx plugin before espree plugin. acornOptions.plugins = { jsx: true, espree: true }; } } program = acorn.parse(code, acornOptions); program.sourceType = extra.isModule ? "module" : "script"; if (extra.comment || extra.attachComment) { program.comments = extra.comments; } if (extra.tokens) { program.tokens = extra.tokens; } /* * Adjust opening and closing position of program to match Esprima. * Acorn always starts programs at range 0 whereas Esprima starts at the * first AST node's start (the only real difference is when there's leading * whitespace or leading comments). Acorn also counts trailing whitespace * as part of the program whereas Esprima only counts up to the last token. */ if (program.range) { program.range[0] = program.body.length ? program.body[0].range[0] : program.range[0]; program.range[1] = lastToken ? lastToken.range[1] : program.range[1]; } if (program.loc) { program.loc.start = program.body.length ? program.body[0].loc.start : program.loc.start; program.loc.end = lastToken ? lastToken.loc.end : program.loc.end; } return program; } //------------------------------------------------------------------------------ // Public //------------------------------------------------------------------------------ exports.version = require("./package.json").version; exports.tokenize = tokenize; exports.parse = parse; // Deep copy. /* istanbul ignore next */ exports.Syntax = (function() { var name, types = {}; if (typeof Object.create === "function") { types = Object.create(null); } for (name in astNodeTypes) { if (astNodeTypes.hasOwnProperty(name)) { types[name] = astNodeTypes[name]; } } if (typeof Object.freeze === "function") { Object.freeze(types); } return types; }()); /* istanbul ignore next */ exports.VisitorKeys = (function() { var visitorKeys = require("./lib/visitor-keys"); var name, keys = {}; if (typeof Object.create === "function") { keys = Object.create(null); } for (name in visitorKeys) { if (visitorKeys.hasOwnProperty(name)) { keys[name] = visitorKeys[name]; } } if (typeof Object.freeze === "function") { Object.freeze(keys); } return keys; }());