patcher.js 16 KB

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