UNPKG

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