1 | 'use strict';
|
2 |
|
3 | var estraverse = require('estraverse');
|
4 | var escodegen = require('escodegen');
|
5 | var espurify = require('espurify');
|
6 | var espurifyWithRaw = espurify.customize({extra: 'raw'});
|
7 | var isArray = require('isarray');
|
8 | var deepEqual = require('deep-equal');
|
9 | var syntax = estraverse.Syntax;
|
10 | var EspowerLocationDetector = require('espower-location-detector');
|
11 | var EspowerError = require('./espower-error');
|
12 | var toBeSkipped = require('./rules/to-be-skipped');
|
13 | var toBeCaptured = require('./rules/to-be-captured');
|
14 | var canonicalCodeOptions = {
|
15 | format: {
|
16 | indent: {
|
17 | style: ''
|
18 | },
|
19 | newline: ''
|
20 | },
|
21 | verbatim: 'x-verbatim-espower'
|
22 | };
|
23 |
|
24 | function 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 |
|
32 | AssertionVisitor.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 |
|
43 | AssertionVisitor.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 |
|
52 | return undefined;
|
53 | }
|
54 | this.verifyNotInstrumented(currentNode);
|
55 |
|
56 | this.currentArgumentPath = [].concat(controller.path());
|
57 | return undefined;
|
58 | };
|
59 |
|
60 | AssertionVisitor.prototype.leave = function (controller) {
|
61 |
|
62 | };
|
63 |
|
64 | AssertionVisitor.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 |
|
73 | AssertionVisitor.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 |
|
101 | AssertionVisitor.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 |
|
108 | AssertionVisitor.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 |
|
115 | AssertionVisitor.prototype.isCapturingArgument = function () {
|
116 | return !!this.currentArgumentPath;
|
117 | };
|
118 |
|
119 | AssertionVisitor.prototype.isLeavingAssertion = function (controller) {
|
120 | return isPathIdentical(this.assertionPath, controller.path());
|
121 | };
|
122 |
|
123 | AssertionVisitor.prototype.isLeavingArgument = function (controller) {
|
124 | return isPathIdentical(this.currentArgumentPath, controller.path());
|
125 | };
|
126 |
|
127 |
|
128 |
|
129 | AssertionVisitor.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 |
|
152 | AssertionVisitor.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 |
|
183 | AssertionVisitor.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 |
|
202 | AssertionVisitor.prototype.guessPowerAssertCalleeObjectFor = function (node) {
|
203 | switch(node.type) {
|
204 | case syntax.Identifier:
|
205 | return node;
|
206 | case syntax.MemberExpression:
|
207 | return node.object;
|
208 | }
|
209 | return null;
|
210 | };
|
211 |
|
212 | function 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 |
|
221 | function 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 |
|
236 | function 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 |
|
249 | function isPathIdentical (path1, path2) {
|
250 | if (!path1 || !path2) {
|
251 | return false;
|
252 | }
|
253 | return path1.join('/') === path2.join('/');
|
254 | }
|
255 |
|
256 | function 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 |
|
281 | function astEqual (ast1, ast2) {
|
282 | return deepEqual(espurify(ast1), espurify(ast2));
|
283 | }
|
284 |
|
285 | function 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 |
|
295 | function 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 |
|
304 | function getParentNode (controller) {
|
305 | var parents = controller.parents();
|
306 | return parents[parents.length - 1];
|
307 | }
|
308 |
|
309 | function getCurrentKey (controller) {
|
310 | var path = controller.path();
|
311 | return path ? path[path.length - 1] : null;
|
312 | }
|
313 |
|
314 | module.exports = AssertionVisitor;
|