comments.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. var assert = require("assert");
  2. var types = require("./types");
  3. var n = types.namedTypes;
  4. var isArray = types.builtInTypes.array;
  5. var isObject = types.builtInTypes.object;
  6. var linesModule = require("./lines");
  7. var fromString = linesModule.fromString;
  8. var Lines = linesModule.Lines;
  9. var concat = linesModule.concat;
  10. var util = require("./util");
  11. var comparePos = util.comparePos;
  12. var childNodesCacheKey = require("private").makeUniqueKey();
  13. // TODO Move a non-caching implementation of this function into ast-types,
  14. // and implement a caching wrapper function here.
  15. function getSortedChildNodes(node, lines, resultArray) {
  16. if (!node) {
  17. return;
  18. }
  19. // The .loc checks below are sensitive to some of the problems that
  20. // are fixed by this utility function. Specifically, if it decides to
  21. // set node.loc to null, indicating that the node's .loc information
  22. // is unreliable, then we don't want to add node to the resultArray.
  23. util.fixFaultyLocations(node, lines);
  24. if (resultArray) {
  25. if (n.Node.check(node) &&
  26. n.SourceLocation.check(node.loc)) {
  27. // This reverse insertion sort almost always takes constant
  28. // time because we almost always (maybe always?) append the
  29. // nodes in order anyway.
  30. for (var i = resultArray.length - 1; i >= 0; --i) {
  31. if (comparePos(resultArray[i].loc.end,
  32. node.loc.start) <= 0) {
  33. break;
  34. }
  35. }
  36. resultArray.splice(i + 1, 0, node);
  37. return;
  38. }
  39. } else if (node[childNodesCacheKey]) {
  40. return node[childNodesCacheKey];
  41. }
  42. var names;
  43. if (isArray.check(node)) {
  44. names = Object.keys(node);
  45. } else if (isObject.check(node)) {
  46. names = types.getFieldNames(node);
  47. } else {
  48. return;
  49. }
  50. if (!resultArray) {
  51. Object.defineProperty(node, childNodesCacheKey, {
  52. value: resultArray = [],
  53. enumerable: false
  54. });
  55. }
  56. for (var i = 0, nameCount = names.length; i < nameCount; ++i) {
  57. getSortedChildNodes(node[names[i]], lines, resultArray);
  58. }
  59. return resultArray;
  60. }
  61. // As efficiently as possible, decorate the comment object with
  62. // .precedingNode, .enclosingNode, and/or .followingNode properties, at
  63. // least one of which is guaranteed to be defined.
  64. function decorateComment(node, comment, lines) {
  65. var childNodes = getSortedChildNodes(node, lines);
  66. // Time to dust off the old binary search robes and wizard hat.
  67. var left = 0, right = childNodes.length;
  68. while (left < right) {
  69. var middle = (left + right) >> 1;
  70. var child = childNodes[middle];
  71. if (comparePos(child.loc.start, comment.loc.start) <= 0 &&
  72. comparePos(comment.loc.end, child.loc.end) <= 0) {
  73. // The comment is completely contained by this child node.
  74. decorateComment(comment.enclosingNode = child, comment, lines);
  75. return; // Abandon the binary search at this level.
  76. }
  77. if (comparePos(child.loc.end, comment.loc.start) <= 0) {
  78. // This child node falls completely before the comment.
  79. // Because we will never consider this node or any nodes
  80. // before it again, this node must be the closest preceding
  81. // node we have encountered so far.
  82. var precedingNode = child;
  83. left = middle + 1;
  84. continue;
  85. }
  86. if (comparePos(comment.loc.end, child.loc.start) <= 0) {
  87. // This child node falls completely after the comment.
  88. // Because we will never consider this node or any nodes after
  89. // it again, this node must be the closest following node we
  90. // have encountered so far.
  91. var followingNode = child;
  92. right = middle;
  93. continue;
  94. }
  95. throw new Error("Comment location overlaps with node location");
  96. }
  97. if (precedingNode) {
  98. comment.precedingNode = precedingNode;
  99. }
  100. if (followingNode) {
  101. comment.followingNode = followingNode;
  102. }
  103. }
  104. exports.attach = function(comments, ast, lines) {
  105. if (!isArray.check(comments)) {
  106. return;
  107. }
  108. var tiesToBreak = [];
  109. comments.forEach(function(comment) {
  110. comment.loc.lines = lines;
  111. decorateComment(ast, comment, lines);
  112. var pn = comment.precedingNode;
  113. var en = comment.enclosingNode;
  114. var fn = comment.followingNode;
  115. if (pn && fn) {
  116. var tieCount = tiesToBreak.length;
  117. if (tieCount > 0) {
  118. var lastTie = tiesToBreak[tieCount - 1];
  119. assert.strictEqual(
  120. lastTie.precedingNode === comment.precedingNode,
  121. lastTie.followingNode === comment.followingNode
  122. );
  123. if (lastTie.followingNode !== comment.followingNode) {
  124. breakTies(tiesToBreak, lines);
  125. }
  126. }
  127. tiesToBreak.push(comment);
  128. } else if (pn) {
  129. // No contest: we have a trailing comment.
  130. breakTies(tiesToBreak, lines);
  131. addTrailingComment(pn, comment);
  132. } else if (fn) {
  133. // No contest: we have a leading comment.
  134. breakTies(tiesToBreak, lines);
  135. addLeadingComment(fn, comment);
  136. } else if (en) {
  137. // The enclosing node has no child nodes at all, so what we
  138. // have here is a dangling comment, e.g. [/* crickets */].
  139. breakTies(tiesToBreak, lines);
  140. addDanglingComment(en, comment);
  141. } else {
  142. throw new Error("AST contains no nodes at all?");
  143. }
  144. });
  145. breakTies(tiesToBreak, lines);
  146. comments.forEach(function(comment) {
  147. // These node references were useful for breaking ties, but we
  148. // don't need them anymore, and they create cycles in the AST that
  149. // may lead to infinite recursion if we don't delete them here.
  150. delete comment.precedingNode;
  151. delete comment.enclosingNode;
  152. delete comment.followingNode;
  153. });
  154. };
  155. function breakTies(tiesToBreak, lines) {
  156. var tieCount = tiesToBreak.length;
  157. if (tieCount === 0) {
  158. return;
  159. }
  160. var pn = tiesToBreak[0].precedingNode;
  161. var fn = tiesToBreak[0].followingNode;
  162. var gapEndPos = fn.loc.start;
  163. // Iterate backwards through tiesToBreak, examining the gaps
  164. // between the tied comments. In order to qualify as leading, a
  165. // comment must be separated from fn by an unbroken series of
  166. // whitespace-only gaps (or other comments).
  167. for (var indexOfFirstLeadingComment = tieCount;
  168. indexOfFirstLeadingComment > 0;
  169. --indexOfFirstLeadingComment) {
  170. var comment = tiesToBreak[indexOfFirstLeadingComment - 1];
  171. assert.strictEqual(comment.precedingNode, pn);
  172. assert.strictEqual(comment.followingNode, fn);
  173. var gap = lines.sliceString(comment.loc.end, gapEndPos);
  174. if (/\S/.test(gap)) {
  175. // The gap string contained something other than whitespace.
  176. break;
  177. }
  178. gapEndPos = comment.loc.start;
  179. }
  180. while (indexOfFirstLeadingComment <= tieCount &&
  181. (comment = tiesToBreak[indexOfFirstLeadingComment]) &&
  182. // If the comment is a //-style comment and indented more
  183. // deeply than the node itself, reconsider it as trailing.
  184. (comment.type === "Line" || comment.type === "CommentLine") &&
  185. comment.loc.start.column > fn.loc.start.column) {
  186. ++indexOfFirstLeadingComment;
  187. }
  188. tiesToBreak.forEach(function(comment, i) {
  189. if (i < indexOfFirstLeadingComment) {
  190. addTrailingComment(pn, comment);
  191. } else {
  192. addLeadingComment(fn, comment);
  193. }
  194. });
  195. tiesToBreak.length = 0;
  196. }
  197. function addCommentHelper(node, comment) {
  198. var comments = node.comments || (node.comments = []);
  199. comments.push(comment);
  200. }
  201. function addLeadingComment(node, comment) {
  202. comment.leading = true;
  203. comment.trailing = false;
  204. addCommentHelper(node, comment);
  205. }
  206. function addDanglingComment(node, comment) {
  207. comment.leading = false;
  208. comment.trailing = false;
  209. addCommentHelper(node, comment);
  210. }
  211. function addTrailingComment(node, comment) {
  212. comment.leading = false;
  213. comment.trailing = true;
  214. addCommentHelper(node, comment);
  215. }
  216. function printLeadingComment(commentPath, print) {
  217. var comment = commentPath.getValue();
  218. n.Comment.assert(comment);
  219. var loc = comment.loc;
  220. var lines = loc && loc.lines;
  221. var parts = [print(commentPath)];
  222. if (comment.trailing) {
  223. // When we print trailing comments as leading comments, we don't
  224. // want to bring any trailing spaces along.
  225. parts.push("\n");
  226. } else if (lines instanceof Lines) {
  227. var trailingSpace = lines.slice(
  228. loc.end,
  229. lines.skipSpaces(loc.end)
  230. );
  231. if (trailingSpace.length === 1) {
  232. // If the trailing space contains no newlines, then we want to
  233. // preserve it exactly as we found it.
  234. parts.push(trailingSpace);
  235. } else {
  236. // If the trailing space contains newlines, then replace it
  237. // with just that many newlines, with all other spaces removed.
  238. parts.push(new Array(trailingSpace.length).join("\n"));
  239. }
  240. } else {
  241. parts.push("\n");
  242. }
  243. return concat(parts);
  244. }
  245. function printTrailingComment(commentPath, print) {
  246. var comment = commentPath.getValue(commentPath);
  247. n.Comment.assert(comment);
  248. var loc = comment.loc;
  249. var lines = loc && loc.lines;
  250. var parts = [];
  251. if (lines instanceof Lines) {
  252. var fromPos = lines.skipSpaces(loc.start, true) || lines.firstPos();
  253. var leadingSpace = lines.slice(fromPos, loc.start);
  254. if (leadingSpace.length === 1) {
  255. // If the leading space contains no newlines, then we want to
  256. // preserve it exactly as we found it.
  257. parts.push(leadingSpace);
  258. } else {
  259. // If the leading space contains newlines, then replace it
  260. // with just that many newlines, sans all other spaces.
  261. parts.push(new Array(leadingSpace.length).join("\n"));
  262. }
  263. }
  264. parts.push(print(commentPath));
  265. return concat(parts);
  266. }
  267. exports.printComments = function(path, print) {
  268. var value = path.getValue();
  269. var innerLines = print(path);
  270. var comments = n.Node.check(value) &&
  271. types.getFieldValue(value, "comments");
  272. if (!comments || comments.length === 0) {
  273. return innerLines;
  274. }
  275. var leadingParts = [];
  276. var trailingParts = [innerLines];
  277. path.each(function(commentPath) {
  278. var comment = commentPath.getValue();
  279. var leading = types.getFieldValue(comment, "leading");
  280. var trailing = types.getFieldValue(comment, "trailing");
  281. if (leading || (trailing && !(n.Statement.check(value) ||
  282. comment.type === "Block" ||
  283. comment.type === "CommentBlock"))) {
  284. leadingParts.push(printLeadingComment(commentPath, print));
  285. } else if (trailing) {
  286. trailingParts.push(printTrailingComment(commentPath, print));
  287. }
  288. }, "comments");
  289. leadingParts.push.apply(leadingParts, trailingParts);
  290. return concat(leadingParts);
  291. };