You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

392 lines
15 KiB

5 years ago
  1. "use strict";
  2. var __importDefault = (this && this.__importDefault) || function (mod) {
  3. return (mod && mod.__esModule) ? mod : { "default": mod };
  4. };
  5. var __importStar = (this && this.__importStar) || function (mod) {
  6. if (mod && mod.__esModule) return mod;
  7. var result = {};
  8. if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
  9. result["default"] = mod;
  10. return result;
  11. };
  12. Object.defineProperty(exports, "__esModule", { value: true });
  13. var assert_1 = __importDefault(require("assert"));
  14. var linesModule = __importStar(require("./lines"));
  15. var types = __importStar(require("ast-types"));
  16. var Printable = types.namedTypes.Printable;
  17. var Expression = types.namedTypes.Expression;
  18. var ReturnStatement = types.namedTypes.ReturnStatement;
  19. var SourceLocation = types.namedTypes.SourceLocation;
  20. var util_1 = require("./util");
  21. var fast_path_1 = __importDefault(require("./fast-path"));
  22. var isObject = types.builtInTypes.object;
  23. var isArray = types.builtInTypes.array;
  24. var isString = types.builtInTypes.string;
  25. var riskyAdjoiningCharExp = /[0-9a-z_$]/i;
  26. var Patcher = function Patcher(lines) {
  27. assert_1.default.ok(this instanceof Patcher);
  28. assert_1.default.ok(lines instanceof linesModule.Lines);
  29. var self = this, replacements = [];
  30. self.replace = function (loc, lines) {
  31. if (isString.check(lines))
  32. lines = linesModule.fromString(lines);
  33. replacements.push({
  34. lines: lines,
  35. start: loc.start,
  36. end: loc.end
  37. });
  38. };
  39. self.get = function (loc) {
  40. // If no location is provided, return the complete Lines object.
  41. loc = loc || {
  42. start: { line: 1, column: 0 },
  43. end: { line: lines.length,
  44. column: lines.getLineLength(lines.length) }
  45. };
  46. var sliceFrom = loc.start, toConcat = [];
  47. function pushSlice(from, to) {
  48. assert_1.default.ok(util_1.comparePos(from, to) <= 0);
  49. toConcat.push(lines.slice(from, to));
  50. }
  51. replacements.sort(function (a, b) {
  52. return util_1.comparePos(a.start, b.start);
  53. }).forEach(function (rep) {
  54. if (util_1.comparePos(sliceFrom, rep.start) > 0) {
  55. // Ignore nested replacement ranges.
  56. }
  57. else {
  58. pushSlice(sliceFrom, rep.start);
  59. toConcat.push(rep.lines);
  60. sliceFrom = rep.end;
  61. }
  62. });
  63. pushSlice(sliceFrom, loc.end);
  64. return linesModule.concat(toConcat);
  65. };
  66. };
  67. exports.Patcher = Patcher;
  68. var Pp = Patcher.prototype;
  69. Pp.tryToReprintComments = function (newNode, oldNode, print) {
  70. var patcher = this;
  71. if (!newNode.comments &&
  72. !oldNode.comments) {
  73. // We were (vacuously) able to reprint all the comments!
  74. return true;
  75. }
  76. var newPath = fast_path_1.default.from(newNode);
  77. var oldPath = fast_path_1.default.from(oldNode);
  78. newPath.stack.push("comments", getSurroundingComments(newNode));
  79. oldPath.stack.push("comments", getSurroundingComments(oldNode));
  80. var reprints = [];
  81. var ableToReprintComments = findArrayReprints(newPath, oldPath, reprints);
  82. // No need to pop anything from newPath.stack or oldPath.stack, since
  83. // newPath and oldPath are fresh local variables.
  84. if (ableToReprintComments && reprints.length > 0) {
  85. reprints.forEach(function (reprint) {
  86. var oldComment = reprint.oldPath.getValue();
  87. assert_1.default.ok(oldComment.leading || oldComment.trailing);
  88. patcher.replace(oldComment.loc,
  89. // Comments can't have .comments, so it doesn't matter whether we
  90. // print with comments or without.
  91. print(reprint.newPath).indentTail(oldComment.loc.indent));
  92. });
  93. }
  94. return ableToReprintComments;
  95. };
  96. // Get all comments that are either leading or trailing, ignoring any
  97. // comments that occur inside node.loc. Returns an empty array for nodes
  98. // with no leading or trailing comments.
  99. function getSurroundingComments(node) {
  100. var result = [];
  101. if (node.comments &&
  102. node.comments.length > 0) {
  103. node.comments.forEach(function (comment) {
  104. if (comment.leading || comment.trailing) {
  105. result.push(comment);
  106. }
  107. });
  108. }
  109. return result;
  110. }
  111. Pp.deleteComments = function (node) {
  112. if (!node.comments) {
  113. return;
  114. }
  115. var patcher = this;
  116. node.comments.forEach(function (comment) {
  117. if (comment.leading) {
  118. // Delete leading comments along with any trailing whitespace they
  119. // might have.
  120. patcher.replace({
  121. start: comment.loc.start,
  122. end: node.loc.lines.skipSpaces(comment.loc.end, false, false)
  123. }, "");
  124. }
  125. else if (comment.trailing) {
  126. // Delete trailing comments along with any leading whitespace they
  127. // might have.
  128. patcher.replace({
  129. start: node.loc.lines.skipSpaces(comment.loc.start, true, false),
  130. end: comment.loc.end
  131. }, "");
  132. }
  133. });
  134. };
  135. function getReprinter(path) {
  136. assert_1.default.ok(path instanceof fast_path_1.default);
  137. // Make sure that this path refers specifically to a Node, rather than
  138. // some non-Node subproperty of a Node.
  139. var node = path.getValue();
  140. if (!Printable.check(node))
  141. return;
  142. var orig = node.original;
  143. var origLoc = orig && orig.loc;
  144. var lines = origLoc && origLoc.lines;
  145. var reprints = [];
  146. if (!lines || !findReprints(path, reprints))
  147. return;
  148. return function (print) {
  149. var patcher = new Patcher(lines);
  150. reprints.forEach(function (reprint) {
  151. var newNode = reprint.newPath.getValue();
  152. var oldNode = reprint.oldPath.getValue();
  153. SourceLocation.assert(oldNode.loc, true);
  154. var needToPrintNewPathWithComments = !patcher.tryToReprintComments(newNode, oldNode, print);
  155. if (needToPrintNewPathWithComments) {
  156. // Since we were not able to preserve all leading/trailing
  157. // comments, we delete oldNode's comments, print newPath with
  158. // comments, and then patch the resulting lines where oldNode used
  159. // to be.
  160. patcher.deleteComments(oldNode);
  161. }
  162. var newLines = print(reprint.newPath, {
  163. includeComments: needToPrintNewPathWithComments,
  164. // If the oldNode we're replacing already had parentheses, we may
  165. // not need to print the new node with any extra parentheses,
  166. // because the existing parentheses will suffice. However, if the
  167. // newNode has a different type than the oldNode, let the printer
  168. // decide if reprint.newPath needs parentheses, as usual.
  169. avoidRootParens: (oldNode.type === newNode.type &&
  170. reprint.oldPath.hasParens())
  171. }).indentTail(oldNode.loc.indent);
  172. var nls = needsLeadingSpace(lines, oldNode.loc, newLines);
  173. var nts = needsTrailingSpace(lines, oldNode.loc, newLines);
  174. // If we try to replace the argument of a ReturnStatement like
  175. // return"asdf" with e.g. a literal null expression, we run the risk
  176. // of ending up with returnnull, so we need to add an extra leading
  177. // space in situations where that might happen. Likewise for
  178. // "asdf"in obj. See #170.
  179. if (nls || nts) {
  180. var newParts = [];
  181. nls && newParts.push(" ");
  182. newParts.push(newLines);
  183. nts && newParts.push(" ");
  184. newLines = linesModule.concat(newParts);
  185. }
  186. patcher.replace(oldNode.loc, newLines);
  187. });
  188. // Recall that origLoc is the .loc of an ancestor node that is
  189. // guaranteed to contain all the reprinted nodes and comments.
  190. var patchedLines = patcher.get(origLoc).indentTail(-orig.loc.indent);
  191. if (path.needsParens()) {
  192. return linesModule.concat(["(", patchedLines, ")"]);
  193. }
  194. return patchedLines;
  195. };
  196. }
  197. exports.getReprinter = getReprinter;
  198. ;
  199. // If the last character before oldLoc and the first character of newLines
  200. // are both identifier characters, they must be separated by a space,
  201. // otherwise they will most likely get fused together into a single token.
  202. function needsLeadingSpace(oldLines, oldLoc, newLines) {
  203. var posBeforeOldLoc = util_1.copyPos(oldLoc.start);
  204. // The character just before the location occupied by oldNode.
  205. var charBeforeOldLoc = oldLines.prevPos(posBeforeOldLoc) &&
  206. oldLines.charAt(posBeforeOldLoc);
  207. // First character of the reprinted node.
  208. var newFirstChar = newLines.charAt(newLines.firstPos());
  209. return charBeforeOldLoc &&
  210. riskyAdjoiningCharExp.test(charBeforeOldLoc) &&
  211. newFirstChar &&
  212. riskyAdjoiningCharExp.test(newFirstChar);
  213. }
  214. // If the last character of newLines and the first character after oldLoc
  215. // are both identifier characters, they must be separated by a space,
  216. // otherwise they will most likely get fused together into a single token.
  217. function needsTrailingSpace(oldLines, oldLoc, newLines) {
  218. // The character just after the location occupied by oldNode.
  219. var charAfterOldLoc = oldLines.charAt(oldLoc.end);
  220. var newLastPos = newLines.lastPos();
  221. // Last character of the reprinted node.
  222. var newLastChar = newLines.prevPos(newLastPos) &&
  223. newLines.charAt(newLastPos);
  224. return newLastChar &&
  225. riskyAdjoiningCharExp.test(newLastChar) &&
  226. charAfterOldLoc &&
  227. riskyAdjoiningCharExp.test(charAfterOldLoc);
  228. }
  229. function findReprints(newPath, reprints) {
  230. var newNode = newPath.getValue();
  231. Printable.assert(newNode);
  232. var oldNode = newNode.original;
  233. Printable.assert(oldNode);
  234. assert_1.default.deepEqual(reprints, []);
  235. if (newNode.type !== oldNode.type) {
  236. return false;
  237. }
  238. var oldPath = new fast_path_1.default(oldNode);
  239. var canReprint = findChildReprints(newPath, oldPath, reprints);
  240. if (!canReprint) {
  241. // Make absolutely sure the calling code does not attempt to reprint
  242. // any nodes.
  243. reprints.length = 0;
  244. }
  245. return canReprint;
  246. }
  247. function findAnyReprints(newPath, oldPath, reprints) {
  248. var newNode = newPath.getValue();
  249. var oldNode = oldPath.getValue();
  250. if (newNode === oldNode)
  251. return true;
  252. if (isArray.check(newNode))
  253. return findArrayReprints(newPath, oldPath, reprints);
  254. if (isObject.check(newNode))
  255. return findObjectReprints(newPath, oldPath, reprints);
  256. return false;
  257. }
  258. function findArrayReprints(newPath, oldPath, reprints) {
  259. var newNode = newPath.getValue();
  260. var oldNode = oldPath.getValue();
  261. if (newNode === oldNode ||
  262. newPath.valueIsDuplicate() ||
  263. oldPath.valueIsDuplicate()) {
  264. return true;
  265. }
  266. isArray.assert(newNode);
  267. var len = newNode.length;
  268. if (!(isArray.check(oldNode) &&
  269. oldNode.length === len))
  270. return false;
  271. for (var i = 0; i < len; ++i) {
  272. newPath.stack.push(i, newNode[i]);
  273. oldPath.stack.push(i, oldNode[i]);
  274. var canReprint = findAnyReprints(newPath, oldPath, reprints);
  275. newPath.stack.length -= 2;
  276. oldPath.stack.length -= 2;
  277. if (!canReprint) {
  278. return false;
  279. }
  280. }
  281. return true;
  282. }
  283. function findObjectReprints(newPath, oldPath, reprints) {
  284. var newNode = newPath.getValue();
  285. isObject.assert(newNode);
  286. if (newNode.original === null) {
  287. // If newNode.original node was set to null, reprint the node.
  288. return false;
  289. }
  290. var oldNode = oldPath.getValue();
  291. if (!isObject.check(oldNode))
  292. return false;
  293. if (newNode === oldNode ||
  294. newPath.valueIsDuplicate() ||
  295. oldPath.valueIsDuplicate()) {
  296. return true;
  297. }
  298. if (Printable.check(newNode)) {
  299. if (!Printable.check(oldNode)) {
  300. return false;
  301. }
  302. // Here we need to decide whether the reprinted code for newNode is
  303. // appropriate for patching into the location of oldNode.
  304. if (newNode.type === oldNode.type) {
  305. var childReprints = [];
  306. if (findChildReprints(newPath, oldPath, childReprints)) {
  307. reprints.push.apply(reprints, childReprints);
  308. }
  309. else if (oldNode.loc) {
  310. // If we have no .loc information for oldNode, then we won't be
  311. // able to reprint it.
  312. reprints.push({
  313. oldPath: oldPath.copy(),
  314. newPath: newPath.copy()
  315. });
  316. }
  317. else {
  318. return false;
  319. }
  320. return true;
  321. }
  322. if (Expression.check(newNode) &&
  323. Expression.check(oldNode) &&
  324. // If we have no .loc information for oldNode, then we won't be
  325. // able to reprint it.
  326. oldNode.loc) {
  327. // If both nodes are subtypes of Expression, then we should be able
  328. // to fill the location occupied by the old node with code printed
  329. // for the new node with no ill consequences.
  330. reprints.push({
  331. oldPath: oldPath.copy(),
  332. newPath: newPath.copy()
  333. });
  334. return true;
  335. }
  336. // The nodes have different types, and at least one of the types is
  337. // not a subtype of the Expression type, so we cannot safely assume
  338. // the nodes are syntactically interchangeable.
  339. return false;
  340. }
  341. return findChildReprints(newPath, oldPath, reprints);
  342. }
  343. function findChildReprints(newPath, oldPath, reprints) {
  344. var newNode = newPath.getValue();
  345. var oldNode = oldPath.getValue();
  346. isObject.assert(newNode);
  347. isObject.assert(oldNode);
  348. if (newNode.original === null) {
  349. // If newNode.original node was set to null, reprint the node.
  350. return false;
  351. }
  352. // If this node needs parentheses and will not be wrapped with
  353. // parentheses when reprinted, then return false to skip reprinting and
  354. // let it be printed generically.
  355. if (newPath.needsParens() &&
  356. !oldPath.hasParens()) {
  357. return false;
  358. }
  359. var keys = util_1.getUnionOfKeys(oldNode, newNode);
  360. if (oldNode.type === "File" ||
  361. newNode.type === "File") {
  362. // Don't bother traversing file.tokens, an often very large array
  363. // returned by Babylon, and useless for our purposes.
  364. delete keys.tokens;
  365. }
  366. // Don't bother traversing .loc objects looking for reprintable nodes.
  367. delete keys.loc;
  368. var originalReprintCount = reprints.length;
  369. for (var k in keys) {
  370. if (k.charAt(0) === "_") {
  371. // Ignore "private" AST properties added by e.g. Babel plugins and
  372. // parsers like Babylon.
  373. continue;
  374. }
  375. newPath.stack.push(k, types.getFieldValue(newNode, k));
  376. oldPath.stack.push(k, types.getFieldValue(oldNode, k));
  377. var canReprint = findAnyReprints(newPath, oldPath, reprints);
  378. newPath.stack.length -= 2;
  379. oldPath.stack.length -= 2;
  380. if (!canReprint) {
  381. return false;
  382. }
  383. }
  384. // Return statements might end up running into ASI issues due to
  385. // comments inserted deep within the tree, so reprint them if anything
  386. // changed within them.
  387. if (ReturnStatement.check(newPath.getNode()) &&
  388. reprints.length > originalReprintCount) {
  389. return false;
  390. }
  391. return true;
  392. }