UNPKG

lively.ast

Version:

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

294 lines (241 loc) 10.3 kB
/*global process, global, exports*/ import { obj, string, Path, chain, arr, fun } from "lively.lang"; import { parse } from "./parser.js"; import { statementOf } from "./query.js"; import Visitor from "../generated/estree-visitor.js"; import stringify from "./stringify.js"; function getCommentPrecedingNode(parsed, node) { var statementPath = statementOf(parsed, node, {asPath: true}), blockPath = statementPath.slice(0, -2), block = Path(blockPath).get(parsed); return !block.comments || !block.comments.length ? null : chain(extractComments(parsed)) .reversed() .detect(function(ea) { return ea.followingNode === node; }) .value(); } function extractComments(astOrCode, optCode) { var parsed = typeof astOrCode === "string" ? parse(astOrCode, {withComments: true}) : astOrCode, code = optCode ? optCode : (typeof astOrCode === "string" ? astOrCode : stringify(astOrCode)), parsedComments = arr.sortBy(commentsWithPathsAndNodes(parsed), c => c.comment.start); return parsedComments.map(function(c, i) { // 1. a method comment like "x: function() {\n//foo\n ...}"? if (isInObjectMethod(c)) { return obj.merge([c, c.comment, {type: 'method', comment: c.comment.text}, methodAttributesOf(c)]); } if (isInComputedMethod(c)) { return obj.merge([c, c.comment, {type: 'method', comment: c.comment.text}, computedMethodAttributesOf(c)]); } // 2. function statement comment like "function foo() {\n//foo\n ...}"? if (isInFunctionStatement(c)) { return obj.merge([c, c.comment, {type: 'function', comment: c.comment.text}, functionAttributesOf(c)]); } // 3. assigned method like "foo.bar = function(x) {/*comment*/};" if (isInAssignedMethod(c)) { return obj.merge([c, c.comment, {type: 'method', comment: c.comment.text}, methodAttributesOfAssignment(c)]); } // 4. comment preceding another node? var followingNode = followingNodeOf(c); if (!followingNode) return obj.merge([c, c.comment, {followingNode:followingNode}, unknownComment(c)]); // is there another comment in front of the node> var followingComment = parsedComments[i+1]; if (followingComment && followingComment.comment.start <= followingNode.start) return obj.merge([c, c.comment, {followingNode:followingNode}, unknownComment(c)]); // 3. an obj var comment like "// foo\nvar obj = {...}"? if (isSingleObjVarDeclaration(followingNode)) { return obj.merge([c, c.comment, {followingNode:followingNode}, {type: 'object',comment: c.comment.text}, objAttributesOf(followingNode)]) } // 4. Is it a simple var declaration like "// foo\nvar obj = 23"? if (isSingleVarDeclaration(followingNode)) { return obj.merge([c, c.comment, {followingNode:followingNode}, {type: 'var',comment: c.comment.text}, objAttributesOf(followingNode)]) } return obj.merge([c, c.comment, {followingNode:followingNode}, unknownComment(c)]); }); // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- function commentsWithPathsAndNodes(parsed) { var comments = [], v = new Visitor() v.accept = fun.wrap(v.accept, (proceed, node, state, path) => { if (node.comments) { arr.pushAll(comments, node.comments.map(function(comment) { return {path: path, comment: comment, node: node}; })); } return proceed(node, state, path); }); v.accept(parsed, comments, []); return comments; } function followingNodeOf(comment) { return arr.detect(comment.node.body, function(node) { return node.start > comment.comment.end; }); } function unknownComment(comment) { return {type: "unknown", comment: comment.comment.text} } // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- function isInFunctionStatement(comment) { var node = Path(comment.path.slice(0,-1)).get(parsed); return node && node.type === "FunctionDeclaration"; } function functionAttributesOf(comment) { var funcNode = Path(comment.path.slice(0,-1)).get(parsed), name = funcNode.id ? funcNode.id.name : "<error: no name for function>"; return {name: name, args: arr.pluck(funcNode.params, "name")}; } // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- function isInObjectMethod(comment) { return arr.equals(comment.path.slice(-2), ["value", "body"]) // obj expr } function isInAssignedMethod(comment) { return arr.equals(comment.path.slice(-2), ["right", "body"]); // asignment } function methodAttributesOf(comment) { var methodNode = Path(comment.path.slice(0, -2)).get(parsed), name = methodNode.key ? methodNode.key.name : "<error: no name for method>"; // if it's someting like "var obj = {foo: function() {...}};" var p = comment.path.slice(), objectName = "<error: no object found for method>"; while (p.length && arr.last(p) !== 'init') p.pop(); if (p.length) { objectName = Path(p.slice(0, -1).concat(["id", "name"])).get(parsed); } // if it's someting like "exports.obj = {foo: function() {...}};" if (string.startsWith(objectName, "<error")) { p = comment.path.slice(); while (p.length && arr.last(p) !== 'right') p.pop(); if (p.length) { var assignNode = Path(p.slice(0, -1).concat(["left"])).get(parsed); objectName = code.slice(assignNode.start, assignNode.end); } } // if it's someting like "Object.extend(Foo.prototype, {m: function() {/*some comment*/ return 23; }})" if (string.startsWith(objectName, "<error")) { p = comment.path.slice(); var callExpr = Path(p.slice(0, -6)).get(parsed), isCall = callExpr && callExpr.type === "CallExpression", firstArg = isCall && callExpr.arguments[0]; if (firstArg) objectName = code.slice(firstArg.start, firstArg.end); } return { name: name, args: arr.pluck(methodNode.value.params, "name"), objectName: objectName } } function methodAttributesOfAssignment(comment) { var node = Path(comment.path.slice(0,-1)).get(parsed) if (node.type !== "FunctionExpression" && node.type !== "FunctionDeclaration") return {}; var statement = statementOf(parsed, node); if (statement.type !== "ExpressionStatement" || statement.expression.type !== "AssignmentExpression") return {}; var objName = code.slice( statement.expression.left.object.start, statement.expression.left.object.end); var methodName = code.slice( statement.expression.left.property.start, statement.expression.left.property.end); return { name: methodName, objectName: objName, args: arr.pluck(node.params, "name") }; } function isInComputedMethod(comment) { var path = comment.path.slice(-5); arr.removeAt(path, 1); return arr.equals(path, ["properties","value","callee","body"]); } function computedMethodAttributesOf(comment) { var name, args, pathToProp; pathToProp = comment.path.slice(0, -3); var propertyNode = Path(pathToProp).get(parsed); if (propertyNode && propertyNode.type === "Property") { // if it is a function immediatelly called args = arr.pluck(propertyNode.value.callee.params, "name"); name = propertyNode.key ? propertyNode.key.name : "<error: no name for method>"; } if (!name) { // if it is an object member function pathToProp = comment.path.slice(0, -2); propertyNode = Path(pathToProp).get(parsed); if (propertyNode && propertyNode.type === "Property") { args = arr.pluck(propertyNode.value.params, "name"); name = propertyNode.key ? propertyNode.key.name : "<error: no name for method>"; } } if (!name) { name = "<error: no name for method>"; args = []; pathToProp = comment.path } // if it's someting like "var obj = {foo: function() {...}};" var p = arr.clone(pathToProp); var objectName = "<error: no object found for method>"; while (p.length && arr.last(p) !== 'init') p.pop(); if (p.length) { objectName = Path(p.slice(0, -1).concat(["id", "name"])).get(parsed); } // if it's someting like "exports.obj = {foo: function() {...}};" if (string.startsWith(objectName, "<error")) { var p = arr.clone(pathToProp); while (p.length && arr.last(p) !== 'right') p.pop(); if (p.length) { var assignNode = Path(p.slice(0, -1).concat(["left"])).get(parsed); objectName = code.slice(assignNode.start, assignNode.end); } } // if it's someting like "Object.extend(Foo.prototype, {m: function() {/*some comment*/ return 23; }})" if (string.startsWith(objectName, "<error")) { var p = arr.clone(pathToProp); var callExpr = Path(p.slice(0, -4)).get(parsed), isCall = callExpr && callExpr.type === "CallExpression", firstArg = isCall && callExpr.arguments[0]; if (firstArg) objectName = code.slice(firstArg.start, firstArg.end); } return {name: name, args: args, objectName: objectName} } // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // like "var foo = {/*...*/}" or "var foo = bar = {/*...*/};" function isSingleObjVarDeclaration(node) { // should be a var declaration with one declarator with a value // being an JS object return isSingleVarDeclaration(node) && (node.declarations[0].init.type === "ObjectExpression" || isObjectAssignment(node.declarations[0].init)); } function isSingleVarDeclaration(node) { return node && node.type === 'VariableDeclaration' && node.declarations.length === 1; } function objAttributesOf(node) { return {name: node.declarations[0].id.name}; }; // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // like "foo = {/*...*/}" function isObjectAssignment(node) { if (node.type !== "AssignmentExpression") return false; if (node.right.type === "ObjectExpression") return true; if (node.right.type === "AssignmentExpression") return isObjectAssignment(node.right);; return false; } } export { getCommentPrecedingNode, extractComments }