Collection.js 12 KB

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