UNPKG

10.9 kBJavaScriptView Raw
1'use strict';
2// babel-plugin-__coverage__
3//
4// This is my first Babel plugin, and I wrote it during the night.
5// Therefore, be prepared to see a lot of copypasta and wtf code.
6
7var _babelTemplate = require('babel-template');
8
9var _babelTemplate2 = _interopRequireDefault(_babelTemplate);
10
11var _babelHelperFunctionName = require('babel-helper-function-name');
12
13var _babelHelperFunctionName2 = _interopRequireDefault(_babelHelperFunctionName);
14
15var _fs = require('fs');
16
17function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
18
19function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
20
21var coverageTemplate = (0, _babelTemplate2.default)('\n var GLOBAL = (new Function(\'return this\'))()\n var COVERAGE = GLOBAL[\'__coverage__\'] || (GLOBAL[\'__coverage__\'] = { })\n var FILE_COVERAGE = COVERAGE[PATH] = GLOBAL[\'JSON\'].parse(INITIAL)\n');
22
23//
24// Takes a relative path and returns a real path.
25// Assumes the path name is relative to working directory.
26//
27function getRealpath(n) {
28 try {
29 return (0, _fs.realpathSync)(n) || n;
30 } catch (e) {
31 return n;
32 }
33}
34
35module.exports = function (_ref) {
36 var t = _ref.types;
37
38 //
39 // Return the immediate data structure local to a file.
40 //
41 function getData(context) {
42 var path = getRealpath(context.file.opts.filename);
43 //
44 // XXX: Is it OK to mutate `context.file`? I don’t know but it works!
45 //
46 return context.file.__coverage__data || (context.file.__coverage__data = {
47 //
48 // Initial data that will be added in front of generated source code
49 //
50 base: {
51 path: path,
52 s: {},
53 b: {},
54 f: {},
55 statementMap: {},
56 fnMap: {},
57 branchMap: {}
58 },
59 //
60 // The counter that generates the next ID for each statement type.
61 nextId: {
62 s: 1,
63 b: 1,
64 f: 1
65 }
66 });
67 }
68
69 //
70 // Turns a `SourceLocation` into a plain object.
71 //
72 function locToObject(loc) {
73 return {
74 start: {
75 line: loc.start.line,
76 column: loc.start.column
77 },
78 end: {
79 line: loc.end.line,
80 column: loc.end.column
81 }
82 };
83 }
84
85 //
86 // Generates an AST representing an expression that will increment the
87 // code coverage counter.
88 //
89 function increase(context, type, id, index) {
90 var wrap = index != null
91 // If `index` present, turn `x` into `x[index]`.
92 ? function (x) {
93 return t.memberExpression(x, t.numericLiteral(index), true);
94 } : function (x) {
95 return x;
96 };
97 return t.unaryExpression('++', wrap(t.memberExpression(t.memberExpression(getData(context).id, t.identifier(type)), t.stringLiteral(id), true)));
98 }
99
100 //
101 // Adds coverage traking expression to a path.
102 //
103 // - If it’s a statement (`a`), turns into `++coverage; a`.
104 // - If it’s an expression (`x`), turns into `(++coverage, x)`.
105 //
106 function instrument(path, increment) {
107 if (path.isStatement()) {
108 path.insertBefore(t.expressionStatement(increment));
109 } else if (path.isExpression()) {
110 path.replaceWith(t.sequenceExpression([increment, path.node]));
111 } else {
112 throw new Error('wtf? I can’t cover a ' + path.node.type + '!!!!??');
113 }
114 }
115
116 //
117 // Adds coverage to any statement.
118 //
119 function instrumentStatement(context, path) {
120 var node = path.node;
121
122 // Don’t cover code generated by Babel.
123 if (!node.loc) return;
124
125 // Make sure we don’t cover already instrumented code (only applies to statements).
126 // XXX: Hacky node mutation again. PRs welcome!
127 if (node.__coverage__instrumented) return;
128 node.__coverage__instrumented = true;
129
130 var data = getData(context);
131 var id = String(data.nextId.s++);
132 data.base.s[id] = 0;
133 data.base.statementMap[id] = locToObject(node.loc);
134 instrument(path, increase(context, 's', id));
135 }
136
137 //
138 // Returns the next branch ID and adds the information to `branchMap` object.
139 //
140 function nextBranchId(context, line, type, locations) {
141 var data = getData(context);
142 var id = String(data.nextId.b++);
143 data.base.b[id] = locations.map(function () {
144 return 0;
145 });
146 data.base.branchMap[id] = { line: line, type: type, locations: locations.map(locToObject) };
147 return id;
148 }
149
150 //
151 // `a` => `++coverage; a` For most common type of statements.
152 //
153 function coverStatement(path) {
154 instrumentStatement(this, path);
155 }
156
157 //
158 // `var x = 1` => `var x = (++coverage, 1)`
159 //
160 function coverVariableDeclarator(path) {
161 if (!path.node.init) return;
162 instrumentStatement(this, path.get('init'));
163 }
164
165 //
166 // Adds branch coverage to `if` statements.
167 //
168 function coverIfStatement(path) {
169 if (!path.node.loc) return;
170 instrumentStatement(this, path);
171 if (!path.get('consequent').node) path.set('consequent', t.emptyStatement());
172 if (!path.get('alternate').node) path.set('alternate', t.emptyStatement());
173 var node = path.node;
174 var loc1 = node.consequent.loc || node.loc;
175 var loc2 = node.alternate.loc || loc1;
176 var id = nextBranchId(this, node.loc.start.line, 'if', [loc1, loc2]);
177 instrument(path.get('consequent'), increase(this, 'b', id, 0));
178 instrument(path.get('alternate'), increase(this, 'b', id, 1));
179 }
180
181 //
182 // Adds branch coverage to `switch` statements.
183 //
184 function coverSwitchStatement(path) {
185 if (!path.node.loc) return;
186 instrumentStatement(this, path);
187 var validCases = path.get('cases').filter(function (p) {
188 return p.node.loc;
189 });
190 var id = nextBranchId(this, path.node.loc.start.line, 'switch', validCases.map(function (p) {
191 return p.node.loc;
192 }));
193 var index = 0;
194 var _iteratorNormalCompletion = true;
195 var _didIteratorError = false;
196 var _iteratorError = undefined;
197
198 try {
199 for (var _iterator = validCases[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
200 var p = _step.value;
201
202 if (p.node.test) {
203 instrumentStatement(this, p.get('test'));
204 }
205 p.node.consequent.unshift(increase(this, 'b', id, index++));
206 }
207 } catch (err) {
208 _didIteratorError = true;
209 _iteratorError = err;
210 } finally {
211 try {
212 if (!_iteratorNormalCompletion && _iterator.return) {
213 _iterator.return();
214 }
215 } finally {
216 if (_didIteratorError) {
217 throw _iteratorError;
218 }
219 }
220 }
221 }
222
223 //
224 // `for (;; x)` => `for (;; ++coverage, x)`.
225 // Because the increment may be stopped in the first iteration due to `break`.
226 //
227 function coverForStatement(path) {
228 instrumentStatement(this, path);
229 if (path.get('update').node) {
230 instrumentStatement(this, path.get('update'));
231 }
232 }
233
234 //
235 // Covers a function.
236 //
237 function coverFunction(path) {
238 if (!path.node.loc) return;
239 var node = path.node;
240 var data = getData(this);
241 var id = String(data.nextId.f++);
242 var nameOf = function nameOf(namedNode) {
243 return namedNode && namedNode.id && namedNode.id.name || null;
244 };
245 data.base.f[id] = 0;
246 data.base.fnMap[id] = {
247 name: nameOf((0, _babelHelperFunctionName2.default)(path)), // I love Babel!
248 line: node.loc.start.line,
249 loc: locToObject(node.loc)
250 };
251 var increment = increase(this, 'f', id);
252 var body = path.get('body');
253 if (body.isBlockStatement()) {
254 body.node.body.unshift(t.expressionStatement(increment));
255 } else if (body.isExpression()) {
256 body.replaceWith(t.sequenceExpression([increment, body.node]));
257 } else {
258 throw new Error('wtf?? Can’t cover function with ' + body.node.type);
259 }
260 }
261
262 //
263 // `a ? b : c` => `a ? (++coverage, b) : (++coverage, c)`.
264 // Also adds branch coverage.
265 //
266 function coverConditionalExpression(path) {
267 if (!path.node.loc) return;
268 var node = path.node;
269 var loc1 = node.consequent.loc || node.loc;
270 var loc2 = node.alternate.loc || loc1;
271 var id = nextBranchId(this, node.loc.start.line, 'cond-expr', [loc1, loc2]);
272 instrumentStatement(this, path.get('consequent'));
273 instrumentStatement(this, path.get('alternate'));
274 instrument(path.get('consequent'), increase(this, 'b', id, 0));
275 instrument(path.get('alternate'), increase(this, 'b', id, 1));
276 }
277
278 //
279 // `a || b` => `a || (++coverage, b)`. Required due to short circuiting.
280 // Also adds branch coverage.
281 //
282 function coverLogicalExpression(path) {
283 if (!path.node.loc) return;
284 var node = path.node;
285 var loc1 = node.left.loc || node.loc;
286 var loc2 = node.right.loc || loc1;
287 var id = nextBranchId(this, node.loc.start.line, 'binary-expr', [loc1, loc2]);
288 // left is always evaluated!!
289 instrumentStatement(this, path.get('right'));
290 instrument(path.get('left'), increase(this, 'b', id, 0));
291 instrument(path.get('right'), increase(this, 'b', id, 1));
292 }
293
294 return {
295 visitor: {
296 //
297 // Shamelessly copied from istanbul.
298 //
299 ExpressionStatement: coverStatement,
300 BreakStatement: coverStatement,
301 ContinueStatement: coverStatement,
302 DebuggerStatement: coverStatement,
303 ReturnStatement: coverStatement,
304 ThrowStatement: coverStatement,
305 TryStatement: coverStatement,
306 VariableDeclarator: coverVariableDeclarator,
307 IfStatement: coverIfStatement,
308 ForStatement: coverForStatement,
309 ForInStatement: coverStatement,
310 ForOfStatement: coverStatement,
311 WhileStatement: coverStatement,
312 DoWhileStatement: coverStatement,
313 SwitchStatement: coverSwitchStatement,
314 ArrowFunctionExpression: coverFunction,
315 FunctionExpression: coverFunction,
316 FunctionDeclaration: coverFunction,
317 LabeledStatement: coverStatement,
318 ConditionalExpression: coverConditionalExpression,
319 LogicalExpression: coverLogicalExpression,
320
321 Program: {
322 enter: function enter(path) {
323 // Save the variable name used for tracking coverage.
324 getData(this).id = path.scope.generateUidIdentifier('__coverage__file');
325 },
326 exit: function exit(path) {
327 var _path$node$body;
328
329 // Prepends the coverage runtime.
330 var realPath = getRealpath(this.file.opts.filename);
331 (_path$node$body = path.node.body).unshift.apply(_path$node$body, _toConsumableArray(coverageTemplate({
332 GLOBAL: path.scope.generateUidIdentifier('__coverage__global'),
333 COVERAGE: path.scope.generateUidIdentifier('__coverage__object'),
334 FILE_COVERAGE: getData(this).id,
335 PATH: t.stringLiteral(realPath),
336 INITIAL: t.stringLiteral(JSON.stringify(getData(this).base))
337 })));
338 }
339 }
340 }
341 };
342};
\No newline at end of file