recast-harmony
Version:
JavaScript syntax tree transformer, conservative pretty-printer, and automatic source map generator
298 lines (247 loc) • 9.26 kB
JavaScript
var assert = require("assert");
var types = require("./types");
var isArray = types.builtInTypes.array;
var isObject = types.builtInTypes.object;
var linesModule = require("./lines");
var fromString = linesModule.fromString;
var Lines = linesModule.Lines;
var concat = linesModule.concat;
var comparePos = require("./util").comparePos;
exports.add = function(ast, lines) {
var comments = ast.comments;
if (!isArray.check(comments)) {
return;
}
delete ast.comments;
assert.ok(lines instanceof Lines);
var pt = new PosTracker,
len = comments.length,
comment,
key,
loc, locs = pt.locs,
pair,
sorted = [];
pt.visit(ast);
for (var i = 0; i < len; ++i) {
comment = comments[i];
Object.defineProperty(comment.loc, "lines", { value: lines });
pt.getEntry(comment, "end").comment = comment;
}
for (key in locs) {
loc = locs[key];
pair = key.split(",");
sorted.push({
line: +pair[0],
column: +pair[1],
startNode: loc.startNode,
endNode: loc.endNode,
comment: loc.comment
});
}
sorted.sort(comparePos);
var pendingComments = [];
var previousNode;
function addComment(node, comment) {
if (node) {
var comments = node.comments || (node.comments = []);
comments.push(comment);
}
}
function dumpTrailing() {
pendingComments.forEach(function(comment) {
addComment(previousNode, comment);
comment.trailing = true;
});
pendingComments.length = 0;
}
sorted.forEach(function(entry) {
if (entry.endNode) {
// If we're ending a node with comments still pending, then we
// need to attach those comments to the previous node before
// updating the previous node.
dumpTrailing();
previousNode = entry.endNode;
}
if (entry.comment) {
pendingComments.push(entry.comment);
}
if (entry.startNode) {
var node = entry.startNode;
var nodeStartColumn = node.loc.start.column;
var didAddLeadingComment = false;
var gapEndLoc = node.loc.start;
// Iterate backwards through pendingComments, examining the
// gaps between them. In order to earn the .possiblyLeading
// status, a comment must be separated from entry.startNode by
// an unbroken series of whitespace-only gaps.
for (var i = pendingComments.length - 1; i >= 0; --i) {
var comment = pendingComments[i];
var gap = lines.slice(comment.loc.end, gapEndLoc);
gapEndLoc = comment.loc.start;
if (gap.isOnlyWhitespace()) {
comment.possiblyLeading = true;
} else {
break;
}
}
pendingComments.forEach(function(comment) {
if (!comment.possiblyLeading) {
// If comment.possiblyLeading was not set to true
// above, the comment must be a trailing comment.
comment.trailing = true;
addComment(previousNode, comment);
} else if (didAddLeadingComment) {
// If we previously added a leading comment to this
// node, then any subsequent pending comments must
// also be leading comments, even if they are indented
// more deeply than the node itself.
assert.strictEqual(comment.possiblyLeading, true);
comment.trailing = false;
addComment(node, comment);
} else if (comment.type === "Line" &&
comment.loc.start.column > nodeStartColumn) {
// If the comment is a //-style comment and indented
// more deeply than the node itself, and we have not
// encountered any other leading comments, treat this
// comment as a trailing comment and add it to the
// previous node.
comment.trailing = true;
addComment(previousNode, comment);
} else {
// Here we have the first leading comment for this node.
comment.trailing = false;
addComment(node, comment);
didAddLeadingComment = true;
}
});
pendingComments.length = 0;
// Note: the previous node is the node that started OR ended
// most recently.
previousNode = entry.startNode;
}
});
// Provided we have a previous node to add them to, dump any
// still-pending comments into the last node we came across.
dumpTrailing();
};
function PosTracker() {
assert.ok(this instanceof PosTracker);
this.locs = {};
}
var PTp = PosTracker.prototype;
PTp.getEntry = function(node, which) {
var locs = this.locs,
loc = node && node.loc,
pos = loc && loc[which],
key = pos && (pos.line + "," + pos.column);
return key && (locs[key] || (locs[key] = {}));
};
PTp.visit = function(node) {
if (isArray.check(node)) {
node.forEach(this.visit, this);
} else if (isObject.check(node)) {
var entry = this.getEntry(node, "start");
if (entry && !entry.startNode) {
entry.startNode = node;
}
var names = types.getFieldNames(node);
for (var i = 0, len = names.length; i < len; ++i) {
this.visit(node[names[i]]);
}
if ((entry = this.getEntry(node, "end"))) {
entry.endNode = node;
}
}
};
function printLeadingComment(comment) {
var orig = comment.original;
var loc = orig && orig.loc;
var lines = loc && loc.lines;
var parts = [];
if (comment.type === "Block") {
parts.push("/*", comment.value, "*/");
} else if (comment.type === "Line") {
parts.push("//", comment.value);
} else assert.fail(comment.type);
if (comment.trailing) {
// When we print trailing comments as leading comments, we don't
// want to bring any trailing spaces along.
parts.push("\n");
} else if (lines instanceof Lines) {
var trailingSpace = lines.slice(
loc.end,
lines.skipSpaces(loc.end)
);
if (trailingSpace.length === 1) {
// If the trailing space contains no newlines, then we want to
// preserve it exactly as we found it.
parts.push(trailingSpace);
} else {
// If the trailing space contains newlines, then replace it
// with just that many newlines, with all other spaces removed.
parts.push(new Array(trailingSpace.length).join("\n"));
}
} else {
parts.push("\n");
}
return concat(parts).stripMargin(loc ? loc.start.column : 0);
}
function printTrailingComment(comment) {
var orig = comment.original;
var loc = orig && orig.loc;
var lines = loc && loc.lines;
var parts = [];
if (lines instanceof Lines) {
var fromPos = lines.skipSpaces(loc.start, true) || lines.firstPos();
var leadingSpace = lines.slice(fromPos, loc.start);
if (leadingSpace.length === 1) {
// If the leading space contains no newlines, then we want to
// preserve it exactly as we found it.
parts.push(leadingSpace);
} else {
// If the leading space contains newlines, then replace it
// with just that many newlines, sans all other spaces.
parts.push(new Array(leadingSpace.length).join("\n"));
}
}
if (comment.type === "Block") {
parts.push("/*", comment.value, "*/");
} else if (comment.type === "Line") {
parts.push("//", comment.value, "\n");
} else assert.fail(comment.type);
return concat(parts).stripMargin(
loc ? loc.start.column : 0,
true // Skip the first line, in case there were leading spaces.
);
}
exports.printComments = function(comments, innerLines) {
if (innerLines) {
assert.ok(innerLines instanceof Lines);
} else {
innerLines = fromString("");
}
var count = comments ? comments.length : 0;
if (count === 0) {
return innerLines;
}
var parts = [];
var leading = [];
var trailing = [];
comments.forEach(function(comment) {
// For now, only /*comments*/ can be trailing comments.
if (comment.type === "Block" &&
comment.trailing) {
trailing.push(comment);
} else {
leading.push(comment);
}
});
leading.forEach(function(comment) {
parts.push(printLeadingComment(comment));
});
parts.push(innerLines);
trailing.forEach(function(comment) {
parts.push(printTrailingComment(comment));
});
return concat(parts);
};