1 | ;
|
2 |
|
3 | const utils = require("./utils.js");
|
4 | const FastPath = require("./fast-path.js");
|
5 | const 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 |
|
10 | class 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 |
|
199 | Object.setPrototypeOf(Visitor.prototype, null);
|
200 |
|
201 | module.exports = Visitor;
|