1 | 'use strict';
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | const Fs = require('fs');
|
11 | const Path = require('path');
|
12 | const Espree = require('espree');
|
13 | const SourceMapConsumer = require('source-map').SourceMapConsumer;
|
14 | const SourceMapSupport = require('source-map-support');
|
15 | const Transform = require('./transform');
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | const internals = {
|
21 | patterns: [],
|
22 | sources: {}
|
23 | };
|
24 |
|
25 |
|
26 | internals.prime = function (extension) {
|
27 |
|
28 | require.extensions[extension] = function (localModule, filename) {
|
29 |
|
30 | for (let i = 0; i < internals.patterns.length; ++i) {
|
31 | if (internals.patterns[i].test(filename.replace(/\\/g, '/'))) {
|
32 | return localModule._compile(internals.instrument(filename), filename);
|
33 | }
|
34 | }
|
35 |
|
36 | const src = Fs.readFileSync(filename, 'utf8');
|
37 | return localModule._compile(Transform.transform(filename, src), filename);
|
38 | };
|
39 | };
|
40 |
|
41 |
|
42 | exports.instrument = function (options) {
|
43 |
|
44 | internals.patterns.unshift(internals.pattern(options));
|
45 |
|
46 | Transform.install(options, internals.prime);
|
47 | };
|
48 |
|
49 |
|
50 | internals.pattern = function (options) {
|
51 |
|
52 | const coveragePath = options.coveragePath || '';
|
53 | const base = internals.escape(coveragePath);
|
54 | const excludes = options.coverageExclude ? [].concat(options.coverageExclude).map((path) => {
|
55 |
|
56 | let isFile = false;
|
57 | try {
|
58 | const pathStat = Fs.statSync(Path.join(coveragePath, path));
|
59 | isFile = pathStat.isFile();
|
60 | }
|
61 | catch (ex) {
|
62 | if (ex.code !== 'ENOENT') {
|
63 | console.error(ex);
|
64 | }
|
65 | }
|
66 |
|
67 | const escaped = internals.escape(path);
|
68 | return isFile ? escaped : `${escaped}\\/`;
|
69 | }).join('|') : '';
|
70 |
|
71 | const regex = '^' + base + (excludes ? (base[base.length - 1] === '/' ? '' : '\\/') + '(?!' + excludes + ')' : '');
|
72 | return new RegExp(regex);
|
73 | };
|
74 |
|
75 |
|
76 | internals.escape = function (string) {
|
77 |
|
78 | return string.replace(/\\/g, '/').replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&');
|
79 | };
|
80 |
|
81 |
|
82 | internals.instrument = function (filename) {
|
83 |
|
84 | filename = filename.replace(/\\/g, '/');
|
85 |
|
86 | const file = Fs.readFileSync(filename, 'utf8');
|
87 | let content = file.replace(/^\#\!.*/, '');
|
88 | content = Transform.transform(filename, content);
|
89 |
|
90 | const tracking = [];
|
91 | const statements = [];
|
92 | const chunks = content.split('');
|
93 | let ids = 0;
|
94 | const bypass = {};
|
95 | const nodesByLine = {};
|
96 |
|
97 | const addStatement = function (line, node, bool) {
|
98 |
|
99 | const id = ++ids;
|
100 | statements.push({
|
101 | id,
|
102 | loc: node.loc,
|
103 | line,
|
104 | bool: bool && node.type !== 'ConditionalExpression' && node.type !== 'LogicalExpression'
|
105 | });
|
106 | return id;
|
107 | };
|
108 |
|
109 | const addToLines = function (node, line) {
|
110 |
|
111 | if (!(line in nodesByLine)) {
|
112 | nodesByLine[line] = [];
|
113 | }
|
114 |
|
115 | nodesByLine[line].push(node.type);
|
116 | };
|
117 |
|
118 | const annotate = function (node, parent) {
|
119 |
|
120 | const line = node.loc.start.line;
|
121 |
|
122 |
|
123 | node.parent = parent;
|
124 |
|
125 | node.source = function () {
|
126 |
|
127 | return chunks.slice(node.range[0], node.range[1]).join('');
|
128 | };
|
129 |
|
130 | node.set = function (s) {
|
131 |
|
132 | chunks[node.range[0]] = s;
|
133 | for (let i = node.range[0] + 1; i < node.range[1]; ++i) {
|
134 | chunks[i] = '';
|
135 | }
|
136 | };
|
137 |
|
138 |
|
139 | addToLines(node, line);
|
140 | const end = node.loc.end.line;
|
141 | if (end !== line) {
|
142 | addToLines(node, end);
|
143 | }
|
144 |
|
145 |
|
146 |
|
147 | const bypassTests = [];
|
148 | for (let i = node.range[0]; i <= node.range[1]; ++i) {
|
149 | bypassTests.push(bypass[i]);
|
150 | }
|
151 |
|
152 | if (bypassTests.every((test) => test)) {
|
153 | return;
|
154 | }
|
155 |
|
156 |
|
157 |
|
158 | Object.keys(node).forEach((name) => {
|
159 |
|
160 | if (name === 'parent') {
|
161 | return;
|
162 | }
|
163 |
|
164 | const children = [].concat(node[name]);
|
165 | children.forEach((child) => {
|
166 |
|
167 | if (child && typeof child.type === 'string') {
|
168 | annotate(child, node);
|
169 | }
|
170 | });
|
171 | });
|
172 |
|
173 |
|
174 |
|
175 | const decoratedTypes = [
|
176 | 'IfStatement',
|
177 | 'WhileStatement',
|
178 | 'DoWhileStatement',
|
179 | 'ForStatement',
|
180 | 'ForInStatement',
|
181 | 'WithStatement'
|
182 | ];
|
183 |
|
184 | let consequent;
|
185 |
|
186 | if (decoratedTypes.indexOf(node.type) !== -1) {
|
187 | if (node.alternate &&
|
188 | node.alternate.type !== 'BlockStatement') {
|
189 |
|
190 | node.alternate.set(`{${node.alternate.source()}}`);
|
191 | }
|
192 |
|
193 | consequent = node.consequent || node.body;
|
194 | if (consequent.type !== 'BlockStatement') {
|
195 | consequent.set(`{${consequent.source()}}`);
|
196 | }
|
197 | }
|
198 |
|
199 | if (node.type === 'ExpressionStatement' && node.expression.value === 'use strict') {
|
200 | return;
|
201 | }
|
202 |
|
203 | const trackedTypes = [
|
204 | 'ExpressionStatement',
|
205 | 'BreakStatement',
|
206 | 'ContinueStatement',
|
207 | 'VariableDeclaration',
|
208 | 'ReturnStatement',
|
209 | 'ThrowStatement',
|
210 | 'TryStatement',
|
211 | 'IfStatement',
|
212 | 'WhileStatement',
|
213 | 'DoWhileStatement',
|
214 | 'ForStatement',
|
215 | 'ForInStatement',
|
216 | 'SwitchStatement',
|
217 | 'WithStatement',
|
218 | 'LabeledStatement'
|
219 | ];
|
220 |
|
221 |
|
222 | if (node.parent && node.parent.type === 'BlockStatement' &&
|
223 | node.parent.parent.type.includes('FunctionExpression') &&
|
224 | node.parent.body[0] === node) {
|
225 |
|
226 | const id = addStatement(line, node, false);
|
227 |
|
228 | node.set(`global.__$$labCov._statement('${filename}', ${id}, ${line}, true); ${node.source()};`);
|
229 | }
|
230 | else if (trackedTypes.indexOf(node.type) !== -1 &&
|
231 | (node.type !== 'VariableDeclaration' || (node.parent.type !== 'ForStatement' && node.parent.type !== 'ForInStatement' && node.parent.type !== 'ForOfStatement')) &&
|
232 | node.parent.type !== 'LabeledStatement') {
|
233 |
|
234 | tracking.push(line);
|
235 | node.set(`global.__$$labCov._line('${filename}',${line});${node.source()}`);
|
236 | }
|
237 | else if (node.type === 'ConditionalExpression') {
|
238 | consequent = addStatement(line, node.consequent, false);
|
239 | const alternate = addStatement(line, node.alternate, false);
|
240 |
|
241 | node.set(`(${node.test.source()}? global.__$$labCov._statement('${filename}',${consequent},${line},(${node.consequent.source()})) : global.__$$labCov._statement('${filename}',${alternate},${line},(${node.alternate.source()})))`);
|
242 | }
|
243 | else if (node.type === 'LogicalExpression') {
|
244 | const left = addStatement(line, node.left, true);
|
245 | const right = addStatement(line, node.right, node.parent.type === 'LogicalExpression');
|
246 |
|
247 | node.set(`(global.__$$labCov._statement(\'${filename }\',${left},${line},${node.left.source()})${node.operator}global.__$$labCov._statement(\'${filename}\',${right},${line},${node.right.source()}))`);
|
248 | }
|
249 | else if (node.parent && node.parent.type === 'ArrowFunctionExpression' &&
|
250 | node.type.includes('Expression')) {
|
251 |
|
252 | const id = addStatement(line, node, false);
|
253 |
|
254 | node.set(`global.__$$labCov._statement('${filename}', ${id}, ${line}, ${node.source()})`);
|
255 | }
|
256 | else if (node.parent &&
|
257 | node.parent.test === node &&
|
258 | node.parent.type !== 'SwitchCase') {
|
259 |
|
260 | const test = addStatement(line, node, true);
|
261 |
|
262 | node.set(`global.__$$labCov._statement(\'${filename}\',${test},${line},${node.source()})`);
|
263 | }
|
264 | };
|
265 |
|
266 |
|
267 |
|
268 | const tree = Espree.parse(content, {
|
269 | loc: true,
|
270 | comment: true,
|
271 | range: true,
|
272 | ecmaFeatures: {
|
273 | experimentalObjectRestSpread: true
|
274 | },
|
275 | ecmaVersion: 9
|
276 | });
|
277 |
|
278 |
|
279 |
|
280 | let skipStart = 0;
|
281 | let segmentSkip = false;
|
282 | tree.comments.forEach((comment) => {
|
283 |
|
284 | const directive = comment.value.match(/^\s*\$lab\:coverage\:(off|on)\$\s*$/);
|
285 | if (directive) {
|
286 | const skip = directive[1] !== 'on';
|
287 | if (skip !== segmentSkip) {
|
288 | segmentSkip = skip;
|
289 | if (skip) {
|
290 | skipStart = comment.range[1];
|
291 | }
|
292 | else {
|
293 | for (let i = skipStart; i < comment.range[0]; ++i) {
|
294 | bypass[i] = true;
|
295 | }
|
296 | }
|
297 | }
|
298 | }
|
299 | });
|
300 |
|
301 |
|
302 |
|
303 | annotate(tree);
|
304 |
|
305 |
|
306 |
|
307 | const transformedFile = content.replace(/\/\/\#(.*)\r?\n?$/, '');
|
308 | internals.sources[filename] = transformedFile.replace(/(\r\n|\n|\r)/gm, '\n').split('\n');
|
309 |
|
310 |
|
311 |
|
312 | if (typeof global.__$$labCov === 'undefined') {
|
313 | global.__$$labCov = {
|
314 | files: {},
|
315 |
|
316 | _line: function (name, line) {
|
317 |
|
318 | global.__$$labCov.files[name].lines[line]++;
|
319 | },
|
320 |
|
321 | _statement: function (name, id, line, source) {
|
322 |
|
323 | const statement = global.__$$labCov.files[name].statements[line][id];
|
324 | if (!statement.bool) {
|
325 | statement.hit[!source] = true;
|
326 | }
|
327 |
|
328 | statement.hit[!!source] = true;
|
329 | return source;
|
330 | }
|
331 | };
|
332 | }
|
333 |
|
334 | if (typeof global.__$$labCov.files[filename] === 'undefined') {
|
335 | global.__$$labCov.files[filename] = {
|
336 | statements: {},
|
337 | lines: {},
|
338 | commentedLines: {}
|
339 | };
|
340 |
|
341 | const record = global.__$$labCov.files[filename];
|
342 | tracking.forEach((item) => {
|
343 |
|
344 | record.lines[item] = 0;
|
345 | });
|
346 |
|
347 | statements.forEach((item) => {
|
348 |
|
349 | record.statements[item.line] = record.statements[item.line] || {};
|
350 | record.statements[item.line][item.id] = { hit: {}, bool: item.bool, loc: item.loc };
|
351 | });
|
352 |
|
353 | const blank = /^\s*$/;
|
354 |
|
355 |
|
356 |
|
357 | record.sloc = internals.sources[filename].filter((line) => !blank.test(line)).length -
|
358 | tree.comments.map((node) => {
|
359 |
|
360 | const start = node.loc.start.line;
|
361 | const end = node.loc.end.line;
|
362 | let commented = 0;
|
363 |
|
364 | for (let i = start; i <= end; ++i) {
|
365 |
|
366 | if ((!nodesByLine[i] || !nodesByLine[i].find((type) => type !== 'Line' && type !== 'Block')) && !record.commentedLines[i]) {
|
367 | record.commentedLines[i] = true;
|
368 |
|
369 |
|
370 | if (i === start || i === end || !blank.test(internals.sources[filename][i - 1])) {
|
371 | commented++;
|
372 | }
|
373 | }
|
374 | }
|
375 |
|
376 | return commented;
|
377 | }).reduce((a, b) => a + b, 0);
|
378 |
|
379 | if (transformedFile !== content) {
|
380 | record.sloc++;
|
381 | }
|
382 | }
|
383 |
|
384 | return chunks.join('');
|
385 | };
|
386 |
|
387 |
|
388 | exports.analyze = async function (options) {
|
389 |
|
390 |
|
391 |
|
392 | const report = global.__$$labCov || { files: {} };
|
393 | const pattern = internals.pattern(options);
|
394 |
|
395 | const cov = {
|
396 | sloc: 0,
|
397 | hits: 0,
|
398 | misses: 0,
|
399 | percent: 0,
|
400 | files: []
|
401 | };
|
402 |
|
403 |
|
404 |
|
405 | const files = Object.keys(report.files);
|
406 | for (let i = 0; i < files.length; ++i) {
|
407 | const filename = files[i];
|
408 | if (pattern.test(filename)) {
|
409 | report.files[filename].source = internals.sources[filename] || [];
|
410 | const data = await internals.file(filename, report.files[filename], options);
|
411 |
|
412 | cov.files.push(data);
|
413 | cov.hits += data.hits;
|
414 | cov.misses += data.misses;
|
415 | cov.sloc += data.sloc;
|
416 | }
|
417 | }
|
418 |
|
419 |
|
420 |
|
421 | cov.files.sort((a, b) => {
|
422 |
|
423 | const segmentsA = a.filename.split('/');
|
424 | const segmentsB = b.filename.split('/');
|
425 |
|
426 | const al = segmentsA.length;
|
427 | const bl = segmentsB.length;
|
428 |
|
429 | for (let i = 0; i < al && i < bl; ++i) {
|
430 |
|
431 | if (segmentsA[i] === segmentsB[i]) {
|
432 | continue;
|
433 | }
|
434 |
|
435 | const lastA = i + 1 === al;
|
436 | const lastB = i + 1 === bl;
|
437 |
|
438 | if (lastA !== lastB) {
|
439 | return lastA ? -1 : 1;
|
440 | }
|
441 |
|
442 | return segmentsA[i] < segmentsB[i] ? -1 : 1;
|
443 | }
|
444 |
|
445 | return segmentsA.length < segmentsB.length ? -1 : 1;
|
446 | });
|
447 |
|
448 |
|
449 |
|
450 | if (cov.sloc > 0) {
|
451 | cov.percent = (cov.hits / cov.sloc) * 100;
|
452 | }
|
453 |
|
454 | return cov;
|
455 | };
|
456 |
|
457 | internals.addSourceMapsInformation = function (smc, ret, num) {
|
458 |
|
459 | const source = ret.source[num];
|
460 | const position = {
|
461 | source: ret.filename,
|
462 | line: num,
|
463 |
|
464 | column: source.source.length
|
465 | };
|
466 | let originalPosition = smc.originalPositionFor(position);
|
467 |
|
468 |
|
469 | source.originalFilename = originalPosition.source && originalPosition.source.replace(/\\/g, '/');
|
470 | source.originalLine = originalPosition.line;
|
471 |
|
472 | if (!ret.sourcemaps) {
|
473 | ret.sourcemaps = true;
|
474 | }
|
475 |
|
476 | if (source.chunks) {
|
477 | source.chunks.forEach((chunk) => {
|
478 |
|
479 | originalPosition = smc.originalPositionFor({ line: num, column: chunk.column });
|
480 | chunk.originalFilename = originalPosition.source;
|
481 | chunk.originalLine = originalPosition.line;
|
482 | chunk.originalColumn = originalPosition.column;
|
483 | });
|
484 | }
|
485 | };
|
486 |
|
487 |
|
488 | internals.file = async function (filename, data, options) {
|
489 |
|
490 | const ret = {
|
491 |
|
492 | filename: filename.replace(Path.join(process.cwd(), '/').replace(/\\/g, '/'), ''),
|
493 | percent: 0,
|
494 | hits: 0,
|
495 | misses: 0,
|
496 | sloc: data.sloc,
|
497 | source: {}
|
498 | };
|
499 |
|
500 |
|
501 | const sourcemap = options.sourcemaps ? SourceMapSupport.retrieveSourceMap(ret.filename) : null;
|
502 | const smc = sourcemap ? await new SourceMapConsumer(sourcemap.map) : null;
|
503 |
|
504 |
|
505 | data.source.forEach((line, num) => {
|
506 |
|
507 | num++;
|
508 |
|
509 | let isMiss = false;
|
510 | ret.source[num] = {
|
511 | source: line
|
512 | };
|
513 |
|
514 | if (data.lines[num] === 0) {
|
515 | isMiss = true;
|
516 | ret.misses++;
|
517 | }
|
518 | else if (line) {
|
519 | if (data.statements[num]) {
|
520 | const mask = new Array(line.length);
|
521 | Object.keys(data.statements[num]).forEach((id) => {
|
522 |
|
523 | const statement = data.statements[num][id];
|
524 | if (statement.hit.true &&
|
525 | statement.hit.false) {
|
526 |
|
527 | return;
|
528 | }
|
529 |
|
530 | if (statement.loc.start.line !== num) {
|
531 | data.statements[statement.loc.start.line] = data.statements[statement.loc.start.line] || {};
|
532 | data.statements[statement.loc.start.line][id] = statement;
|
533 | return;
|
534 | }
|
535 |
|
536 | if (statement.loc.end.line !== num) {
|
537 | data.statements[statement.loc.end.line] = data.statements[statement.loc.end.line] || {};
|
538 | data.statements[statement.loc.end.line][id] = {
|
539 | hit: statement.hit,
|
540 | loc: {
|
541 | start: {
|
542 | line: statement.loc.end.line,
|
543 | column: 0
|
544 | },
|
545 | end: {
|
546 | line: statement.loc.end.line,
|
547 | column: statement.loc.end.column
|
548 | }
|
549 | }
|
550 | };
|
551 |
|
552 | statement.loc.end.column = line.length;
|
553 | }
|
554 |
|
555 | isMiss = true;
|
556 | const issue = statement.hit.true ? 'true' : (statement.hit.false ? 'false' : 'never');
|
557 | for (let i = statement.loc.start.column; i < statement.loc.end.column; ++i) {
|
558 | mask[i] = issue;
|
559 | }
|
560 | });
|
561 |
|
562 | const chunks = [];
|
563 |
|
564 | let from = 0;
|
565 | for (let i = 1; i < mask.length; ++i) {
|
566 | if (mask[i] !== mask[i - 1]) {
|
567 | chunks.push({ source: line.slice(from, i), miss: mask[i - 1], column: from });
|
568 | from = i;
|
569 | }
|
570 | }
|
571 |
|
572 | chunks.push({ source: line.slice(from), miss: mask[from], column: from });
|
573 |
|
574 | if (isMiss) {
|
575 | ret.source[num].chunks = chunks;
|
576 | ret.misses++;
|
577 | }
|
578 | else {
|
579 | ret.hits++;
|
580 | }
|
581 | }
|
582 | else if (!data.commentedLines[num] && line.trim()) {
|
583 |
|
584 | ret.hits++;
|
585 | }
|
586 | }
|
587 |
|
588 | if (smc) {
|
589 | internals.addSourceMapsInformation(smc, ret, num);
|
590 | }
|
591 |
|
592 | ret.source[num].hits = data.lines[num];
|
593 | ret.source[num].miss = isMiss;
|
594 | });
|
595 |
|
596 | ret.percent = ret.hits / ret.sloc * 100;
|
597 | return ret;
|
598 | };
|