diff --git a/index.js b/index.js index 93e04786..c44bf89a 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ -const recast = require("recast"); const babylon = require("babylon"); const Printer = require("./src/printer").Printer; const flowParser = require("flow-parser"); +const comments = require("./src/comments"); var babylonOptions = { sourceType: 'module', @@ -40,17 +40,16 @@ module.exports = { } } else { - ast = recast.parse(text, { - parser: { - parse: function(source) { - return babylon.parse(source, babylonOptions); - } - } - }); + ast = babylon.parse(text, babylonOptions); } - opts.originalText = text; + // Interleave comment nodes + if(ast.comments) { + comments.attach(ast.comments, ast, text); + ast.comments = []; + } ast.tokens = []; + opts.originalText = text; const printer = new Printer(opts); return printer.printGenerically(ast).code; diff --git a/src/comments.js b/src/comments.js index 495947a0..3ae0c87e 100644 --- a/src/comments.js +++ b/src/comments.js @@ -13,26 +13,22 @@ var childNodesCacheKey = require("private").makeUniqueKey(); // TODO Move a non-caching implementation of this function into ast-types, // and implement a caching wrapper function here. -function getSortedChildNodes(node, lines, resultArray) { +function getSortedChildNodes(node, text, resultArray) { if (!node) { return; } - // The .loc checks below are sensitive to some of the problems that - // are fixed by this utility function. Specifically, if it decides to - // set node.loc to null, indicating that the node's .loc information - // is unreliable, then we don't want to add node to the resultArray. - util.fixFaultyLocations(node, lines); + // The loc checks below are sensitive to some of the problems that + // are fixed by this utility function. + util.fixFaultyLocations(node, text); if (resultArray) { - if (n.Node.check(node) && - n.SourceLocation.check(node.loc)) { + if (n.Node.check(node)) { // This reverse insertion sort almost always takes constant // time because we almost always (maybe always?) append the // nodes in order anyway. for (var i = resultArray.length - 1; i >= 0; --i) { - if (comparePos(resultArray[i].loc.end, - node.loc.start) <= 0) { + if (resultArray[i].end - node.start <= 0) { break; } } @@ -60,7 +56,7 @@ function getSortedChildNodes(node, lines, resultArray) { } for (var i = 0, nameCount = names.length; i < nameCount; ++i) { - getSortedChildNodes(node[names[i]], lines, resultArray); + getSortedChildNodes(node[names[i]], text, resultArray); } return resultArray; @@ -69,8 +65,8 @@ function getSortedChildNodes(node, lines, resultArray) { // As efficiently as possible, decorate the comment object with // .precedingNode, .enclosingNode, and/or .followingNode properties, at // least one of which is guaranteed to be defined. -function decorateComment(node, comment, lines) { - var childNodes = getSortedChildNodes(node, lines); +function decorateComment(node, comment, text) { + var childNodes = getSortedChildNodes(node, text); // Time to dust off the old binary search robes and wizard hat. var left = 0, right = childNodes.length; @@ -78,14 +74,13 @@ function decorateComment(node, comment, lines) { var middle = (left + right) >> 1; var child = childNodes[middle]; - if (comparePos(child.loc.start, comment.loc.start) <= 0 && - comparePos(comment.loc.end, child.loc.end) <= 0) { + if (child.start - comment.start <= 0 && comment.end - child.end <= 0) { // The comment is completely contained by this child node. - decorateComment(comment.enclosingNode = child, comment, lines); + decorateComment(comment.enclosingNode = child, comment, text); return; // Abandon the binary search at this level. } - if (comparePos(child.loc.end, comment.loc.start) <= 0) { + if (child.end - comment.start <= 0) { // This child node falls completely before the comment. // Because we will never consider this node or any nodes // before it again, this node must be the closest preceding @@ -95,7 +90,7 @@ function decorateComment(node, comment, lines) { continue; } - if (comparePos(comment.loc.end, child.loc.start) <= 0) { + if (comment.end - child.start <= 0) { // This child node falls completely after the comment. // Because we will never consider this node or any nodes after // it again, this node must be the closest following node we @@ -117,7 +112,7 @@ function decorateComment(node, comment, lines) { } } -exports.attach = function(comments, ast, lines) { +exports.attach = function(comments, ast, text) { if (!isArray.check(comments)) { return; } @@ -125,8 +120,7 @@ exports.attach = function(comments, ast, lines) { var tiesToBreak = []; comments.forEach(function(comment) { - comment.loc.lines = lines; - decorateComment(ast, comment, lines); + decorateComment(ast, comment, text); var pn = comment.precedingNode; var en = comment.enclosingNode; @@ -143,7 +137,7 @@ exports.attach = function(comments, ast, lines) { ); if (lastTie.followingNode !== comment.followingNode) { - breakTies(tiesToBreak, lines); + breakTies(tiesToBreak, text); } } @@ -151,18 +145,18 @@ exports.attach = function(comments, ast, lines) { } else if (pn) { // No contest: we have a trailing comment. - breakTies(tiesToBreak, lines); + breakTies(tiesToBreak, text); addTrailingComment(pn, comment); } else if (fn) { // No contest: we have a leading comment. - breakTies(tiesToBreak, lines); + breakTies(tiesToBreak, text); addLeadingComment(fn, comment); } else if (en) { // The enclosing node has no child nodes at all, so what we // have here is a dangling comment, e.g. [/* crickets */]. - breakTies(tiesToBreak, lines); + breakTies(tiesToBreak, text); addDanglingComment(en, comment); } else { @@ -170,7 +164,7 @@ exports.attach = function(comments, ast, lines) { } }); - breakTies(tiesToBreak, lines); + breakTies(tiesToBreak, text); comments.forEach(function(comment) { // These node references were useful for breaking ties, but we @@ -182,7 +176,7 @@ exports.attach = function(comments, ast, lines) { }); }; -function breakTies(tiesToBreak, lines) { +function breakTies(tiesToBreak, text) { var tieCount = tiesToBreak.length; if (tieCount === 0) { return; @@ -190,7 +184,7 @@ function breakTies(tiesToBreak, lines) { var pn = tiesToBreak[0].precedingNode; var fn = tiesToBreak[0].followingNode; - var gapEndPos = fn.loc.start; + var gapEndPos = fn.start; // Iterate backwards through tiesToBreak, examining the gaps // between the tied comments. In order to qualify as leading, a @@ -203,23 +197,23 @@ function breakTies(tiesToBreak, lines) { assert.strictEqual(comment.precedingNode, pn); assert.strictEqual(comment.followingNode, fn); - var gap = lines.sliceString(comment.loc.end, gapEndPos); + var gap = text.slice(comment.end, gapEndPos); if (/\S/.test(gap)) { // The gap string contained something other than whitespace. break; } - gapEndPos = comment.loc.start; + gapEndPos = comment.start; } - while (indexOfFirstLeadingComment <= tieCount && - (comment = tiesToBreak[indexOfFirstLeadingComment]) && - // If the comment is a //-style comment and indented more - // deeply than the node itself, reconsider it as trailing. - (comment.type === "Line" || comment.type === "CommentLine") && - comment.loc.start.column > fn.loc.start.column) { - ++indexOfFirstLeadingComment; - } + // while (indexOfFirstLeadingComment <= tieCount && + // (comment = tiesToBreak[indexOfFirstLeadingComment]) && + // // If the comment is a //-style comment and indented more + // // deeply than the node itself, reconsider it as trailing. + // (comment.type === "Line" || comment.type === "CommentLine") && + // comment.loc.start.column > fn.loc.start.column) { + // ++indexOfFirstLeadingComment; + // } tiesToBreak.forEach(function(comment, i) { if (i < indexOfFirstLeadingComment) { diff --git a/src/util.js b/src/util.js index ace0cfb6..4f9ed1a8 100644 --- a/src/util.js +++ b/src/util.js @@ -84,136 +84,55 @@ util.composeSourceMaps = function(formerMap, latterMap) { return smg.toJSON(); }; -util.getTrueLoc = function(node, lines) { - // It's possible that node is newly-created (not parsed by Esprima), - // in which case it probably won't have a .loc property (or an - // .original property for that matter). That's fine; we'll just - // pretty-print it as usual. - if (!node.loc) { - return null; +function expandLoc(parentNode, childNode) { + if (childNode.start - parentNode.start < 0) { + parentNode.start = childNode.start; } - var result = { - start: node.loc.start, - end: node.loc.end - }; - - function include(node) { - expandLoc(result, node.loc); - } - - // If the node has any comments, their locations might contribute to - // the true start/end positions of the node. - if (node.comments) { - node.comments.forEach(include); - } - - // If the node is an export declaration and its .declaration has any - // decorators, their locations might contribute to the true start/end - // positions of the export declaration node. - if (node.declaration && util.isExportDeclaration(node) && - node.declaration.decorators) { - node.declaration.decorators.forEach(include); - } - - if (comparePos(result.start, result.end) < 0) { - // Trim leading whitespace. - result.start = copyPos(result.start); - lines.skipSpaces(result.start, false, true); - - if (comparePos(result.start, result.end) < 0) { - // Trim trailing whitespace, if the end location is not already the - // same as the start location. - result.end = copyPos(result.end); - lines.skipSpaces(result.end, true, true); - } - } - - return result; -}; - -function expandLoc(parentLoc, childLoc) { - if (parentLoc && childLoc) { - if (comparePos(childLoc.start, parentLoc.start) < 0) { - parentLoc.start = childLoc.start; - } - - if (comparePos(parentLoc.end, childLoc.end) < 0) { - parentLoc.end = childLoc.end; - } + if (parentNode.end - childNode.end < 0) { + parentNode.end = childNode.end; } } -util.fixFaultyLocations = function(node, lines) { - var loc = node.loc; - if (loc) { - if (loc.start.line < 1) { - loc.start.line = 1; - } - - if (loc.end.line < 1) { - loc.end.line = 1; - } - } - +util.fixFaultyLocations = function(node, text) { if (node.type === "TemplateLiteral") { - fixTemplateLiteral(node, lines); + fixTemplateLiteral(node, text); - } else if (loc && node.decorators) { - // Expand the .loc of the node responsible for printing the decorators + } else if (node.decorators) { + // Expand the loc of the node responsible for printing the decorators // (here, the decorated node) so that it includes node.decorators. node.decorators.forEach(function (decorator) { - expandLoc(loc, decorator.loc); + expandLoc(node, decorator); }); - } else if (node.declaration && util.isExportDeclaration(node)) { - // Nullify .loc information for the child declaration so that we never - // try to reprint it without also reprinting the export declaration. - node.declaration.loc = null; - - // Expand the .loc of the node responsible for printing the decorators + // Expand the loc of the node responsible for printing the decorators // (here, the export declaration) so that it includes node.decorators. var decorators = node.declaration.decorators; if (decorators) { decorators.forEach(function (decorator) { - expandLoc(loc, decorator.loc); + expandLoc(node, decorator); }); } - } else if ((n.MethodDefinition && n.MethodDefinition.check(node)) || (n.Property.check(node) && (node.method || node.shorthand))) { - // If the node is a MethodDefinition or a .method or .shorthand - // Property, then the location information stored in - // node.value.loc is very likely untrustworthy (just the {body} - // part of a method, or nothing in the case of shorthand - // properties), so we null out that information to prevent - // accidental reuse of bogus source code during reprinting. - node.value.loc = null; - if (n.FunctionExpression.check(node.value)) { // FunctionExpression method values should be anonymous, // because their .id fields are ignored anyway. node.value.id = null; } - } else if (node.type === "ObjectTypeProperty") { - var loc = node.loc; - var end = loc && loc.end; - if (end) { - end = copyPos(end); - if (lines.prevPos(end) && - lines.charAt(end) === ",") { - // Some parsers accidentally include trailing commas in the - // .loc.end information for ObjectTypeProperty nodes. - if ((end = lines.skipSpaces(end, true, true))) { - loc.end = end; - } + var end = skipSpaces(text, node.end, true); + if (end !== false && text.charAt(end) === ",") { + // Some parsers accidentally include trailing commas in the + // .end information for ObjectTypeProperty nodes. + if ((end = skipSpaces(text, end - 1, true)) !== false) { + loc.end = end; } } } }; -function fixTemplateLiteral(node, lines) { +function fixTemplateLiteral(node, text) { assert.strictEqual(node.type, "TemplateLiteral"); if (node.quasis.length === 0) { @@ -221,53 +140,53 @@ function fixTemplateLiteral(node, lines) { return; } - // First we need to exclude the opening ` from the .loc of the first + // First we need to exclude the opening ` from the loc of the first // quasi element, in case the parser accidentally decided to include it. - var afterLeftBackTickPos = copyPos(node.loc.start); - assert.strictEqual(lines.charAt(afterLeftBackTickPos), "`"); - assert.ok(lines.nextPos(afterLeftBackTickPos)); + var afterLeftBackTickPos = node.start; + assert.strictEqual(text.charAt(afterLeftBackTickPos), "`"); + assert.ok(afterLeftBackTickPos < text.length); var firstQuasi = node.quasis[0]; - if (comparePos(firstQuasi.loc.start, afterLeftBackTickPos) < 0) { - firstQuasi.loc.start = afterLeftBackTickPos; + if (firstQuasi.start - afterLeftBackTickPos < 0) { + firstQuasi.start = afterLeftBackTickPos; } - // Next we need to exclude the closing ` from the .loc of the last quasi + // Next we need to exclude the closing ` from the loc of the last quasi // element, in case the parser accidentally decided to include it. - var rightBackTickPos = copyPos(node.loc.end); - assert.ok(lines.prevPos(rightBackTickPos)); - assert.strictEqual(lines.charAt(rightBackTickPos), "`"); + var rightBackTickPos = node.end; + assert.ok(rightBackTickPos >= 0); + assert.strictEqual(text.charAt(rightBackTickPos), "`"); var lastQuasi = node.quasis[node.quasis.length - 1]; - if (comparePos(rightBackTickPos, lastQuasi.loc.end) < 0) { - lastQuasi.loc.end = rightBackTickPos; + if (rightBackTickPos - lastQuasi.end < 0) { + lastQuasi.end = rightBackTickPos; } - // Now we need to exclude ${ and } characters from the .loc's of all + // Now we need to exclude ${ and } characters from the loc's of all // quasi elements, since some parsers accidentally include them. node.expressions.forEach(function (expr, i) { - // Rewind from expr.loc.start over any whitespace and the ${ that + // Rewind from expr.start over any whitespace and the ${ that // precedes the expression. The position of the $ should be the same - // as the .loc.end of the preceding quasi element, but some parsers - // accidentally include the ${ in the .loc of the quasi element. - var dollarCurlyPos = lines.skipSpaces(expr.loc.start, true, false); - if (lines.prevPos(dollarCurlyPos) && - lines.charAt(dollarCurlyPos) === "{" && - lines.prevPos(dollarCurlyPos) && - lines.charAt(dollarCurlyPos) === "$") { + // as the .end of the preceding quasi element, but some parsers + // accidentally include the ${ in the loc of the quasi element. + var dollarCurlyPos = skipSpaces(text, expr.start - 1, true); + if (dollarCurlyPos - 1 >= 0 && + text.charAt(dollarCurlyPos - 1) === "{" && + dollarCurlyPos - 2 >= 0 && + text.charAt(dollarCurlyPos - 2) === "$") { var quasiBefore = node.quasis[i]; - if (comparePos(dollarCurlyPos, quasiBefore.loc.end) < 0) { - quasiBefore.loc.end = dollarCurlyPos; + if (dollarCurlyPos - quasiBefore.end < 0) { + quasiBefore.end = dollarCurlyPos; } } // Likewise, some parsers accidentally include the } that follows - // the expression in the .loc of the following quasi element. - var rightCurlyPos = lines.skipSpaces(expr.loc.end, false, false); - if (lines.charAt(rightCurlyPos) === "}") { - assert.ok(lines.nextPos(rightCurlyPos)); + // the expression in the loc of the following quasi element. + var rightCurlyPos = skipSpaces(text, expr.end); + if (text.charAt(rightCurlyPos) === "}") { + assert.ok(rightCurlyPos + 1 < text.length); // Now rightCurlyPos is technically the position just after the }. var quasiAfter = node.quasis[i + 1]; - if (comparePos(quasiAfter.loc.start, rightCurlyPos) < 0) { - quasiAfter.loc.start = rightCurlyPos; + if (quasiAfter.start - rightCurlyPos < 0) { + quasiAfter.start = rightCurlyPos; } } }); @@ -337,3 +256,20 @@ util.newlineExistsBefore = function(text, index) { util.newlineExistsAfter = function(text, index) { return _findNewline(text, index); } + +function skipSpaces(text, index, backwards) { + const length = text.length; + let cursor = backwards ? (index - 1) : (index + 1); + // Look forward and see if there is a newline after/before this code + // by scanning up/back to the next non-indentation character. + while (cursor > 0 && cursor < length) { + const c = text.charAt(cursor); + // Skip any whitespace chars + if (!c.match(/\S/)) { + return cursor; + } + backwards ? cursor-- : cursor++; + } + return false; +} +util.skipSpaces = skipSpaces;