UNPKG

6.99 kBJavaScriptView Raw
1"use strict";
2
3const utils = require("./utils.js");
4const FastPath = require("./fast-path.js");
5const codeOfUnderscore = "_".charCodeAt(0);
6
7// This Visitor API was inspired by a similar API provided by ast-types:
8// https://github.com/benjamn/ast-types/blob/master/lib/path-visitor.js
9
10class Visitor {
11 constructor() {
12 const that = this;
13 const visit = this.visit;
14 const visitWithoutReset = this.visitWithoutReset;
15 const visitChildren = this.visitChildren;
16
17 // Avoid slower `Function#bind` for Node < 7.
18 this.visit = function () {
19 visit.apply(that, arguments);
20 };
21
22 this.visitWithoutReset = function (path) {
23 visitWithoutReset.call(that, path);
24 };
25
26 this.visitChildren = function (path) {
27 visitChildren.call(that, path);
28 };
29
30 // Define some internal properties before the constructor returns to
31 // help the JS engine understand the shape of the visitor object.
32 this._beforeReset();
33 }
34
35 _beforeReset() {
36 // Since this.possibleIndexes can cause the traversal to ignore entire
37 // subtrees if misused, it's important to force the reset method to
38 // redefine it each time the visit method is called.
39 this.possibleIndexes = null;
40 this._piLowerBound = 0;
41 this._piUpperBound = 0;
42 }
43
44 // The reset method does nothing unless it is overridden.
45 reset() {}
46
47 _afterReset() {
48 // Reset the bounds we are currently considering within
49 // this.possibleIndexes, so that we can adjust the bounds in
50 // _visitChildrenIfPossible below. This allows us to pretend we're
51 // working with this.possibleIndexes.slice(this._piLowerBound,
52 // this._piUpperBound) without actually modifying the array.
53 if (Array.isArray(this.possibleIndexes)) {
54 this._piUpperBound = this.possibleIndexes.length;
55 }
56 }
57
58 visit(astOrPath) {
59 const args = Array.prototype.slice.call(arguments);
60 const path = args[0] = FastPath.from(astOrPath);
61 this._beforeReset();
62 // The user-defined reset method has a chance to (re)initialize any
63 // instance variables, including this.possibleIndexes.
64 this.reset.apply(this, args);
65 this._afterReset();
66 this.visitWithoutReset(path);
67 }
68
69 visitWithoutReset(path) {
70 const value = path.getValue();
71 if (Array.isArray(value)) {
72 path.each(this.visitWithoutReset);
73 } else if (path.getNode() === value) {
74 const method = this["visit" + value.type];
75 if (typeof method === "function") {
76 // The method must call this.visitChildren(path) to continue traversing.
77 method.call(this, path);
78 } else {
79 // Only continue the search if this.possibleIndexes is undefined,
80 // or if [node.start, node.end) contains at least one possible
81 // index of an identifier that we care about.
82 this._visitChildrenIfPossible(path);
83 }
84 }
85 }
86
87 // Calls this.visitChildren(nodePath) unless this.possibleIndexes is
88 // defined and we can determine that the current [node.start, node.end)
89 // interval does not contain any of the possible indexes we care about.
90 _visitChildrenIfPossible(nodePath) {
91 const array = this.possibleIndexes;
92 if (! array) {
93 // If this.possibleIndexes is not defined, then we don't know
94 // anything about where (not) to look, so err on the side of
95 // visiting the children.
96 return this.visitChildren(nodePath);
97 }
98
99 const node = nodePath.getValue();
100
101 if (node.type === "File" ||
102 node.type === "Program") {
103 // Always visit children of File and Program AST nodes, since
104 // they're always near the root of the AST, and they don't have any
105 // identifiers of their own that aren't part of other nodes, so it
106 // might be awkward to use this.possibleIndexes to include them.
107 return this.visitChildren(nodePath);
108 }
109
110 const start = node.start;
111 const end = node.end;
112
113 if (typeof start !== "number" ||
114 typeof end !== "number") {
115 // If we don't have start/end for this node, err on the side of
116 // visiting the children.
117 return this.visitChildren(nodePath);
118 }
119
120 // Save the current this._pi{Lower,Upper}Bound values so that we can
121 // restore them at the end of this method.
122 const oldLowerBound = this._piLowerBound;
123 const oldUpperBound = this._piUpperBound;
124
125 let lowerBound = typeof oldLowerBound === "number" ? oldLowerBound : 0;
126 let upperBound = typeof oldUpperBound === "number" ? oldUpperBound : array.length;
127
128 // Find the first possible index not less than node.start. While a
129 // binary search might seem more efficient here, remember that we are
130 // descending a tree of nested AST nodes, with each new [node.start,
131 // node.end) interval getting a little smaller at every level. This
132 // while loop is responsible for closing the gap since the last time
133 // we updated the lower bound, which means lowerBound should only need
134 // to be incremented a small number of times, whereas a binary search
135 // would take log_2(array.length - lowerBound) steps.
136 while (lowerBound < upperBound &&
137 array[lowerBound] < start) {
138 ++lowerBound;
139 }
140
141 // Find the first possible index greater than node.end. Again, a
142 // binary search would be more expensive here, since we expect to
143 // decrement upperBound only a small number of times, typically.
144 while (lowerBound < upperBound &&
145 end < array[upperBound - 1]) {
146 --upperBound;
147 }
148
149 // Now array.slice(lowerBound, upperBound) contains exactly the
150 // subsequence of possible indexes that are contained by the interval
151 // [node.start, node.end). If that interval is empty, then this node
152 // does not contain any of the indexes we care about, and we can avoid
153 // visiting its children. If the interval is non-empty, first update
154 // this._pi{Lower,Upper}Bound, then visit the children, then reset
155 // this._pi{Lower,Upper}Bound to the old{Upper,Lower}Bound values.
156 if (lowerBound < upperBound) {
157 this._piLowerBound = lowerBound;
158 this._piUpperBound = upperBound;
159 this.visitChildren(nodePath);
160 this._piLowerBound = oldLowerBound;
161 this._piUpperBound = oldUpperBound;
162 }
163 }
164
165 visitChildren(path) {
166 if (! path.valueIsNode()) {
167 return;
168 }
169
170 const node = path.getValue();
171 const keys = Object.keys(node);
172 const keyCount = keys.length;
173
174 for (let i = 0; i < keyCount; ++i) {
175 const key = keys[i];
176
177 // Ignore .loc.{start,end} objects.
178 if (key === "loc") {
179 continue;
180 }
181
182 // Ignore "private" properties added by Babel.
183 if (key.charCodeAt(0) === codeOfUnderscore) {
184 continue;
185 }
186
187 const value = node[key];
188
189 // Ignore properties whose values aren't objects.
190 if (! utils.isObject(value)) {
191 continue;
192 }
193
194 path.call(this.visitWithoutReset, key);
195 }
196 }
197};
198
199Object.setPrototypeOf(Visitor.prototype, null);
200
201module.exports = Visitor;