Collection.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. /*
  2. * Copyright (c) 2015-present, Facebook, Inc.
  3. * All rights reserved.
  4. *
  5. * This source code is licensed under the BSD-style license found in the
  6. * LICENSE file in the root directory of this source tree. An additional grant
  7. * of patent rights can be found in the PATENTS file in the same directory.
  8. *
  9. */
  10. 'use strict';
  11. const assert = require('assert');
  12. const recast = require('recast');
  13. const _ = require('lodash');
  14. const astTypes = recast.types;
  15. var types = astTypes.namedTypes;
  16. const NodePath = astTypes.NodePath;
  17. const Node = types.Node;
  18. /**
  19. * This represents a generic collection of node paths. It only has a generic
  20. * API to access and process the elements of the list. It doesn't know anything
  21. * about AST types.
  22. *
  23. * @mixes traversalMethods
  24. * @mixes mutationMethods
  25. * @mixes transformMethods
  26. * @mixes globalMethods
  27. */
  28. class Collection {
  29. /**
  30. * @param {Array} paths An array of AST paths
  31. * @param {Collection} parent A parent collection
  32. * @param {Array} types An array of types all the paths in the collection
  33. * have in common. If not passed, it will be inferred from the paths.
  34. * @return {Collection}
  35. */
  36. constructor(paths, parent, types) {
  37. assert.ok(Array.isArray(paths), 'Collection is passed an array');
  38. assert.ok(
  39. paths.every(p => p instanceof NodePath),
  40. 'Array contains only paths'
  41. );
  42. this._parent = parent;
  43. this.__paths = paths;
  44. if (types && !Array.isArray(types)) {
  45. types = _toTypeArray(types);
  46. } else if (!types || Array.isArray(types) && types.length === 0) {
  47. types = _inferTypes(paths);
  48. }
  49. this._types = types.length === 0 ? _defaultType : types;
  50. }
  51. /**
  52. * Returns a new collection containing the nodes for which the callback
  53. * returns true.
  54. *
  55. * @param {function} callback
  56. * @return {Collection}
  57. */
  58. filter(callback) {
  59. return new this.constructor(this.__paths.filter(callback), this);
  60. }
  61. /**
  62. * Executes callback for each node/path in the collection.
  63. *
  64. * @param {function} callback
  65. * @return {Collection} The collection itself
  66. */
  67. forEach(callback) {
  68. this.__paths.forEach(
  69. (path, i, paths) => callback.call(path, path, i, paths)
  70. );
  71. return this;
  72. }
  73. /**
  74. * Tests whether at-least one path passes the test implemented by the provided callback.
  75. *
  76. * @param {function} callback
  77. * @return {boolean}
  78. */
  79. some(callback) {
  80. return this.__paths.some(
  81. (path, i, paths) => callback.call(path, path, i, paths)
  82. );
  83. }
  84. /**
  85. * Tests whether all paths pass the test implemented by the provided callback.
  86. *
  87. * @param {function} callback
  88. * @return {boolean}
  89. */
  90. every(callback) {
  91. return this.__paths.every(
  92. (path, i, paths) => callback.call(path, path, i, paths)
  93. );
  94. }
  95. /**
  96. * Executes the callback for every path in the collection and returns a new
  97. * collection from the return values (which must be paths).
  98. *
  99. * The callback can return null to indicate to exclude the element from the
  100. * new collection.
  101. *
  102. * If an array is returned, the array will be flattened into the result
  103. * collection.
  104. *
  105. * @param {function} callback
  106. * @param {Type} type Force the new collection to be of a specific type
  107. */
  108. map(callback, type) {
  109. const paths = [];
  110. this.forEach(function(path) {
  111. /*jshint eqnull:true*/
  112. let result = callback.apply(path, arguments);
  113. if (result == null) return;
  114. if (!Array.isArray(result)) {
  115. result = [result];
  116. }
  117. for (let i = 0; i < result.length; i++) {
  118. if (paths.indexOf(result[i]) === -1) {
  119. paths.push(result[i]);
  120. }
  121. }
  122. });
  123. return fromPaths(paths, this, type);
  124. }
  125. /**
  126. * Returns the number of elements in this collection.
  127. *
  128. * @return {number}
  129. */
  130. size() {
  131. return this.__paths.length;
  132. }
  133. /**
  134. * Returns the number of elements in this collection.
  135. *
  136. * @return {number}
  137. */
  138. get length() {
  139. return this.__paths.length;
  140. }
  141. /**
  142. * Returns an array of AST nodes in this collection.
  143. *
  144. * @return {Array}
  145. */
  146. nodes() {
  147. return this.__paths.map(p => p.value);
  148. }
  149. paths() {
  150. return this.__paths;
  151. }
  152. getAST() {
  153. if (this._parent) {
  154. return this._parent.getAST();
  155. }
  156. return this.__paths;
  157. }
  158. toSource(options) {
  159. if (this._parent) {
  160. return this._parent.toSource(options);
  161. }
  162. if (this.__paths.length === 1) {
  163. return recast.print(this.__paths[0], options).code;
  164. } else {
  165. return this.__paths.map(p => recast.print(p, options).code);
  166. }
  167. }
  168. /**
  169. * Returns a new collection containing only the element at position index.
  170. *
  171. * In case of a negative index, the element is taken from the end:
  172. *
  173. * .at(0) - first element
  174. * .at(-1) - last element
  175. *
  176. * @param {number} index
  177. * @return {Collection}
  178. */
  179. at(index) {
  180. return fromPaths(
  181. this.__paths.slice(
  182. index,
  183. index === -1 ? undefined : index + 1
  184. ),
  185. this
  186. );
  187. }
  188. /**
  189. * Proxies to NodePath#get of the first path.
  190. *
  191. * @param {string|number} ...fields
  192. */
  193. get() {
  194. const path = this.__paths[0];
  195. if (!path) {
  196. throw Error(
  197. 'You cannot call "get" on a collection with no paths. ' +
  198. 'Instead, check the "length" property first to verify at least 1 path exists.'
  199. );
  200. }
  201. return path.get.apply(path, arguments);
  202. }
  203. /**
  204. * Returns the type(s) of the collection. This is only used for unit tests,
  205. * I don't think other consumers would need it.
  206. *
  207. * @return {Array<string>}
  208. */
  209. getTypes() {
  210. return this._types;
  211. }
  212. /**
  213. * Returns true if this collection has the type 'type'.
  214. *
  215. * @param {Type} type
  216. * @return {boolean}
  217. */
  218. isOfType(type) {
  219. return !!type && this._types.indexOf(type.toString()) > -1;
  220. }
  221. }
  222. /**
  223. * Given a set of paths, this infers the common types of all paths.
  224. * @private
  225. * @param {Array} paths An array of paths.
  226. * @return {Type} type An AST type
  227. */
  228. function _inferTypes(paths) {
  229. let _types = [];
  230. if (paths.length > 0 && Node.check(paths[0].node)) {
  231. const nodeType = types[paths[0].node.type];
  232. const sameType = paths.length === 1 ||
  233. paths.every(path => nodeType.check(path.node));
  234. if (sameType) {
  235. _types = [nodeType.toString()].concat(
  236. astTypes.getSupertypeNames(nodeType.toString())
  237. );
  238. } else {
  239. // try to find a common type
  240. _types = _.intersection.apply(
  241. null,
  242. paths.map(path => astTypes.getSupertypeNames(path.node.type))
  243. );
  244. }
  245. }
  246. return _types;
  247. }
  248. function _toTypeArray(value) {
  249. value = !Array.isArray(value) ? [value] : value;
  250. value = value.map(v => v.toString());
  251. if (value.length > 1) {
  252. return _.union(value, _.intersection.apply(
  253. null,
  254. value.map(type => astTypes.getSupertypeNames(type))
  255. ));
  256. } else {
  257. return value.concat(astTypes.getSupertypeNames(value[0]));
  258. }
  259. }
  260. /**
  261. * Creates a new collection from an array of node paths.
  262. *
  263. * If type is passed, it will create a typed collection if such a collection
  264. * exists. The nodes or path values must be of the same type.
  265. *
  266. * Otherwise it will try to infer the type from the path list. If every
  267. * element has the same type, a typed collection is created (if it exists),
  268. * otherwise, a generic collection will be created.
  269. *
  270. * @ignore
  271. * @param {Array} paths An array of paths
  272. * @param {Collection} parent A parent collection
  273. * @param {Type} type An AST type
  274. * @return {Collection}
  275. */
  276. function fromPaths(paths, parent, type) {
  277. assert.ok(
  278. paths.every(n => n instanceof NodePath),
  279. 'Every element in the array should be a NodePath'
  280. );
  281. return new Collection(paths, parent, type);
  282. }
  283. /**
  284. * Creates a new collection from an array of nodes. This is a convenience
  285. * method which converts the nodes to node paths first and calls
  286. *
  287. * Collections.fromPaths(paths, parent, type)
  288. *
  289. * @ignore
  290. * @param {Array} nodes An array of AST nodes
  291. * @param {Collection} parent A parent collection
  292. * @param {Type} type An AST type
  293. * @return {Collection}
  294. */
  295. function fromNodes(nodes, parent, type) {
  296. assert.ok(
  297. nodes.every(n => Node.check(n)),
  298. 'Every element in the array should be a Node'
  299. );
  300. return fromPaths(
  301. nodes.map(n => new NodePath(n)),
  302. parent,
  303. type
  304. );
  305. }
  306. const CPt = Collection.prototype;
  307. /**
  308. * This function adds the provided methods to the prototype of the corresponding
  309. * typed collection. If no type is passed, the methods are added to
  310. * Collection.prototype and are available for all collections.
  311. *
  312. * @param {Object} methods Methods to add to the prototype
  313. * @param {Type=} type Optional type to add the methods to
  314. */
  315. function registerMethods(methods, type) {
  316. for (const methodName in methods) {
  317. if (!methods.hasOwnProperty(methodName)) {
  318. return;
  319. }
  320. if (hasConflictingRegistration(methodName, type)) {
  321. let msg = `There is a conflicting registration for method with name "${methodName}".\nYou tried to register an additional method with `;
  322. if (type) {
  323. msg += `type "${type.toString()}".`
  324. } else {
  325. msg += 'universal type.'
  326. }
  327. msg += '\nThere are existing registrations for that method with ';
  328. const conflictingRegistrations = CPt[methodName].typedRegistrations;
  329. if (conflictingRegistrations) {
  330. msg += `type ${Object.keys(conflictingRegistrations).join(', ')}.`;
  331. } else {
  332. msg += 'universal type.';
  333. }
  334. throw Error(msg);
  335. }
  336. if (!type) {
  337. CPt[methodName] = methods[methodName];
  338. } else {
  339. type = type.toString();
  340. if (!CPt.hasOwnProperty(methodName)) {
  341. installTypedMethod(methodName);
  342. }
  343. var registrations = CPt[methodName].typedRegistrations;
  344. registrations[type] = methods[methodName];
  345. astTypes.getSupertypeNames(type).forEach(function (name) {
  346. registrations[name] = false;
  347. });
  348. }
  349. }
  350. }
  351. function installTypedMethod(methodName) {
  352. if (CPt.hasOwnProperty(methodName)) {
  353. throw new Error(`Internal Error: "${methodName}" method is already installed`);
  354. }
  355. const registrations = {};
  356. function typedMethod() {
  357. const types = Object.keys(registrations);
  358. for (let i = 0; i < types.length; i++) {
  359. const currentType = types[i];
  360. if (registrations[currentType] && this.isOfType(currentType)) {
  361. return registrations[currentType].apply(this, arguments);
  362. }
  363. }
  364. throw Error(
  365. `You have a collection of type [${this.getTypes()}]. ` +
  366. `"${methodName}" is only defined for one of [${types.join('|')}].`
  367. );
  368. }
  369. typedMethod.typedRegistrations = registrations;
  370. CPt[methodName] = typedMethod;
  371. }
  372. function hasConflictingRegistration(methodName, type) {
  373. if (!type) {
  374. return CPt.hasOwnProperty(methodName);
  375. }
  376. if (!CPt.hasOwnProperty(methodName)) {
  377. return false;
  378. }
  379. const registrations = CPt[methodName] && CPt[methodName].typedRegistrations;
  380. if (!registrations) {
  381. return true;
  382. }
  383. type = type.toString();
  384. if (registrations.hasOwnProperty(type)) {
  385. return true;
  386. }
  387. return astTypes.getSupertypeNames(type.toString()).some(function (name) {
  388. return !!registrations[name];
  389. });
  390. }
  391. var _defaultType = [];
  392. /**
  393. * Sets the default collection type. In case a collection is created form an
  394. * empty set of paths and no type is specified, we return a collection of this
  395. * type.
  396. *
  397. * @ignore
  398. * @param {Type} type
  399. */
  400. function setDefaultCollectionType(type) {
  401. _defaultType = _toTypeArray(type);
  402. }
  403. exports.fromPaths = fromPaths;
  404. exports.fromNodes = fromNodes;
  405. exports.registerMethods = registerMethods;
  406. exports.hasConflictingRegistration = hasConflictingRegistration;
  407. exports.setDefaultCollectionType = setDefaultCollectionType;