node-path.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. /**
  2. * The MIT License (MIT)
  3. * Copyright (c) 2017-present Dmitry Soshnikov <dmitry.soshnikov@gmail.com>
  4. */
  5. 'use strict';
  6. var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
  7. function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
  8. var DEFAULT_COLLECTION_PROP = 'expressions';
  9. var DEFAULT_SINGLE_PROP = 'expression';
  10. /**
  11. * NodePath class encapsulates a traversing node,
  12. * its parent node, property name in the parent node, and
  13. * an index (in case if a node is part of a collection).
  14. * It also provides set of methods for AST manipulation.
  15. */
  16. var NodePath = function () {
  17. /**
  18. * NodePath constructor.
  19. *
  20. * @param Object node - an AST node
  21. * @param NodePath parentPath - a nullable parent path
  22. * @param string property - property name of the node in the parent
  23. * @param number index - index of the node in a collection.
  24. */
  25. function NodePath(node) {
  26. var parentPath = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
  27. var property = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
  28. var index = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
  29. _classCallCheck(this, NodePath);
  30. this.node = node;
  31. this.parentPath = parentPath;
  32. this.parent = parentPath ? parentPath.node : null;
  33. this.property = property;
  34. this.index = index;
  35. }
  36. _createClass(NodePath, [{
  37. key: '_enforceProp',
  38. value: function _enforceProp(property) {
  39. if (!this.node.hasOwnProperty(property)) {
  40. throw new Error('Node of type ' + this.node.type + ' doesn\'t have "' + property + '" collection.');
  41. }
  42. }
  43. /**
  44. * Sets a node into a children collection or the single child.
  45. * By default child nodes are supposed to be under `expressions` property.
  46. * An explicit property can be passed.
  47. *
  48. * @param Object node - a node to set into a collection or as single child
  49. * @param number index - index at which to set
  50. * @param string property - name of the collection or single property
  51. */
  52. }, {
  53. key: 'setChild',
  54. value: function setChild(node) {
  55. var index = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
  56. var property = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
  57. var childPath = void 0;
  58. if (index != null) {
  59. if (!property) {
  60. property = DEFAULT_COLLECTION_PROP;
  61. }
  62. this._enforceProp(property);
  63. this.node[property][index] = node;
  64. childPath = NodePath.getForNode(node, this, property, index);
  65. } else {
  66. if (!property) {
  67. property = DEFAULT_SINGLE_PROP;
  68. }
  69. this._enforceProp(property);
  70. this.node[property] = node;
  71. childPath = NodePath.getForNode(node, this, property, null);
  72. }
  73. return childPath;
  74. }
  75. /**
  76. * Appends a node to a children collection.
  77. * By default child nodes are supposed to be under `expressions` property.
  78. * An explicit property can be passed.
  79. *
  80. * @param Object node - a node to set into a collection or as single child
  81. * @param string property - name of the collection or single property
  82. */
  83. }, {
  84. key: 'appendChild',
  85. value: function appendChild(node) {
  86. var property = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
  87. if (!property) {
  88. property = DEFAULT_COLLECTION_PROP;
  89. }
  90. this._enforceProp(property);
  91. var end = this.node[property].length;
  92. return this.setChild(node, end, property);
  93. }
  94. /**
  95. * Inserts a node into a collection.
  96. * By default child nodes are supposed to be under `expressions` property.
  97. * An explicit property can be passed.
  98. *
  99. * @param Object node - a node to insert into a collection
  100. * @param number index - index at which to insert
  101. * @param string property - name of the collection property
  102. */
  103. }, {
  104. key: 'insertChildAt',
  105. value: function insertChildAt(node, index) {
  106. var property = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : DEFAULT_COLLECTION_PROP;
  107. this._enforceProp(property);
  108. this.node[property].splice(index, 0, node);
  109. // If we inserted a node before the traversing index,
  110. // we should increase the later.
  111. if (index <= NodePath.getTraversingIndex()) {
  112. NodePath.updateTraversingIndex(+1);
  113. }
  114. this._rebuildIndex(this.node, property);
  115. }
  116. /**
  117. * Removes a node.
  118. */
  119. }, {
  120. key: 'remove',
  121. value: function remove() {
  122. if (this.isRemoved()) {
  123. return;
  124. }
  125. NodePath.registry.delete(this.node);
  126. this.node = null;
  127. if (!this.parent) {
  128. return;
  129. }
  130. // A node is in a collection.
  131. if (this.index !== null) {
  132. this.parent[this.property].splice(this.index, 1);
  133. // If we remove a node before the traversing index,
  134. // we should increase the later.
  135. if (this.index <= NodePath.getTraversingIndex()) {
  136. NodePath.updateTraversingIndex(-1);
  137. }
  138. // Rebuild index.
  139. this._rebuildIndex(this.parent, this.property);
  140. this.index = null;
  141. this.property = null;
  142. return;
  143. }
  144. // A simple node.
  145. delete this.parent[this.property];
  146. this.property = null;
  147. }
  148. /**
  149. * Rebuilds child nodes index (used on remove/insert).
  150. */
  151. }, {
  152. key: '_rebuildIndex',
  153. value: function _rebuildIndex(parent, property) {
  154. var parentPath = NodePath.getForNode(parent);
  155. for (var i = 0; i < parent[property].length; i++) {
  156. var path = NodePath.getForNode(parent[property][i], parentPath, property, i);
  157. path.index = i;
  158. }
  159. }
  160. /**
  161. * Whether the path was removed.
  162. */
  163. }, {
  164. key: 'isRemoved',
  165. value: function isRemoved() {
  166. return this.node === null;
  167. }
  168. /**
  169. * Replaces a node with the passed one.
  170. */
  171. }, {
  172. key: 'replace',
  173. value: function replace(newNode) {
  174. NodePath.registry.delete(this.node);
  175. this.node = newNode;
  176. if (!this.parent) {
  177. return null;
  178. }
  179. // A node is in a collection.
  180. if (this.index !== null) {
  181. this.parent[this.property][this.index] = newNode;
  182. }
  183. // A simple node.
  184. else {
  185. this.parent[this.property] = newNode;
  186. }
  187. // Rebuild the node path for the new node.
  188. return NodePath.getForNode(newNode, this.parentPath, this.property, this.index);
  189. }
  190. /**
  191. * Updates a node inline.
  192. */
  193. }, {
  194. key: 'update',
  195. value: function update(nodeProps) {
  196. Object.assign(this.node, nodeProps);
  197. }
  198. /**
  199. * Returns parent.
  200. */
  201. }, {
  202. key: 'getParent',
  203. value: function getParent() {
  204. return this.parentPath;
  205. }
  206. /**
  207. * Returns nth child.
  208. */
  209. }, {
  210. key: 'getChild',
  211. value: function getChild() {
  212. var n = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
  213. if (this.node.expressions) {
  214. return NodePath.getForNode(this.node.expressions[n], this, DEFAULT_COLLECTION_PROP, n);
  215. } else if (this.node.expression && n == 0) {
  216. return NodePath.getForNode(this.node.expression, this, DEFAULT_SINGLE_PROP);
  217. }
  218. return null;
  219. }
  220. /**
  221. * Whether a path node is syntactically equal to the passed one.
  222. *
  223. * NOTE: we don't rely on `source` property from the `loc` data
  224. * (which would be the fastest comparison), since it might be unsync
  225. * after several modifications. We use here simple `JSON.stringify`
  226. * excluding the `loc` data.
  227. *
  228. * @param NodePath other - path to compare to.
  229. * @return boolean
  230. */
  231. }, {
  232. key: 'hasEqualSource',
  233. value: function hasEqualSource(path) {
  234. return JSON.stringify(this.node, jsonSkipLoc) === JSON.stringify(path.node, jsonSkipLoc);
  235. }
  236. /**
  237. * JSON-encodes a node skipping location.
  238. */
  239. }, {
  240. key: 'jsonEncode',
  241. value: function jsonEncode() {
  242. var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
  243. format = _ref.format,
  244. useLoc = _ref.useLoc;
  245. return JSON.stringify(this.node, useLoc ? null : jsonSkipLoc, format);
  246. }
  247. /**
  248. * Returns previous sibling.
  249. */
  250. }, {
  251. key: 'getPreviousSibling',
  252. value: function getPreviousSibling() {
  253. if (!this.parent || this.index == null) {
  254. return null;
  255. }
  256. return NodePath.getForNode(this.parent[this.property][this.index - 1], NodePath.getForNode(this.parent), this.property, this.index - 1);
  257. }
  258. /**
  259. * Returns next sibling.
  260. */
  261. }, {
  262. key: 'getNextSibling',
  263. value: function getNextSibling() {
  264. if (!this.parent || this.index == null) {
  265. return null;
  266. }
  267. return NodePath.getForNode(this.parent[this.property][this.index + 1], NodePath.getForNode(this.parent), this.property, this.index + 1);
  268. }
  269. /**
  270. * Returns a NodePath instance for a node.
  271. *
  272. * The same NodePath can be reused in several places, e.g.
  273. * a parent node passed for all its children.
  274. */
  275. }], [{
  276. key: 'getForNode',
  277. value: function getForNode(node) {
  278. var parentPath = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
  279. var prop = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
  280. var index = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : -1;
  281. if (!node) {
  282. return null;
  283. }
  284. if (!NodePath.registry.has(node)) {
  285. NodePath.registry.set(node, new NodePath(node, parentPath, prop, index == -1 ? null : index));
  286. }
  287. var path = NodePath.registry.get(node);
  288. if (parentPath !== null) {
  289. path.parentPath = parentPath;
  290. path.parent = path.parentPath.node;
  291. }
  292. if (prop !== null) {
  293. path.property = prop;
  294. }
  295. if (index >= 0) {
  296. path.index = index;
  297. }
  298. return path;
  299. }
  300. /**
  301. * Initializes the NodePath registry. The registry is a map from
  302. * a node to its NodePath instance.
  303. */
  304. }, {
  305. key: 'initRegistry',
  306. value: function initRegistry() {
  307. if (!NodePath.registry) {
  308. NodePath.registry = new Map();
  309. }
  310. NodePath.registry.clear();
  311. }
  312. /**
  313. * Updates index of a currently traversing collection.
  314. */
  315. }, {
  316. key: 'updateTraversingIndex',
  317. value: function updateTraversingIndex(dx) {
  318. return NodePath.traversingIndexStack[NodePath.traversingIndexStack.length - 1] += dx;
  319. }
  320. /**
  321. * Returns current traversing index.
  322. */
  323. }, {
  324. key: 'getTraversingIndex',
  325. value: function getTraversingIndex() {
  326. return NodePath.traversingIndexStack[NodePath.traversingIndexStack.length - 1];
  327. }
  328. }]);
  329. return NodePath;
  330. }();
  331. NodePath.initRegistry();
  332. /**
  333. * Index of a currently traversing collection is stored on top of the
  334. * `NodePath.traversingIndexStack`. Remove/insert methods can adjust
  335. * this index.
  336. */
  337. NodePath.traversingIndexStack = [];
  338. // Helper function used to skip `loc` in JSON operations.
  339. function jsonSkipLoc(prop, value) {
  340. if (prop === 'loc') {
  341. return undefined;
  342. }
  343. return value;
  344. }
  345. module.exports = NodePath;