UNPKG

lively.ast

Version:

Parsing JS code into ASTs and tools to query and transform these trees.

257 lines (224 loc) 9.04 kB
import { arr } from "lively.lang"; import { acorn, loose, addAstIndex, findNodesIncluding } from "./acorn-extension.js"; import { AllNodesVisitor } from "./visitors.js"; export { parse, parseFunction, fuzzyParse, addSource } acorn.walk.addSource = addSource; function addSource(parsed, source) { if (typeof parsed === "string") { source = parsed; parsed = parse(parsed); } source && AllNodesVisitor.run(parsed, (node, state, path) => !node.source && (node.source = source.slice(node.start, node.end))); return parsed; } function nodesAt(pos, ast) { ast = typeof ast === 'string' ? this.parse(ast) : ast; return findNodesIncluding(ast, pos); } function parseFunction(source, options = {}) { var src = '(' + source + ')', ast = parse(src, options); if (options.addSource) addSource(ast, src); return ast.body[0].expression; } function fuzzyParse(source, options) { // options: verbose, addSource, type options = options || {}; options.ecmaVersion = options.ecmaVersion || 8; options.sourceType = options.sourceType || "module"; options.plugins = options.plugins || {}; // if (options.plugins.hasOwnProperty("jsx")) options.plugins.jsx = options.plugins.jsx; options.plugins.asyncawait = options.plugins.hasOwnProperty("asyncawait") ? options.plugins.asyncawait : {inAsyncFunction: true}; options.plugins.objectSpread = options.plugins.hasOwnProperty("objectSpread") ? options.plugins.objectSpread : true; var ast, safeSource, err; if (options.type === 'LabeledStatement') { safeSource = '$={' + source + '}'; } try { // we only parse to find errors ast = parse(safeSource || source, options); if (safeSource) ast = null; // we parsed only for finding errors else if (options.addSource) addSource(ast, source); } catch (e) { err = e; } if (err && err.raisedAt !== undefined) { if (safeSource) { // fix error pos err.pos -= 3; err.raisedAt -= 3; err.loc.column -= 3; } var parseErrorSource = ''; parseErrorSource += source.slice(err.raisedAt - 20, err.raisedAt); parseErrorSource += '<-error->'; parseErrorSource += source.slice(err.raisedAt, err.raisedAt + 20); options.verbose && show('parse error: ' + parseErrorSource); err.parseErrorSource = parseErrorSource; } else if (err && options.verbose) { show('' + err + err.stack); } if (!ast) { ast = loose.parse_dammit(source, options); if (options.addSource) addSource(ast, source); ast.isFuzzy = true; ast.parseError = err; } return ast; } function acornParseAsyncAware(source, options) { var asyncSource = `async () => {\n${source}\n}`, offset = "async () => {\n".length; if (options.onComment) { var orig = options.onComment; options.onComment = function(isBlock, text, start, end, line, column) { start -= offset; end -= offset; return orig.call(this, isBlock, text, start, end, line, column); }; } var parsed = acorn.parse(asyncSource, options); if (parsed.loc) { var SourceLocation = parsed.loc.constructor; } parsed = {body: parsed.body[0].expression.body.body, sourceType: "module", type: "Program"}; AllNodesVisitor.run(parsed, (node, state, path) => { if (node._positionFixed) return; node._offsetFixed = true; if (node.start || node.start === 0) { node.start -= offset; node.end -= offset; } if (node.loc && SourceLocation) { var {start: {column: sc, line: sl}, end: {column: ec, line: el}} = node.loc; node.loc = new SourceLocation(options, {column: sc, line: sl-1}, {column: ec, line: el-1}); } if (options.addSource && (!node.source)) { node.source = source.slice(node.start, node.end); } }); parsed.start = parsed.body[0].start; parsed.end = arr.last(parsed.body).end; if (options.addSource) parsed.source = source; if (parsed.body[0].loc && SourceLocation) { parsed.loc = new SourceLocation(options, parsed.body[0].loc.start, arr.last(parsed.body).loc.end); } return parsed; } function parse(source, options) { // proxy function to acorn.parse. // Note that we will implement useful functionality on top of the pure // acorn interface and make it available here (such as more convenient // comment parsing). For using the pure acorn interface use the acorn // global. // See https://github.com/marijnh/acorn for full acorn doc and parse options. // options: { // addSource: BOOL, -- add source property to each node // addAstIndex: BOOL, -- each node gets an index number // withComments: BOOL, -- adds comment objects to Program/BlockStatements: // {isBlock: BOOL, text: STRING, node: NODE, // start: INTEGER, end: INTEGER, line: INTEGER, column: INTEGER} // ecmaVersion: 3|5|6, // allowReturnOutsideFunction: BOOL, -- Default is false // locations: BOOL -- Default is false // } options = options || {}; options.ecmaVersion = options.ecmaVersion || 8; options.sourceType = options.sourceType || "module"; if (!options.hasOwnProperty("allowImportExportEverywhere")) options.allowImportExportEverywhere = true; options.plugins = options.plugins || {}; options.plugins.asyncawait = options.plugins.hasOwnProperty("asyncawait") ? options.plugins.asyncawait : {inAsyncFunction: true}; options.plugins.objectSpread = options.plugins.hasOwnProperty("objectSpread") ? options.plugins.objectSpread : true; if (options.withComments) { // record comments delete options.withComments; var comments = []; options.onComment = function(isBlock, text, start, end, line, column) { comments.push({ isBlock: isBlock, text: text, node: null, start: start, end: end, line: line, column: column }); }; } try { var parsed = acorn.parse(source, options); } catch (err) { if (typeof SyntaxError !== "undefined" && err instanceof SyntaxError && err.loc) { var lines = source.split("\n"), {message, loc: {line: row, column}, pos} = err, line = lines[row-1], newMessage = `Syntax error at line ${row} column ${column} (index ${pos}) "${message}"\nsource: ${line.slice(0, column)}<--SyntaxError-->${line.slice(column)}`, betterErr = new SyntaxError(newMessage); betterErr.loc = {line: row, column}; betterErr.pos = pos; throw betterErr } else throw err; } if (options.addSource) addSource(parsed, source); if (options.addAstIndex && !parsed.hasOwnProperty('astIndex')) addAstIndex(parsed); if (parsed && comments) attachCommentsToAST({ast: parsed, comments: comments, nodesWithComments: []}); return parsed; // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- function attachCommentsToAST(commentData) { // for each comment: assign the comment to a block-level AST node commentData = mergeComments(assignCommentsToBlockNodes(commentData)); parsed.allComments = commentData.comments; } function assignCommentsToBlockNodes(commentData) { comments.forEach(function(comment) { var node = arr.detect( nodesAt(comment.start, parsed).reverse(), function(node) { return node.type === 'BlockStatement' || node.type === 'Program'; }); if (!node) node = parsed; if (!node.comments) node.comments = []; node.comments.push(comment); commentData.nodesWithComments.push(node); }); return commentData; } function mergeComments(commentData) { // coalesce non-block comments (multiple following lines of "// ...") into one comment. // This only happens if line comments aren't seperated by newlines commentData.nodesWithComments.forEach(function(blockNode) { arr.clone(blockNode.comments).reduce(function(coalesceData, comment) { if (comment.isBlock) { coalesceData.lastComment = null; return coalesceData; } if (!coalesceData.lastComment) { coalesceData.lastComment = comment; return coalesceData; } // if the comments are seperated by a statement, don't merge var last = coalesceData.lastComment; var nodeInbetween = arr.detect(blockNode.body, function(node) { return node.start >= last.end && node.end <= comment.start; }); if (nodeInbetween) { coalesceData.lastComment = comment; return coalesceData; } // if the comments are seperated by a newline, don't merge var codeInBetween = source.slice(last.end, comment.start); if (/[\n\r][\n\r]+/.test(codeInBetween)) { coalesceData.lastComment = comment; return coalesceData; } // merge comments into one last.text += "\n" + comment.text; last.end = comment.end; arr.remove(blockNode.comments, comment); arr.remove(commentData.comments, comment); return coalesceData; }, {lastComment: null}); }); return commentData; } }