UNPKG

11.8 kBJavaScriptView Raw
1var types = require("./types");
2var NodePath = require("./node-path");
3var Printable = types.namedTypes.Printable;
4var isArray = types.builtInTypes.array;
5var isObject = types.builtInTypes.object;
6var isFunction = types.builtInTypes.function;
7var hasOwn = Object.prototype.hasOwnProperty;
8var undefined;
9
10function PathVisitor() {
11 if (!(this instanceof PathVisitor)) {
12 throw new Error(
13 "PathVisitor constructor cannot be invoked without 'new'"
14 );
15 }
16
17 // Permanent state.
18 this._reusableContextStack = [];
19
20 this._methodNameTable = computeMethodNameTable(this);
21 this._shouldVisitComments =
22 hasOwn.call(this._methodNameTable, "Block") ||
23 hasOwn.call(this._methodNameTable, "Line");
24
25 this.Context = makeContextConstructor(this);
26
27 // State reset every time PathVisitor.prototype.visit is called.
28 this._visiting = false;
29 this._changeReported = false;
30}
31
32function computeMethodNameTable(visitor) {
33 var typeNames = Object.create(null);
34
35 for (var methodName in visitor) {
36 if (/^visit[A-Z]/.test(methodName)) {
37 typeNames[methodName.slice("visit".length)] = true;
38 }
39 }
40
41 var supertypeTable = types.computeSupertypeLookupTable(typeNames);
42 var methodNameTable = Object.create(null);
43
44 var typeNames = Object.keys(supertypeTable);
45 var typeNameCount = typeNames.length;
46 for (var i = 0; i < typeNameCount; ++i) {
47 var typeName = typeNames[i];
48 methodName = "visit" + supertypeTable[typeName];
49 if (isFunction.check(visitor[methodName])) {
50 methodNameTable[typeName] = methodName;
51 }
52 }
53
54 return methodNameTable;
55}
56
57PathVisitor.fromMethodsObject = function fromMethodsObject(methods) {
58 if (methods instanceof PathVisitor) {
59 return methods;
60 }
61
62 if (!isObject.check(methods)) {
63 // An empty visitor?
64 return new PathVisitor;
65 }
66
67 function Visitor() {
68 if (!(this instanceof Visitor)) {
69 throw new Error(
70 "Visitor constructor cannot be invoked without 'new'"
71 );
72 }
73 PathVisitor.call(this);
74 }
75
76 var Vp = Visitor.prototype = Object.create(PVp);
77 Vp.constructor = Visitor;
78
79 extend(Vp, methods);
80 extend(Visitor, PathVisitor);
81
82 isFunction.assert(Visitor.fromMethodsObject);
83 isFunction.assert(Visitor.visit);
84
85 return new Visitor;
86};
87
88function extend(target, source) {
89 for (var property in source) {
90 if (hasOwn.call(source, property)) {
91 target[property] = source[property];
92 }
93 }
94
95 return target;
96}
97
98PathVisitor.visit = function visit(node, methods) {
99 return PathVisitor.fromMethodsObject(methods).visit(node);
100};
101
102var PVp = PathVisitor.prototype;
103
104PVp.visit = function() {
105 if (this._visiting) {
106 throw new Error(
107 "Recursively calling visitor.visit(path) resets visitor state. " +
108 "Try this.visit(path) or this.traverse(path) instead."
109 );
110 }
111
112 // Private state that needs to be reset before every traversal.
113 this._visiting = true;
114 this._changeReported = false;
115 this._abortRequested = false;
116
117 var argc = arguments.length;
118 var args = new Array(argc)
119 for (var i = 0; i < argc; ++i) {
120 args[i] = arguments[i];
121 }
122
123 if (!(args[0] instanceof NodePath)) {
124 args[0] = new NodePath({ root: args[0] }).get("root");
125 }
126
127 // Called with the same arguments as .visit.
128 this.reset.apply(this, args);
129
130 try {
131 var root = this.visitWithoutReset(args[0]);
132 var didNotThrow = true;
133 } finally {
134 this._visiting = false;
135
136 if (!didNotThrow && this._abortRequested) {
137 // If this.visitWithoutReset threw an exception and
138 // this._abortRequested was set to true, return the root of
139 // the AST instead of letting the exception propagate, so that
140 // client code does not have to provide a try-catch block to
141 // intercept the AbortRequest exception. Other kinds of
142 // exceptions will propagate without being intercepted and
143 // rethrown by a catch block, so their stacks will accurately
144 // reflect the original throwing context.
145 return args[0].value;
146 }
147 }
148
149 return root;
150};
151
152PVp.AbortRequest = function AbortRequest() {};
153PVp.abort = function() {
154 var visitor = this;
155 visitor._abortRequested = true;
156 var request = new visitor.AbortRequest();
157
158 // If you decide to catch this exception and stop it from propagating,
159 // make sure to call its cancel method to avoid silencing other
160 // exceptions that might be thrown later in the traversal.
161 request.cancel = function() {
162 visitor._abortRequested = false;
163 };
164
165 throw request;
166};
167
168PVp.reset = function(path/*, additional arguments */) {
169 // Empty stub; may be reassigned or overridden by subclasses.
170};
171
172PVp.visitWithoutReset = function(path) {
173 if (this instanceof this.Context) {
174 // Since this.Context.prototype === this, there's a chance we
175 // might accidentally call context.visitWithoutReset. If that
176 // happens, re-invoke the method against context.visitor.
177 return this.visitor.visitWithoutReset(path);
178 }
179
180 if (!(path instanceof NodePath)) {
181 throw new Error("");
182 }
183
184 var value = path.value;
185
186 var methodName = value &&
187 typeof value === "object" &&
188 typeof value.type === "string" &&
189 this._methodNameTable[value.type];
190
191 if (methodName) {
192 var context = this.acquireContext(path);
193 try {
194 return context.invokeVisitorMethod(methodName);
195 } finally {
196 this.releaseContext(context);
197 }
198
199 } else {
200 // If there was no visitor method to call, visit the children of
201 // this node generically.
202 return visitChildren(path, this);
203 }
204};
205
206function visitChildren(path, visitor) {
207 if (!(path instanceof NodePath)) {
208 throw new Error("");
209 }
210 if (!(visitor instanceof PathVisitor)) {
211 throw new Error("");
212 }
213
214 var value = path.value;
215
216 if (isArray.check(value)) {
217 path.each(visitor.visitWithoutReset, visitor);
218 } else if (!isObject.check(value)) {
219 // No children to visit.
220 } else {
221 var childNames = types.getFieldNames(value);
222
223 // The .comments field of the Node type is hidden, so we only
224 // visit it if the visitor defines visitBlock or visitLine, and
225 // value.comments is defined.
226 if (visitor._shouldVisitComments &&
227 value.comments &&
228 childNames.indexOf("comments") < 0) {
229 childNames.push("comments");
230 }
231
232 var childCount = childNames.length;
233 var childPaths = [];
234
235 for (var i = 0; i < childCount; ++i) {
236 var childName = childNames[i];
237 if (!hasOwn.call(value, childName)) {
238 value[childName] = types.getFieldValue(value, childName);
239 }
240 childPaths.push(path.get(childName));
241 }
242
243 for (var i = 0; i < childCount; ++i) {
244 visitor.visitWithoutReset(childPaths[i]);
245 }
246 }
247
248 return path.value;
249}
250
251PVp.acquireContext = function(path) {
252 if (this._reusableContextStack.length === 0) {
253 return new this.Context(path);
254 }
255 return this._reusableContextStack.pop().reset(path);
256};
257
258PVp.releaseContext = function(context) {
259 if (!(context instanceof this.Context)) {
260 throw new Error("");
261 }
262 this._reusableContextStack.push(context);
263 context.currentPath = null;
264};
265
266PVp.reportChanged = function() {
267 this._changeReported = true;
268};
269
270PVp.wasChangeReported = function() {
271 return this._changeReported;
272};
273
274function makeContextConstructor(visitor) {
275 function Context(path) {
276 if (!(this instanceof Context)) {
277 throw new Error("");
278 }
279 if (!(this instanceof PathVisitor)) {
280 throw new Error("");
281 }
282 if (!(path instanceof NodePath)) {
283 throw new Error("");
284 }
285
286 Object.defineProperty(this, "visitor", {
287 value: visitor,
288 writable: false,
289 enumerable: true,
290 configurable: false
291 });
292
293 this.currentPath = path;
294 this.needToCallTraverse = true;
295
296 Object.seal(this);
297 }
298
299 if (!(visitor instanceof PathVisitor)) {
300 throw new Error("");
301 }
302
303 // Note that the visitor object is the prototype of Context.prototype,
304 // so all visitor methods are inherited by context objects.
305 var Cp = Context.prototype = Object.create(visitor);
306
307 Cp.constructor = Context;
308 extend(Cp, sharedContextProtoMethods);
309
310 return Context;
311}
312
313// Every PathVisitor has a different this.Context constructor and
314// this.Context.prototype object, but those prototypes can all use the
315// same reset, invokeVisitorMethod, and traverse function objects.
316var sharedContextProtoMethods = Object.create(null);
317
318sharedContextProtoMethods.reset =
319function reset(path) {
320 if (!(this instanceof this.Context)) {
321 throw new Error("");
322 }
323 if (!(path instanceof NodePath)) {
324 throw new Error("");
325 }
326
327 this.currentPath = path;
328 this.needToCallTraverse = true;
329
330 return this;
331};
332
333sharedContextProtoMethods.invokeVisitorMethod =
334function invokeVisitorMethod(methodName) {
335 if (!(this instanceof this.Context)) {
336 throw new Error("");
337 }
338 if (!(this.currentPath instanceof NodePath)) {
339 throw new Error("");
340 }
341
342 var result = this.visitor[methodName].call(this, this.currentPath);
343
344 if (result === false) {
345 // Visitor methods return false to indicate that they have handled
346 // their own traversal needs, and we should not complain if
347 // this.needToCallTraverse is still true.
348 this.needToCallTraverse = false;
349
350 } else if (result !== undefined) {
351 // Any other non-undefined value returned from the visitor method
352 // is interpreted as a replacement value.
353 this.currentPath = this.currentPath.replace(result)[0];
354
355 if (this.needToCallTraverse) {
356 // If this.traverse still hasn't been called, visit the
357 // children of the replacement node.
358 this.traverse(this.currentPath);
359 }
360 }
361
362 if (this.needToCallTraverse !== false) {
363 throw new Error(
364 "Must either call this.traverse or return false in " + methodName
365 );
366 }
367
368 var path = this.currentPath;
369 return path && path.value;
370};
371
372sharedContextProtoMethods.traverse =
373function traverse(path, newVisitor) {
374 if (!(this instanceof this.Context)) {
375 throw new Error("");
376 }
377 if (!(path instanceof NodePath)) {
378 throw new Error("");
379 }
380 if (!(this.currentPath instanceof NodePath)) {
381 throw new Error("");
382 }
383
384 this.needToCallTraverse = false;
385
386 return visitChildren(path, PathVisitor.fromMethodsObject(
387 newVisitor || this.visitor
388 ));
389};
390
391sharedContextProtoMethods.visit =
392function visit(path, newVisitor) {
393 if (!(this instanceof this.Context)) {
394 throw new Error("");
395 }
396 if (!(path instanceof NodePath)) {
397 throw new Error("");
398 }
399 if (!(this.currentPath instanceof NodePath)) {
400 throw new Error("");
401 }
402
403 this.needToCallTraverse = false;
404
405 return PathVisitor.fromMethodsObject(
406 newVisitor || this.visitor
407 ).visitWithoutReset(path);
408};
409
410sharedContextProtoMethods.reportChanged = function reportChanged() {
411 this.visitor.reportChanged();
412};
413
414sharedContextProtoMethods.abort = function abort() {
415 this.needToCallTraverse = false;
416 this.visitor.abort();
417};
418
419module.exports = PathVisitor;