"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.parse = void 0; var tslib_1 = require("tslib"); var assert_1 = tslib_1.__importDefault(require("assert")); var types = tslib_1.__importStar(require("ast-types")); var b = types.builders; var isObject = types.builtInTypes.object; var isArray = types.builtInTypes.array; var options_1 = require("./options"); var lines_1 = require("./lines"); var comments_1 = require("./comments"); var util = tslib_1.__importStar(require("./util")); function parse(source, options) { options = options_1.normalize(options); var lines = lines_1.fromString(source, options); var sourceWithoutTabs = lines.toString({ tabWidth: options.tabWidth, reuseWhitespace: false, useTabs: false, }); var comments = []; var ast = options.parser.parse(sourceWithoutTabs, { jsx: true, loc: true, locations: true, range: options.range, comment: true, onComment: comments, tolerant: util.getOption(options, "tolerant", true), ecmaVersion: 6, sourceType: util.getOption(options, "sourceType", "module"), }); // Use ast.tokens if possible, and otherwise fall back to the Esprima // tokenizer. All the preconfigured ../parsers/* expose ast.tokens // automatically, but custom parsers might need additional configuration // to avoid this fallback. var tokens = Array.isArray(ast.tokens) ? ast.tokens : require("esprima").tokenize(sourceWithoutTabs, { loc: true, }); // We will reattach the tokens array to the file object below. delete ast.tokens; // Make sure every token has a token.value string. tokens.forEach(function (token) { if (typeof token.value !== "string") { token.value = lines.sliceString(token.loc.start, token.loc.end); } }); if (Array.isArray(ast.comments)) { comments = ast.comments; delete ast.comments; } if (ast.loc) { // If the source was empty, some parsers give loc.{start,end}.line // values of 0, instead of the minimum of 1. util.fixFaultyLocations(ast, lines); } else { ast.loc = { start: lines.firstPos(), end: lines.lastPos(), }; } ast.loc.lines = lines; ast.loc.indent = 0; var file; var program; if (ast.type === "Program") { program = ast; // In order to ensure we reprint leading and trailing program // comments, wrap the original Program node with a File node. Only // ESTree parsers (Acorn and Esprima) return a Program as the root AST // node. Most other (Babylon-like) parsers return a File. file = b.file(ast, options.sourceFileName || null); file.loc = { start: lines.firstPos(), end: lines.lastPos(), lines: lines, indent: 0, }; } else if (ast.type === "File") { file = ast; program = file.program; } // Expose file.tokens unless the caller passed false for options.tokens. if (options.tokens) { file.tokens = tokens; } // Expand the Program's .loc to include all comments (not just those // attached to the Program node, as its children may have comments as // well), since sometimes program.loc.{start,end} will coincide with the // .loc.{start,end} of the first and last *statements*, mistakenly // excluding comments that fall outside that region. var trueProgramLoc = util.getTrueLoc({ type: program.type, loc: program.loc, body: [], comments: comments, }, lines); program.loc.start = trueProgramLoc.start; program.loc.end = trueProgramLoc.end; // Passing file.program here instead of just file means that initial // comments will be attached to program.body[0] instead of program. comments_1.attach(comments, program.body.length ? file.program : file, lines); // Return a copy of the original AST so that any changes made may be // compared to the original. return new TreeCopier(lines, tokens).copy(file); } exports.parse = parse; var TreeCopier = function TreeCopier(lines, tokens) { assert_1.default.ok(this instanceof TreeCopier); this.lines = lines; this.tokens = tokens; this.startTokenIndex = 0; this.endTokenIndex = tokens.length; this.indent = 0; this.seen = new Map(); }; var TCp = TreeCopier.prototype; TCp.copy = function (node) { if (this.seen.has(node)) { return this.seen.get(node); } if (isArray.check(node)) { var copy_1 = new Array(node.length); this.seen.set(node, copy_1); node.forEach(function (item, i) { copy_1[i] = this.copy(item); }, this); return copy_1; } if (!isObject.check(node)) { return node; } util.fixFaultyLocations(node, this.lines); var copy = Object.create(Object.getPrototypeOf(node), { original: { // Provide a link from the copy to the original. value: node, configurable: false, enumerable: false, writable: true, }, }); this.seen.set(node, copy); var loc = node.loc; var oldIndent = this.indent; var newIndent = oldIndent; var oldStartTokenIndex = this.startTokenIndex; var oldEndTokenIndex = this.endTokenIndex; if (loc) { // When node is a comment, we set node.loc.indent to // node.loc.start.column so that, when/if we print the comment by // itself, we can strip that much whitespace from the left margin of // the comment. This only really matters for multiline Block comments, // but it doesn't hurt for Line comments. if (node.type === "Block" || node.type === "Line" || node.type === "CommentBlock" || node.type === "CommentLine" || this.lines.isPrecededOnlyByWhitespace(loc.start)) { newIndent = this.indent = loc.start.column; } // Every node.loc has a reference to the original source lines as well // as a complete list of source tokens. loc.lines = this.lines; loc.tokens = this.tokens; loc.indent = newIndent; // Set loc.start.token and loc.end.token such that // loc.tokens.slice(loc.start.token, loc.end.token) returns a list of // all the tokens that make up this node. this.findTokenRange(loc); } var keys = Object.keys(node); var keyCount = keys.length; for (var i = 0; i < keyCount; ++i) { var key = keys[i]; if (key === "loc") { copy[key] = node[key]; } else if (key === "tokens" && node.type === "File") { // Preserve file.tokens (uncopied) in case client code cares about // it, even though Recast ignores it when reprinting. copy[key] = node[key]; } else { copy[key] = this.copy(node[key]); } } this.indent = oldIndent; this.startTokenIndex = oldStartTokenIndex; this.endTokenIndex = oldEndTokenIndex; return copy; }; // If we didn't have any idea where in loc.tokens to look for tokens // contained by this loc, a binary search would be appropriate, but // because we maintain this.startTokenIndex and this.endTokenIndex as we // traverse the AST, we only need to make small (linear) adjustments to // those indexes with each recursive iteration. TCp.findTokenRange = function (loc) { // In the unlikely event that loc.tokens[this.startTokenIndex] starts // *after* loc.start, we need to rewind this.startTokenIndex first. while (this.startTokenIndex > 0) { var token = loc.tokens[this.startTokenIndex]; if (util.comparePos(loc.start, token.loc.start) < 0) { --this.startTokenIndex; } else break; } // In the unlikely event that loc.tokens[this.endTokenIndex - 1] ends // *before* loc.end, we need to fast-forward this.endTokenIndex first. while (this.endTokenIndex < loc.tokens.length) { var token = loc.tokens[this.endTokenIndex]; if (util.comparePos(token.loc.end, loc.end) < 0) { ++this.endTokenIndex; } else break; } // Increment this.startTokenIndex until we've found the first token // contained by this node. while (this.startTokenIndex < this.endTokenIndex) { var token = loc.tokens[this.startTokenIndex]; if (util.comparePos(token.loc.start, loc.start) < 0) { ++this.startTokenIndex; } else break; } // Index into loc.tokens of the first token within this node. loc.start.token = this.startTokenIndex; // Decrement this.endTokenIndex until we've found the first token after // this node (not contained by the node). while (this.endTokenIndex > this.startTokenIndex) { var token = loc.tokens[this.endTokenIndex - 1]; if (util.comparePos(loc.end, token.loc.end) < 0) { --this.endTokenIndex; } else break; } // Index into loc.tokens of the first token *after* this node. // If loc.start.token === loc.end.token, the node contains no tokens, // and the index is that of the next token following this node. loc.end.token = this.endTokenIndex; };