1 | 'use strict';
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | const Fs = require('fs');
|
8 | const Path = require('path');
|
9 |
|
10 | const BabelESLint = require('babel-eslint');
|
11 | const SourceMap = require('source-map');
|
12 | const SourceMapSupport = require('source-map-support');
|
13 |
|
14 | const Eslintrc = require('./linter/.eslintrc');
|
15 | const Transform = require('./transform');
|
16 |
|
17 |
|
18 | const internals = {
|
19 | ext: Symbol.for('@hapi/lab/coverage/initialize'),
|
20 | _state: Symbol.for('@hapi/lab/coverage/_state')
|
21 | };
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 | global[internals._state] = global[internals._state] || {
|
28 |
|
29 | modules: new Set(),
|
30 |
|
31 | externals: new Set(),
|
32 |
|
33 | files: {},
|
34 |
|
35 | _line: function (name, line) {
|
36 |
|
37 | global[internals._state].files[name].lines[line]++;
|
38 | },
|
39 |
|
40 | _statement: function (name, id, line, source) {
|
41 |
|
42 | const statement = global[internals._state].files[name].statements[line][id];
|
43 | if (!statement.bool) {
|
44 | statement.hit[!source] = true;
|
45 | }
|
46 |
|
47 | statement.hit[!!source] = true;
|
48 | return source;
|
49 | },
|
50 |
|
51 | _external: function (name, line, source) {
|
52 |
|
53 | const initialize = source[Symbol.for('@hapi/lab/coverage/initialize')];
|
54 | if (typeof initialize !== 'function') {
|
55 | throw new Error('Failed to find a compatible external coverage method in ' + name + ':' + line);
|
56 | }
|
57 |
|
58 | internals.state.externals.add(initialize());
|
59 | return source;
|
60 | }
|
61 | };
|
62 |
|
63 | internals.state = Object.assign({ patterns: [], sources: {} }, global[internals._state]);
|
64 |
|
65 | if (typeof global.__$$labCov === 'undefined') {
|
66 | global.__$$labCov = global[internals._state];
|
67 | }
|
68 |
|
69 |
|
70 |
|
71 | exports.instrument = function (options) {
|
72 |
|
73 | if (options['coverage-module']) {
|
74 | for (const name of options['coverage-module']) {
|
75 | internals.state.modules.add(name);
|
76 | }
|
77 | }
|
78 |
|
79 | internals.state.patterns.unshift(internals.pattern(options));
|
80 | Transform.install(options, internals.prime);
|
81 | };
|
82 |
|
83 |
|
84 | internals.pattern = function (options) {
|
85 |
|
86 | const coveragePath = options.coveragePath || '';
|
87 | const base = internals.escape(coveragePath);
|
88 | const excludes = options.coverageExclude ? [].concat(options.coverageExclude).map((path) => {
|
89 |
|
90 | let isFile = false;
|
91 | try {
|
92 | const pathStat = Fs.statSync(Path.join(coveragePath, path));
|
93 | isFile = pathStat.isFile();
|
94 | }
|
95 | catch (ex) {
|
96 | if (ex.code !== 'ENOENT') {
|
97 | console.error(ex);
|
98 | }
|
99 | }
|
100 |
|
101 | const escaped = internals.escape(path);
|
102 | return isFile ? escaped : `${escaped}\\/`;
|
103 | }).join('|') : '';
|
104 |
|
105 | const regex = '^' + base + (excludes ? (base[base.length - 1] === '/' ? '' : '\\/') + '(?!' + excludes + ')' : '');
|
106 | return new RegExp(regex);
|
107 | };
|
108 |
|
109 |
|
110 | internals.escape = function (string) {
|
111 |
|
112 | return string.replace(/\\/g, '/').replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&');
|
113 | };
|
114 |
|
115 |
|
116 | internals.prime = function (extension) {
|
117 |
|
118 | require.extensions[extension] = function (localModule, filename) {
|
119 |
|
120 | for (let i = 0; i < internals.state.patterns.length; ++i) {
|
121 | if (internals.state.patterns[i].test(filename.replace(/\\/g, '/'))) {
|
122 | return localModule._compile(internals.instrument(filename), filename);
|
123 | }
|
124 | }
|
125 |
|
126 | const src = Fs.readFileSync(filename, 'utf8');
|
127 | return localModule._compile(Transform.transform(filename, src), filename);
|
128 | };
|
129 | };
|
130 |
|
131 |
|
132 | internals.instrument = function (filename) {
|
133 |
|
134 | filename = filename.replace(/\\/g, '/');
|
135 |
|
136 | const file = Fs.readFileSync(filename, 'utf8');
|
137 | let content = file.replace(/^\#\!.*/, '');
|
138 | content = Transform.transform(filename, content);
|
139 |
|
140 | const tracking = [];
|
141 | const statements = [];
|
142 | const chunks = content.split('');
|
143 | let ids = 0;
|
144 | const bypass = {};
|
145 | const nodesByLine = {};
|
146 |
|
147 | const addStatement = function (line, node, bool) {
|
148 |
|
149 | const id = ++ids;
|
150 | statements.push({
|
151 | id,
|
152 | loc: node.loc,
|
153 | line,
|
154 | bool: bool && node.type !== 'ConditionalExpression' && node.type !== 'LogicalExpression'
|
155 | });
|
156 | return id;
|
157 | };
|
158 |
|
159 | const addToLines = function (node, line) {
|
160 |
|
161 | if (!(line in nodesByLine)) {
|
162 | nodesByLine[line] = [];
|
163 | }
|
164 |
|
165 | nodesByLine[line].push(node.type);
|
166 | };
|
167 |
|
168 | const annotate = function (node, parent) {
|
169 |
|
170 | const line = node.loc.start.line;
|
171 |
|
172 |
|
173 |
|
174 | node.parent = parent;
|
175 |
|
176 | node.source = function () {
|
177 |
|
178 | return chunks.slice(node.range[0], node.range[1]).join('');
|
179 | };
|
180 |
|
181 | node.set = function (s) {
|
182 |
|
183 | chunks[node.range[0]] = s;
|
184 | for (let i = node.range[0] + 1; i < node.range[1]; ++i) {
|
185 | chunks[i] = '';
|
186 | }
|
187 | };
|
188 |
|
189 |
|
190 |
|
191 | addToLines(node, line);
|
192 | const end = node.loc.end.line;
|
193 | if (end !== line) {
|
194 | addToLines(node, end);
|
195 | }
|
196 |
|
197 |
|
198 |
|
199 | const bypassTests = [];
|
200 | for (let i = node.range[0]; i <= node.range[1]; ++i) {
|
201 | bypassTests.push(bypass[i]);
|
202 | }
|
203 |
|
204 | if (bypassTests.every((test) => test)) {
|
205 | return;
|
206 | }
|
207 |
|
208 |
|
209 |
|
210 | for (const name in node) {
|
211 | if (name === 'parent') {
|
212 | continue;
|
213 | }
|
214 |
|
215 | const children = [].concat(node[name]);
|
216 | for (const child of children) {
|
217 | if (child &&
|
218 | typeof child.type === 'string') {
|
219 |
|
220 | annotate(child, node);
|
221 | }
|
222 | }
|
223 | }
|
224 |
|
225 |
|
226 |
|
227 | const decoratedTypes = [
|
228 | 'IfStatement',
|
229 | 'WhileStatement',
|
230 | 'DoWhileStatement',
|
231 | 'ForStatement',
|
232 | 'ForInStatement',
|
233 | 'WithStatement'
|
234 | ];
|
235 |
|
236 | if (decoratedTypes.includes(node.type)) {
|
237 | if (node.alternate &&
|
238 | node.alternate.type !== 'BlockStatement') {
|
239 |
|
240 | node.alternate.set(`{${node.alternate.source()}}`);
|
241 | }
|
242 |
|
243 | const consequent = node.consequent || node.body;
|
244 | if (consequent.type !== 'BlockStatement') {
|
245 | consequent.set(`{${consequent.source()}}`);
|
246 | }
|
247 | }
|
248 |
|
249 | if (node.type === 'ExpressionStatement' &&
|
250 | node.expression.value === 'use strict') {
|
251 |
|
252 | return;
|
253 | }
|
254 |
|
255 | if (node.type === 'SequenceExpression') {
|
256 | node.set(`(${node.source()})`);
|
257 | }
|
258 |
|
259 | const trackedTypes = [
|
260 | 'ExpressionStatement',
|
261 | 'BreakStatement',
|
262 | 'ContinueStatement',
|
263 | 'VariableDeclaration',
|
264 | 'ReturnStatement',
|
265 | 'ThrowStatement',
|
266 | 'TryStatement',
|
267 | 'IfStatement',
|
268 | 'WhileStatement',
|
269 | 'DoWhileStatement',
|
270 | 'ForStatement',
|
271 | 'ForInStatement',
|
272 | 'SwitchStatement',
|
273 | 'WithStatement',
|
274 | 'LabeledStatement'
|
275 | ];
|
276 |
|
277 | if (node.parent &&
|
278 | node.parent.type === 'BlockStatement' &&
|
279 | node.parent.parent.type.includes('FunctionExpression') &&
|
280 | node.parent.body[0] === node) {
|
281 |
|
282 | const id = addStatement(line, node, false);
|
283 |
|
284 | node.set(`global.__$$labCov._statement('${filename}', ${id}, ${line}, true); ${node.source()};`);
|
285 | }
|
286 | else if (trackedTypes.includes(node.type) &&
|
287 | (node.type !== 'VariableDeclaration' || (node.parent.type !== 'ForStatement' && node.parent.type !== 'ForInStatement' && node.parent.type !== 'ForOfStatement')) &&
|
288 | node.parent.type !== 'LabeledStatement') {
|
289 |
|
290 | tracking.push(line);
|
291 | node.set(`global.__$$labCov._line('${filename}',${line});${node.source()}`);
|
292 | }
|
293 | else if (node.type === 'ConditionalExpression') {
|
294 | const consequent = addStatement(line, node.consequent, false);
|
295 | const alternate = addStatement(line, node.alternate, false);
|
296 |
|
297 | node.set(`(${node.test.source()}? global.__$$labCov._statement('${filename}',${consequent},${line},(${node.consequent.source()})) : global.__$$labCov._statement('${filename}',${alternate},${line},(${node.alternate.source()})))`);
|
298 | }
|
299 | else if (node.type === 'LogicalExpression') {
|
300 | const left = addStatement(line, node.left, true);
|
301 | const right = addStatement(line, node.right, node.parent.type === 'LogicalExpression' || node.parent.type === 'IfStatement');
|
302 |
|
303 | node.set(`(global.__$$labCov._statement(\'${filename}\',${left},${line},${node.left.source()})${node.operator}global.__$$labCov._statement(\'${filename}\',${right},${line},${node.right.source()}))`);
|
304 | }
|
305 | else if (node.parent &&
|
306 | node.parent.type === 'ArrowFunctionExpression' &&
|
307 | node.type.includes('Expression')) {
|
308 |
|
309 | const id = addStatement(line, node, false);
|
310 |
|
311 | node.set(`global.__$$labCov._statement('${filename}', ${id}, ${line}, ${node.source()})`);
|
312 | }
|
313 | else if (node.parent &&
|
314 | node.parent.test === node &&
|
315 | node.parent.type !== 'SwitchCase') {
|
316 |
|
317 | const test = addStatement(line, node, true);
|
318 |
|
319 | node.set(`global.__$$labCov._statement(\'${filename}\',${test},${line},${node.source()})`);
|
320 | }
|
321 | else if (node.type === 'CallExpression' &&
|
322 | node.callee.name === 'require') {
|
323 |
|
324 | const name = node.arguments[0].value;
|
325 | if (internals.state.modules.has(name)) {
|
326 | node.set(`global.__$$labCov._external(\'${filename}\',${line},${node.source()})`);
|
327 | }
|
328 | }
|
329 | };
|
330 |
|
331 |
|
332 |
|
333 | const tree = BabelESLint.parse(content, Eslintrc.parserOptions);
|
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 | let skipStart = 0;
|
342 | let segmentSkip = false;
|
343 | const skipStack = [];
|
344 |
|
345 | for (const comment of tree.comments) {
|
346 | const directive = comment.value.match(/^\s*\$lab\:coverage\:(off|on|push|pop|ignore)\$\s*$/);
|
347 | if (!directive) {
|
348 | continue;
|
349 | }
|
350 |
|
351 | const command = directive[1];
|
352 | if (command === 'push') {
|
353 | skipStack.push(segmentSkip);
|
354 | continue;
|
355 | }
|
356 |
|
357 | if (command === 'ignore') {
|
358 | for (let i = comment.start - comment.loc.start.column; i < comment.start; ++i) {
|
359 | bypass[i] = true;
|
360 | }
|
361 |
|
362 | continue;
|
363 | }
|
364 |
|
365 | let newSkipState;
|
366 | if (command === 'pop') {
|
367 | if (!skipStack.length) {
|
368 | throw new Error('unable to pop coverage bypass stack');
|
369 | }
|
370 |
|
371 | newSkipState = skipStack.pop();
|
372 | }
|
373 | else {
|
374 | newSkipState = command !== 'on';
|
375 | }
|
376 |
|
377 | if (newSkipState !== segmentSkip) {
|
378 | segmentSkip = newSkipState;
|
379 | if (newSkipState) {
|
380 | skipStart = comment.range[1];
|
381 | }
|
382 | else {
|
383 | for (let i = skipStart; i < comment.range[0]; ++i) {
|
384 | bypass[i] = true;
|
385 | }
|
386 | }
|
387 | }
|
388 | }
|
389 |
|
390 |
|
391 |
|
392 | annotate(tree);
|
393 |
|
394 |
|
395 |
|
396 | internals.state.sources[filename] = content.replace(/(\r\n|\n|\r)/gm, '\n').split('\n');
|
397 |
|
398 |
|
399 |
|
400 | if (!internals.state.files[filename]) {
|
401 | internals.state.files[filename] = {
|
402 | statements: {},
|
403 | lines: {},
|
404 | commentedLines: {}
|
405 | };
|
406 |
|
407 | const record = internals.state.files[filename];
|
408 | tracking.forEach((item) => {
|
409 |
|
410 | record.lines[item] = 0;
|
411 | });
|
412 |
|
413 | statements.forEach((item) => {
|
414 |
|
415 | record.statements[item.line] = record.statements[item.line] || {};
|
416 | record.statements[item.line][item.id] = { hit: {}, bool: item.bool, loc: item.loc };
|
417 | });
|
418 |
|
419 | const blank = /^\s*$/;
|
420 |
|
421 |
|
422 |
|
423 |
|
424 |
|
425 | record.sloc = internals.state.sources[filename].filter((line) => !blank.test(line)).length -
|
426 | tree.comments.map((node) => {
|
427 |
|
428 | const start = node.loc.start.line;
|
429 | const end = node.loc.end.line;
|
430 | let commented = 0;
|
431 |
|
432 |
|
433 |
|
434 | for (let i = start; i <= end; ++i) {
|
435 |
|
436 |
|
437 |
|
438 | if ((!nodesByLine[i] ||
|
439 | !nodesByLine[i].find((type) => type !== 'Line' && type !== 'Block')) &&
|
440 | !record.commentedLines[i]) {
|
441 |
|
442 | record.commentedLines[i] = true;
|
443 |
|
444 |
|
445 |
|
446 | if (i === start ||
|
447 | i === end ||
|
448 | !blank.test(internals.state.sources[filename][i - 1])) {
|
449 |
|
450 | commented++;
|
451 | }
|
452 | }
|
453 | }
|
454 |
|
455 | return commented;
|
456 | })
|
457 | .reduce((a, b) => a + b, 0);
|
458 | }
|
459 |
|
460 | return chunks.join('');
|
461 | };
|
462 |
|
463 |
|
464 | internals.traverse = function (coveragePath, options) {
|
465 |
|
466 | let nextPath = null;
|
467 | const traverse = function (path) {
|
468 |
|
469 | let files = [];
|
470 | nextPath = path;
|
471 |
|
472 | const pathStat = Fs.statSync(path);
|
473 | if (pathStat.isFile()) {
|
474 | return path;
|
475 | }
|
476 |
|
477 | Fs.readdirSync(path).forEach((filename) => {
|
478 |
|
479 | nextPath = Path.join(path, filename);
|
480 | const basename = Path.basename(nextPath);
|
481 | const stat = Fs.statSync(nextPath);
|
482 | if (stat.isDirectory() &&
|
483 | basename[0] !== '.' &&
|
484 | !options['coverage-flat']) {
|
485 |
|
486 | files = files.concat(traverse(nextPath, options));
|
487 | return;
|
488 | }
|
489 |
|
490 | if (stat.isFile() &&
|
491 | basename[0] !== '.' &&
|
492 | options.coveragePattern.test(nextPath.replace(/\\/g, '/'))) {
|
493 |
|
494 | files.push(nextPath);
|
495 | }
|
496 | });
|
497 |
|
498 | return files;
|
499 | };
|
500 |
|
501 | const coverageFiles = [].concat(traverse(coveragePath));
|
502 |
|
503 | return coverageFiles.map((path) => {
|
504 |
|
505 | return Path.resolve(path);
|
506 | });
|
507 | };
|
508 |
|
509 |
|
510 | exports.analyze = async function (options) {
|
511 |
|
512 |
|
513 |
|
514 | const report = internals.state.files;
|
515 | const pattern = internals.pattern(options);
|
516 |
|
517 | const cov = {
|
518 | sloc: 0,
|
519 | hits: 0,
|
520 | misses: 0,
|
521 | percent: 0,
|
522 | externals: 0,
|
523 | files: []
|
524 | };
|
525 |
|
526 |
|
527 |
|
528 | const files = options['coverage-all'] ? internals.traverse(options.coveragePath, options) : Object.keys(report);
|
529 | for (const file of files) {
|
530 | const filename = file.replace(/\\/g, '/');
|
531 | if (pattern.test(filename)) {
|
532 | if (!report[filename]) {
|
533 | internals.instrument(filename);
|
534 | }
|
535 |
|
536 | report[filename].source = internals.state.sources[filename] || [];
|
537 | const data = await internals.file(filename, report[filename], options);
|
538 |
|
539 | cov.files.push(data);
|
540 | cov.hits += data.hits;
|
541 | cov.misses += data.misses;
|
542 | cov.sloc += data.sloc;
|
543 | cov.externals += data.externals ? data.externals.length : 0;
|
544 | }
|
545 | }
|
546 |
|
547 |
|
548 |
|
549 | cov.files.sort((a, b) => {
|
550 |
|
551 | const segmentsA = a.filename.split('/');
|
552 | const segmentsB = b.filename.split('/');
|
553 |
|
554 | const al = segmentsA.length;
|
555 | const bl = segmentsB.length;
|
556 |
|
557 | for (let i = 0; i < al && i < bl; ++i) {
|
558 |
|
559 | if (segmentsA[i] === segmentsB[i]) {
|
560 | continue;
|
561 | }
|
562 |
|
563 | const lastA = i + 1 === al;
|
564 | const lastB = i + 1 === bl;
|
565 |
|
566 | if (lastA !== lastB) {
|
567 | return lastA ? -1 : 1;
|
568 | }
|
569 |
|
570 | return segmentsA[i] < segmentsB[i] ? -1 : 1;
|
571 | }
|
572 |
|
573 | return segmentsA.length < segmentsB.length ? -1 : 1;
|
574 | });
|
575 |
|
576 |
|
577 |
|
578 | if (cov.sloc > 0) {
|
579 | cov.percent = (cov.hits / cov.sloc) * 100;
|
580 | }
|
581 |
|
582 | return cov;
|
583 | };
|
584 |
|
585 |
|
586 | internals.addSourceMapsInformation = function (smc, ret, num) {
|
587 |
|
588 | const source = ret.source[num];
|
589 | const position = {
|
590 | source: ret.filename,
|
591 | line: num,
|
592 |
|
593 | column: source.source.length
|
594 | };
|
595 | let originalPosition = smc.originalPositionFor(position);
|
596 |
|
597 |
|
598 | source.originalFilename = originalPosition.source && originalPosition.source.replace(/\\/g, '/');
|
599 | source.originalLine = originalPosition.line;
|
600 |
|
601 | if (!ret.sourcemaps) {
|
602 | ret.sourcemaps = true;
|
603 | }
|
604 |
|
605 | if (source.chunks) {
|
606 | source.chunks.forEach((chunk) => {
|
607 |
|
608 | originalPosition = smc.originalPositionFor({ line: num, column: chunk.column });
|
609 | chunk.originalFilename = originalPosition.source;
|
610 | chunk.originalLine = originalPosition.line;
|
611 | chunk.originalColumn = originalPosition.column;
|
612 | });
|
613 | }
|
614 | };
|
615 |
|
616 |
|
617 | internals.file = async function (filename, data, options) {
|
618 |
|
619 | const ret = {
|
620 |
|
621 | filename: filename.replace(Path.join(process.cwd(), '/').replace(/\\/g, '/'), ''),
|
622 | percent: 0,
|
623 | hits: 0,
|
624 | misses: 0,
|
625 | sloc: data.sloc,
|
626 | source: {},
|
627 | externals: internals.external(filename)
|
628 | };
|
629 |
|
630 |
|
631 |
|
632 | let sourcemap = null;
|
633 | if (options.sourcemaps) {
|
634 | sourcemap = SourceMapSupport.retrieveSourceMap(ret.filename);
|
635 | if (!sourcemap) {
|
636 | const smre = /\/\/\#.*data:application\/json[^,]+base64,.*\r?\n?$/;
|
637 | let sourceIndex = data.source.length - 1;
|
638 | while (sourceIndex >= 0 && !smre.test(data.source[sourceIndex])) {
|
639 | sourceIndex--;
|
640 | }
|
641 |
|
642 | if (sourceIndex >= 0) {
|
643 | const re = /(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)|(?:\/\*[@#][ \t]+sourceMappingURL=([^\*]+?)[ \t]*(?:\*\/)[ \t]*$)/mg;
|
644 | let lastMatch;
|
645 | let match;
|
646 | while (match = re.exec(data.source[sourceIndex])) {
|
647 | lastMatch = match;
|
648 | }
|
649 |
|
650 | sourcemap = {
|
651 | url: lastMatch[1],
|
652 | map: Buffer.from(lastMatch[1].slice(lastMatch[1].indexOf(',') + 1), 'base64').toString()
|
653 | };
|
654 | }
|
655 | }
|
656 | }
|
657 |
|
658 | const smc = sourcemap ? await new SourceMap.SourceMapConsumer(sourcemap.map) : null;
|
659 |
|
660 |
|
661 |
|
662 | data.source.forEach((line, num) => {
|
663 |
|
664 | num++;
|
665 |
|
666 | let isMiss = false;
|
667 | ret.source[num] = {
|
668 | source: line
|
669 | };
|
670 |
|
671 | if (data.lines[num] === 0) {
|
672 | isMiss = true;
|
673 | ret.misses++;
|
674 | }
|
675 | else if (line) {
|
676 | if (data.statements[num]) {
|
677 | const mask = new Array(line.length);
|
678 | Object.keys(data.statements[num]).forEach((id) => {
|
679 |
|
680 | const statement = data.statements[num][id];
|
681 | if (statement.hit.true &&
|
682 | statement.hit.false) {
|
683 |
|
684 | return;
|
685 | }
|
686 |
|
687 | if (statement.loc.start.line !== num) {
|
688 | data.statements[statement.loc.start.line] = data.statements[statement.loc.start.line] || {};
|
689 | data.statements[statement.loc.start.line][id] = statement;
|
690 | return;
|
691 | }
|
692 |
|
693 | if (statement.loc.end.line !== num) {
|
694 | data.statements[statement.loc.end.line] = data.statements[statement.loc.end.line] || {};
|
695 | data.statements[statement.loc.end.line][id] = {
|
696 | hit: statement.hit,
|
697 | loc: {
|
698 | start: {
|
699 | line: statement.loc.end.line,
|
700 | column: 0
|
701 | },
|
702 | end: {
|
703 | line: statement.loc.end.line,
|
704 | column: statement.loc.end.column
|
705 | }
|
706 | }
|
707 | };
|
708 |
|
709 | statement.loc.end.column = line.length;
|
710 | }
|
711 |
|
712 | isMiss = true;
|
713 | const issue = statement.hit.true ? 'true' : (statement.hit.false ? 'false' : 'never');
|
714 | for (let i = statement.loc.start.column; i < statement.loc.end.column; ++i) {
|
715 | mask[i] = issue;
|
716 | }
|
717 | });
|
718 |
|
719 | const chunks = [];
|
720 |
|
721 | let from = 0;
|
722 | for (let i = 1; i < mask.length; ++i) {
|
723 | if (mask[i] !== mask[i - 1]) {
|
724 | chunks.push({ source: line.slice(from, i), miss: mask[i - 1], column: from });
|
725 | from = i;
|
726 | }
|
727 | }
|
728 |
|
729 | chunks.push({ source: line.slice(from), miss: mask[from], column: from });
|
730 |
|
731 | if (isMiss) {
|
732 | ret.source[num].chunks = chunks;
|
733 | ret.misses++;
|
734 | }
|
735 | else {
|
736 | ret.hits++;
|
737 | }
|
738 | }
|
739 | else if (!data.commentedLines[num] &&
|
740 | line.trim()) {
|
741 |
|
742 | ret.hits++;
|
743 | }
|
744 | }
|
745 |
|
746 | if (smc) {
|
747 | internals.addSourceMapsInformation(smc, ret, num);
|
748 | }
|
749 |
|
750 | ret.source[num].hits = data.lines[num];
|
751 | ret.source[num].miss = isMiss;
|
752 | });
|
753 |
|
754 | ret.percent = ret.hits / ret.sloc * 100;
|
755 | return ret;
|
756 | };
|
757 |
|
758 |
|
759 | internals.external = function (filename) {
|
760 |
|
761 | filename = Path.normalize(filename);
|
762 |
|
763 | const reports = [];
|
764 | for (const external of internals.state.externals) {
|
765 | const report = external.report(filename);
|
766 | if (report) {
|
767 | const items = [].concat(report);
|
768 | for (const item of items) {
|
769 | reports.push(Object.assign({}, item, { source: external.name }));
|
770 | }
|
771 | }
|
772 | }
|
773 |
|
774 | return reports.length ? reports : null;
|
775 | };
|