UNPKG

9.78 kBJavaScriptView Raw
1'use strict';
2
3var estraverse = require('estraverse');
4var escodegen = require('escodegen');
5var espurify = require('espurify');
6var espurifyWithRaw = espurify.customize({extra: 'raw'});
7var isArray = require('isarray');
8var deepEqual = require('deep-equal');
9var syntax = estraverse.Syntax;
10var EspowerLocationDetector = require('espower-location-detector');
11var EspowerError = require('./espower-error');
12var toBeSkipped = require('./rules/to-be-skipped');
13var toBeCaptured = require('./rules/to-be-captured');
14var canonicalCodeOptions = {
15 format: {
16 indent: {
17 style: ''
18 },
19 newline: ''
20 },
21 verbatim: 'x-verbatim-espower'
22};
23
24function AssertionVisitor (matcher, options) {
25 this.matcher = matcher;
26 this.options = options || {};
27 this.locationDetector = new EspowerLocationDetector(this.options);
28 this.currentArgumentPath = null;
29 this.argumentModified = false;
30}
31
32AssertionVisitor.prototype.enter = function (controller) {
33 this.assertionPath = [].concat(controller.path());
34 var currentNode = controller.current();
35 this.canonicalCode = this.generateCanonicalCode(currentNode);
36 this.powerAssertCalleeObject = this.guessPowerAssertCalleeObjectFor(currentNode.callee);
37 this.location = this.locationDetector.locationFor(currentNode);
38 var enclosingFunc = findEnclosingFunction(controller.parents());
39 this.withinGenerator = enclosingFunc && enclosingFunc.generator;
40 this.withinAsync = enclosingFunc && enclosingFunc.async;
41};
42
43AssertionVisitor.prototype.enterArgument = function (controller) {
44 var currentNode = controller.current();
45 var parentNode = getParentNode(controller);
46 var argMatchResult = this.matcher.matchArgument(currentNode, parentNode);
47 if (!argMatchResult) {
48 return undefined;
49 }
50 if (argMatchResult.name === 'message' && argMatchResult.kind === 'optional') {
51 // skip optional message argument
52 return undefined;
53 }
54 this.verifyNotInstrumented(currentNode);
55 // entering target argument
56 this.currentArgumentPath = [].concat(controller.path());
57 return undefined;
58};
59
60AssertionVisitor.prototype.leave = function (controller) {
61 // nothing to do now
62};
63
64AssertionVisitor.prototype.leaveArgument = function (resultTree) {
65 try {
66 return this.argumentModified ? this.captureArgument(resultTree) : resultTree;
67 } finally {
68 this.currentArgumentPath = null;
69 this.argumentModified = false;
70 }
71};
72
73AssertionVisitor.prototype.captureNode = function (controller) {
74 this.argumentModified = true;
75 var currentNode = controller.current();
76 var path = controller.path();
77 var n = newNodeWithLocationCopyOf(currentNode);
78 var relativeEsPath = path.slice(this.assertionPath.length);
79 var newCalleeObject = updateLocRecursively(espurify(this.powerAssertCalleeObject), n, this.options.visitorKeys);
80 return n({
81 type: syntax.CallExpression,
82 callee: n({
83 type: syntax.MemberExpression,
84 computed: false,
85 object: newCalleeObject,
86 property: n({
87 type: syntax.Identifier,
88 name: '_capt'
89 })
90 }),
91 arguments: [
92 currentNode,
93 n({
94 type: syntax.Literal,
95 value: relativeEsPath.join('/')
96 })
97 ]
98 });
99};
100
101AssertionVisitor.prototype.toBeSkipped = function (controller) {
102 var currentNode = controller.current();
103 var parentNode = getParentNode(controller);
104 var currentKey = getCurrentKey(controller);
105 return toBeSkipped(currentNode, parentNode, currentKey);
106};
107
108AssertionVisitor.prototype.toBeCaptured = function (controller) {
109 var currentNode = controller.current();
110 var parentNode = getParentNode(controller);
111 var currentKey = getCurrentKey(controller);
112 return toBeCaptured(currentNode, parentNode, currentKey);
113};
114
115AssertionVisitor.prototype.isCapturingArgument = function () {
116 return !!this.currentArgumentPath;
117};
118
119AssertionVisitor.prototype.isLeavingAssertion = function (controller) {
120 return isPathIdentical(this.assertionPath, controller.path());
121};
122
123AssertionVisitor.prototype.isLeavingArgument = function (controller) {
124 return isPathIdentical(this.currentArgumentPath, controller.path());
125};
126
127// internal
128
129AssertionVisitor.prototype.generateCanonicalCode = function (node) {
130 var visitorKeys = this.options.visitorKeys;
131 var ast = espurifyWithRaw(node);
132 var visitor = {
133 leave: function (currentNode, parentNode) {
134 if (currentNode.type === syntax.Literal && typeof currentNode.raw !== 'undefined') {
135 currentNode['x-verbatim-espower'] = {
136 content : currentNode.raw,
137 precedence : escodegen.Precedence.Primary
138 };
139 return currentNode;
140 } else {
141 return undefined;
142 }
143 }
144 };
145 if (visitorKeys) {
146 visitor.keys = visitorKeys;
147 }
148 estraverse.replace(ast, visitor);
149 return escodegen.generate(ast, canonicalCodeOptions);
150};
151
152AssertionVisitor.prototype.captureArgument = function (node) {
153 var n = newNodeWithLocationCopyOf(node);
154 var props = [];
155 var newCalleeObject = updateLocRecursively(espurify(this.powerAssertCalleeObject), n, this.options.visitorKeys);
156 addLiteralTo(props, n, 'content', this.canonicalCode);
157 addLiteralTo(props, n, 'filepath', this.location.source);
158 addLiteralTo(props, n, 'line', this.location.line);
159 if (this.withinAsync) {
160 addLiteralTo(props, n, 'async', true);
161 }
162 if (this.withinGenerator) {
163 addLiteralTo(props, n, 'generator', true);
164 }
165 return n({
166 type: syntax.CallExpression,
167 callee: n({
168 type: syntax.MemberExpression,
169 computed: false,
170 object: newCalleeObject,
171 property: n({
172 type: syntax.Identifier,
173 name: '_expr'
174 })
175 }),
176 arguments: [node].concat(n({
177 type: syntax.ObjectExpression,
178 properties: props
179 }))
180 });
181};
182
183AssertionVisitor.prototype.verifyNotInstrumented = function (currentNode) {
184 if (currentNode.type !== syntax.CallExpression) {
185 return;
186 }
187 if (currentNode.callee.type !== syntax.MemberExpression) {
188 return;
189 }
190 var prop = currentNode.callee.property;
191 if (prop.type === syntax.Identifier && prop.name === '_expr') {
192 if (astEqual(currentNode.callee.object, this.powerAssertCalleeObject)) {
193 var errorMessage = 'Attempted to transform AST twice.';
194 if (this.options.path) {
195 errorMessage += ' path: ' + this.options.path;
196 }
197 throw new EspowerError(errorMessage, this.verifyNotInstrumented);
198 }
199 }
200};
201
202AssertionVisitor.prototype.guessPowerAssertCalleeObjectFor = function (node) {
203 switch(node.type) {
204 case syntax.Identifier:
205 return node;
206 case syntax.MemberExpression:
207 return node.object; // Returns browser.assert when browser.assert.element(selector)
208 }
209 return null;
210};
211
212function addLiteralTo (props, createNode, name, value) {
213 if (typeof value !== 'undefined') {
214 addToProps(props, createNode, name, createNode({
215 type: syntax.Literal,
216 value: value
217 }));
218 }
219}
220
221function addToProps (props, createNode, name, value) {
222 props.push(createNode({
223 type: syntax.Property,
224 key: createNode({
225 type: syntax.Identifier,
226 name: name
227 }),
228 value: value,
229 method: false,
230 shorthand: false,
231 computed: false,
232 kind: 'init'
233 }));
234}
235
236function updateLocRecursively (node, n, visitorKeys) {
237 var visitor = {
238 leave: function (currentNode, parentNode) {
239 return n(currentNode);
240 }
241 };
242 if (visitorKeys) {
243 visitor.keys = visitorKeys;
244 }
245 estraverse.replace(node, visitor);
246 return node;
247}
248
249function isPathIdentical (path1, path2) {
250 if (!path1 || !path2) {
251 return false;
252 }
253 return path1.join('/') === path2.join('/');
254}
255
256function newNodeWithLocationCopyOf (original) {
257 return function (newNode) {
258 if (typeof original.loc !== 'undefined') {
259 var newLoc = {
260 start: {
261 line: original.loc.start.line,
262 column: original.loc.start.column
263 },
264 end: {
265 line: original.loc.end.line,
266 column: original.loc.end.column
267 }
268 };
269 if (typeof original.loc.source !== 'undefined') {
270 newLoc.source = original.loc.source;
271 }
272 newNode.loc = newLoc;
273 }
274 if (isArray(original.range)) {
275 newNode.range = [original.range[0], original.range[1]];
276 }
277 return newNode;
278 };
279}
280
281function astEqual (ast1, ast2) {
282 return deepEqual(espurify(ast1), espurify(ast2));
283}
284
285function isFunction (node) {
286 switch(node.type) {
287 case syntax.FunctionDeclaration:
288 case syntax.FunctionExpression:
289 case syntax.ArrowFunctionExpression:
290 return true;
291 }
292 return false;
293}
294
295function findEnclosingFunction (parents) {
296 for (var i = parents.length - 1; i >= 0; i--) {
297 if (isFunction(parents[i])) {
298 return parents[i];
299 }
300 }
301 return null;
302}
303
304function getParentNode (controller) {
305 var parents = controller.parents();
306 return parents[parents.length - 1];
307}
308
309function getCurrentKey (controller) {
310 var path = controller.path();
311 return path ? path[path.length - 1] : null;
312}
313
314module.exports = AssertionVisitor;