1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 | 'use strict';
|
10 |
|
11 | const assert = require('assert');
|
12 | const intersection = require('./utils/intersection');
|
13 | const recast = require('recast');
|
14 | const union = require('./utils/union');
|
15 |
|
16 | const astTypes = recast.types;
|
17 | var types = astTypes.namedTypes;
|
18 | const NodePath = astTypes.NodePath;
|
19 | const Node = types.Node;
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | class Collection {
|
32 |
|
33 | |
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
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 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 | filter(callback) {
|
64 | return new this.constructor(this.__paths.filter(callback), this);
|
65 | }
|
66 |
|
67 | |
68 |
|
69 |
|
70 |
|
71 |
|
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 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 | some(callback) {
|
87 | return this.__paths.some(
|
88 | (path, i, paths) => callback.call(path, path, i, paths)
|
89 | );
|
90 | }
|
91 |
|
92 | |
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 | every(callback) {
|
99 | return this.__paths.every(
|
100 | (path, i, paths) => callback.call(path, path, i, paths)
|
101 | );
|
102 | }
|
103 |
|
104 | |
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 | map(callback, type) {
|
118 | const paths = [];
|
119 | this.forEach(function(path) {
|
120 |
|
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 |
|
137 |
|
138 |
|
139 |
|
140 | size() {
|
141 | return this.__paths.length;
|
142 | }
|
143 |
|
144 | |
145 |
|
146 |
|
147 |
|
148 |
|
149 | get length() {
|
150 | return this.__paths.length;
|
151 | }
|
152 |
|
153 | |
154 |
|
155 |
|
156 |
|
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 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
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 |
|
207 |
|
208 |
|
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 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 | getTypes() {
|
228 | return this._types;
|
229 | }
|
230 |
|
231 | |
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 | isOfType(type) {
|
238 | return !!type && this._types.indexOf(type.toString()) > -1;
|
239 | }
|
240 | }
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 | function _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 |
|
262 | _types = intersection(
|
263 | paths.map(path => astTypes.getSupertypeNames(path.node.type))
|
264 | );
|
265 | }
|
266 | }
|
267 |
|
268 | return _types;
|
269 | }
|
270 |
|
271 | function _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 |
|
283 | function _getSupertypeNames(type) {
|
284 | try {
|
285 | return astTypes.getSupertypeNames(type);
|
286 | } catch(error) {
|
287 | if (error.message === '') {
|
288 |
|
289 |
|
290 |
|
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 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 |
|
306 |
|
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 |
|
315 | function 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 |
|
326 |
|
327 |
|
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
335 |
|
336 | function 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 |
|
348 | const CPt = Collection.prototype;
|
349 |
|
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 | function 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 |
|
400 | function 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 |
|
428 | function 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 |
|
454 | var _defaultType = [];
|
455 |
|
456 |
|
457 |
|
458 |
|
459 |
|
460 |
|
461 |
|
462 |
|
463 |
|
464 | function setDefaultCollectionType(type) {
|
465 | _defaultType = _toTypeArray(type);
|
466 | }
|
467 |
|
468 | exports.fromPaths = fromPaths;
|
469 | exports.fromNodes = fromNodes;
|
470 | exports.registerMethods = registerMethods;
|
471 | exports.hasConflictingRegistration = hasConflictingRegistration;
|
472 | exports.setDefaultCollectionType = setDefaultCollectionType;
|