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