monocart-coverage-reports 2.5.9 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1429 +1,1487 @@
1
- /**
2
- * V8 Coverage Data Converter
3
- * @copyright https://github.com/cenfun/monocart-coverage-reports
4
- * @author cenfun@gmail.com
5
- */
6
- const Util = require('../utils/util.js');
7
-
8
- // position mapping for conversion between offset and line/column
9
- const { Locator } = require('monocart-formatter');
10
- const findOriginalRange = require('./find-original-range.js');
11
- const { getJsAstInfo, getCssAstInfo } = require('./ast.js');
12
- const { getIgnoredRanges } = require('./ignore.js');
13
-
14
- const {
15
- sortRanges, dedupeCountRanges, mergeRangesWith
16
- } = require('../utils/dedupe.js');
17
- const {
18
- getSourceType, getSourcePathHandler, initSourceMapSourcePath
19
- } = require('../utils/source-path.js');
20
- const { decode, minimatch } = require('../packages/monocart-coverage-vendor.js');
21
-
22
- const InfoBranch = require('./info-branch.js');
23
- const InfoFunction = require('./info-function.js');
24
- const InfoStatement = require('./info-statement.js');
25
-
26
- // ========================================================================================================
27
-
28
- // debug info
29
- const logMappingErrors = (type, result) => {
30
-
31
- // const {
32
- // errors, start, end, sourcePath
33
- // } = result;
34
- // // if (!sourcePath.endsWith('monocart-v8.js') || type !== 'byte') {
35
- // // return;
36
- // // }
37
- // const list = [
38
- // Util.EC.red('[Mapping]'),
39
- // Util.EC.magenta(type),
40
- // errors.join(' -> '),
41
- // Util.EC.blue(`${start} ~ ${end}`),
42
- // sourcePath
43
- // ];
44
- // console.log(list.join(' '));
45
-
46
- };
47
-
48
- // ========================================================================================================
49
-
50
- const handleIgnoredRanges = (list, ignoredRanges) => {
51
- list.forEach((item) => {
52
- const range = Util.findInRanges(item.start, item.end, ignoredRanges, 'start', 'end');
53
- if (range) {
54
- // console.log(item, range);
55
- item.ignored = true;
56
- }
57
- });
58
- };
59
-
60
-
61
- const applyBytesToLines = (bytes, locator, lineMap) => {
62
- bytes.forEach((range) => {
63
- const {
64
- start, end, count, ignored
65
- } = range;
66
-
67
- // no need handle ignored and uncovered byte
68
- if (count === 0 && ignored) {
69
- return;
70
- }
71
-
72
- const sLoc = locator.offsetToLocation(start);
73
- const eLoc = locator.offsetToLocation(end);
74
-
75
- // update lines coverage
76
- const lines = Util.getRangeLines(sLoc, eLoc);
77
-
78
- lines.forEach((it) => {
79
- const lineItem = lineMap.get(it.line);
80
- if (!lineItem) {
81
- // not found line, could be comment or blank line
82
- return;
83
- }
84
- if (lineItem.ignored) {
85
- return;
86
- }
87
-
88
- it.count = count;
89
-
90
- // default is covered, so only focus on
91
- // 1, biggest covered count
92
- // 2, uncovered entire and pieces
93
- if (count > 0) {
94
- lineItem.coveredCount = Math.max(lineItem.coveredCount, count);
95
- } else {
96
- if (it.entire) {
97
- lineItem.uncoveredEntire = it;
98
- } else {
99
- lineItem.uncoveredPieces.push(it);
100
- }
101
- }
102
- });
103
-
104
- });
105
-
106
- // no ignore items
107
- lineMap.forEach((lineItem, line) => {
108
-
109
- const {
110
- uncoveredEntire, uncoveredPieces, coveredCount
111
- } = lineItem;
112
-
113
- // default count to 1, both js and css
114
- let count = 1;
115
- // full covered true/false for entire line
116
- let covered = true;
117
-
118
- if (uncoveredEntire) {
119
- count = 0;
120
- covered = false;
121
- } else {
122
- count = coveredCount;
123
- const uncoveredLen = uncoveredPieces.length;
124
- if (uncoveredLen > 0) {
125
- covered = false;
126
- // uncovered
127
- count = `1/${uncoveredLen + 1}`;
128
- }
129
- }
130
-
131
- lineItem.covered = covered;
132
- lineItem.count = count;
133
-
134
- });
135
-
136
- };
137
-
138
- const handleLinesCoverage = (bytes, locator, ignoredRanges) => {
139
-
140
- // init lines
141
- let blankCount = 0;
142
- let commentCount = 0;
143
- // line 1 based
144
- const lineMap = new Map();
145
- locator.lines.forEach((lineItem) => {
146
- // exclude blank and comment
147
- if (lineItem.blank) {
148
- blankCount += 1;
149
- return;
150
- }
151
- if (lineItem.comment) {
152
- commentCount += 1;
153
- return;
154
- }
155
- const ignored = Util.findInRanges(lineItem.start, lineItem.end, ignoredRanges);
156
- if (ignored) {
157
- return;
158
- }
159
- // line 1-base
160
- const line = lineItem.line + 1;
161
- lineItem.coveredCount = 1;
162
- lineItem.uncoveredEntire = null;
163
- lineItem.uncoveredPieces = [];
164
- lineMap.set(line, lineItem);
165
- });
166
-
167
- applyBytesToLines(bytes, locator, lineMap);
168
-
169
- const summaryLines = {
170
- total: 0,
171
- covered: 0,
172
- blank: blankCount,
173
- comment: commentCount
174
- };
175
- // data lines for codecov
176
- // https://docs.codecov.com/docs/codecov-custom-coverage-format
177
- const dataLines = {};
178
-
179
- // no ignore items
180
- lineMap.forEach((lineItem, line) => {
181
- const { count, covered } = lineItem;
182
- // data lines
183
- dataLines[line] = count;
184
-
185
- summaryLines.total += 1;
186
- if (covered) {
187
- summaryLines.covered += 1;
188
- }
189
- });
190
-
191
- return {
192
- dataLines,
193
- summaryLines
194
- };
195
- };
196
-
197
- // ========================================================================================================
198
-
199
- const calculateV8Summary = (list) => {
200
-
201
- const summary = {
202
- total: 0,
203
- covered: 0
204
- };
205
-
206
- list.forEach((item) => {
207
- if (item.ignored) {
208
- return;
209
- }
210
-
211
- summary.total += 1;
212
- if (item.count > 0) {
213
- summary.covered += 1;
214
- }
215
- });
216
-
217
- return summary;
218
- };
219
-
220
- // ========================================================================================================
221
- // istanbul coverage format
222
- // https://github.com/istanbuljs/istanbuljs/blob/master/docs/raw-output.md
223
- /**
224
- * * `path` - the file path for which coverage is being tracked
225
- * * `statementMap` - map of statement locations keyed by statement index
226
- * * `fnMap` - map of function metadata keyed by function index
227
- * * `branchMap` - map of branch metadata keyed by branch index
228
- * * `s` - hit counts for statements
229
- * * `f` - hit count for functions
230
- * * `b` - hit count for branches
231
- */
232
- const collectFileCoverage = (v8Data, state, options) => {
233
-
234
- const {
235
- bytes,
236
- functions,
237
- branches,
238
- statements,
239
-
240
- sourcePath,
241
- locator
242
- } = state;
243
-
244
- // ==========================================
245
- // v8 data
246
- const data = {
247
- bytes: dedupeCountRanges(bytes),
248
- functions: [],
249
- branches: [],
250
- statements: []
251
- };
252
-
253
- // ==========================================
254
- // ignore
255
- const ignoredRanges = getIgnoredRanges(locator, options);
256
- if (ignoredRanges) {
257
-
258
- data.ignores = ignoredRanges;
259
-
260
- // data bytes is start/end/count object
261
- handleIgnoredRanges(data.bytes, ignoredRanges);
262
-
263
- // functions start/end/count instance
264
- handleIgnoredRanges(functions, ignoredRanges);
265
-
266
- // branches start/end/count instance
267
- handleIgnoredRanges(branches, ignoredRanges);
268
-
269
- // branch locations start/end/count object
270
- branches.forEach((group) => {
271
- if (group.ignored) {
272
- // all branch group ignored
273
- group.locations.forEach((it) => {
274
- it.ignored = true;
275
- });
276
- } else {
277
- handleIgnoredRanges(group.locations, ignoredRanges);
278
- }
279
- });
280
-
281
- // statements start/end/count instance
282
- handleIgnoredRanges(statements, ignoredRanges);
283
-
284
- // console.log(ignoredRanges);
285
-
286
- }
287
-
288
- // ==========================================
289
- // lines
290
- // after bytes with ignored, before calculateV8Lines
291
- const { dataLines, summaryLines } = handleLinesCoverage(data.bytes, locator, ignoredRanges);
292
-
293
- data.lines = dataLines;
294
-
295
- // console.log('statements', state.sourcePath, statements.length);
296
-
297
- // ==========================================
298
-
299
- data.functions = functions.map((info) => {
300
- return info.getRange();
301
- });
302
- sortRanges(data.functions);
303
-
304
- // branch group with locations to flat branches
305
- data.branches = branches.map((info) => {
306
- return info.getRanges();
307
- }).flat();
308
- sortRanges(data.branches);
309
-
310
- data.statements = statements.map((info) => {
311
- return info.getRange();
312
- });
313
- sortRanges(data.statements);
314
-
315
- // ==========================================
316
-
317
- const summary = {
318
- functions: calculateV8Summary(data.functions),
319
- branches: calculateV8Summary(data.branches),
320
- statements: calculateV8Summary(data.statements),
321
- lines: summaryLines
322
- };
323
-
324
- // ==========================================
325
- // v8 data and summary
326
- v8Data.data = data;
327
- v8Data.summary = summary;
328
-
329
- // ==========================================
330
- // istanbul
331
- const istanbulData = {
332
- path: sourcePath,
333
-
334
- statementMap: {},
335
- fnMap: {},
336
- branchMap: {},
337
-
338
- s: {},
339
- f: {},
340
- b: {}
341
- };
342
-
343
- statements.filter((it) => !it.ignored).forEach((statement, index) => {
344
- istanbulData.statementMap[`${index}`] = statement.generate(locator);
345
- istanbulData.s[`${index}`] = statement.count;
346
- });
347
-
348
- functions.filter((it) => !it.ignored).forEach((fn, index) => {
349
- istanbulData.fnMap[`${index}`] = fn.generate(locator, index);
350
- istanbulData.f[`${index}`] = fn.count;
351
- });
352
-
353
- branches.filter((it) => !it.ignored).forEach((branch, index) => {
354
- const { map, counts } = branch.generate(locator);
355
- istanbulData.branchMap[`${index}`] = map;
356
- istanbulData.b[`${index}`] = counts;
357
- });
358
-
359
- return istanbulData;
360
-
361
- };
362
-
363
- // ========================================================================================================
364
-
365
- const addJsBytesCoverage = (state, range) => {
366
- const {
367
- startOffset, endOffset, count
368
- } = range;
369
- // add bytes range
370
- const byte = {
371
- start: startOffset,
372
- end: endOffset,
373
- count
374
- };
375
- // for debug
376
- if (Util.isDebug() && state.original) {
377
- byte.generatedStart = range.generatedStart;
378
- byte.generatedEnd = range.generatedEnd;
379
- }
380
- state.bytes.push(byte);
381
- };
382
-
383
- const addCssBytesCoverage = (state, range) => {
384
- const {
385
- start, end, count
386
- } = range;
387
- // add css bytes range, already start, end
388
- state.bytes.push({
389
- start,
390
- end,
391
- count
392
- });
393
- };
394
-
395
- // ========================================================================================================
396
-
397
- const handleFunctionsCoverage = (state) => {
398
-
399
- // functions only for js
400
- if (!state.js) {
401
- return;
402
- }
403
-
404
- const { functions, astInfo } = state;
405
- astInfo.functions.forEach((it) => {
406
-
407
- const { start, end } = it;
408
- if (start >= end) {
409
- // console.log('invalid function', start, end);
410
- return;
411
- }
412
-
413
- functions.push(new InfoFunction(it, state.original));
414
- });
415
-
416
- };
417
-
418
- const handleBranchesCoverage = (state) => {
419
-
420
- // functions only for js
421
- if (!state.js) {
422
- return;
423
- }
424
-
425
- const { branches, astInfo } = state;
426
- astInfo.branches.forEach((it) => {
427
-
428
- const { start, end } = it;
429
- if (start >= end) {
430
- // console.log('invalid branch', state.sourcePath, it);
431
- return;
432
- }
433
-
434
- branches.push(new InfoBranch(it, state.original));
435
- });
436
-
437
- };
438
-
439
- const handleStatementsCoverage = (state) => {
440
-
441
- // statement only for js
442
- if (!state.js) {
443
- return;
444
- }
445
-
446
- const { statements, astInfo } = state;
447
- astInfo.statements.forEach((it) => {
448
-
449
- const { start, end } = it;
450
- if (start >= end) {
451
- // console.log('invalid statement', start, end);
452
- return;
453
- }
454
-
455
- statements.push(new InfoStatement(it, state.original));
456
- });
457
-
458
- };
459
-
460
- const handleBytesCoverage = (state) => {
461
-
462
- const { js, coverageList } = state;
463
-
464
- if (js) {
465
- coverageList.forEach((block) => {
466
- block.ranges.forEach((range) => {
467
- addJsBytesCoverage(state, range);
468
- });
469
- });
470
- } else {
471
- coverageList.forEach((range) => {
472
- addCssBytesCoverage(state, range);
473
- });
474
- }
475
-
476
- };
477
-
478
- // ========================================================================================================
479
-
480
- const handleOriginalFunctionsCoverage = (state, originalStateMap) => {
481
-
482
- // functions only for js
483
- if (!state.js) {
484
- return;
485
- }
486
-
487
- // console.log(state.astInfo.functions);
488
-
489
- const updateFunctionBodyRange = (start, end, bodyStart, bodyEnd, originalFunction) => {
490
- if (bodyStart !== start || bodyEnd !== end) {
491
- const result = findOriginalRange(bodyStart, bodyEnd, state, originalStateMap);
492
- if (result.error) {
493
- logMappingErrors('function body', result);
494
- } else {
495
- originalFunction.bodyStart = result.start;
496
- originalFunction.bodyEnd = result.end;
497
- }
498
- }
499
- };
500
-
501
- // function count
502
- state.astInfo.functions.forEach((it) => {
503
- const {
504
- start, end, bodyStart, bodyEnd, wrap
505
- } = it;
506
-
507
- // remove webpack wrap functions for functions count, not for ranges here
508
- if (wrap) {
509
- return;
510
- }
511
-
512
- const result = findOriginalRange(start, end, state, originalStateMap);
513
- if (result.error) {
514
- logMappingErrors('function', result);
515
- return;
516
- }
517
-
518
- const originalFunction = {
519
- ... it,
520
- generatedStart: start,
521
- generatedEnd: end,
522
- start: result.start,
523
- end: result.end,
524
- bodyStart: result.start,
525
- bodyEnd: result.end
526
- };
527
-
528
- // body start and end
529
- updateFunctionBodyRange(start, end, bodyStart, bodyEnd, originalFunction);
530
-
531
- // add back to original ast
532
- result.originalState.astInfo.functions.push(originalFunction);
533
-
534
- });
535
-
536
- };
537
-
538
- const handleOriginalBranchesCoverage = (state, originalStateMap) => {
539
-
540
- // branches only for js
541
- if (!state.js) {
542
- return;
543
- }
544
-
545
- // console.log(state.astInfo.branches);
546
-
547
- // function count
548
- state.astInfo.branches.forEach((group) => {
549
-
550
- const { type, locations } = group;
551
-
552
- // start
553
- const result = findOriginalRange(group.start, group.end, state, originalStateMap);
554
- if (result.error) {
555
- logMappingErrors('branch group', result);
556
- return;
557
- }
558
-
559
- // new group start and end
560
- const groupStart = result.start;
561
- const groupEnd = result.end;
562
-
563
- let hasError;
564
- const newLocations = locations.map((oLoc) => {
565
-
566
- const newLoc = {
567
- ... oLoc
568
- };
569
-
570
- if (newLoc.none) {
571
- newLoc.start = groupStart;
572
- newLoc.end = groupEnd;
573
- return newLoc;
574
- }
575
-
576
- const locResult = findOriginalRange(newLoc.start, newLoc.end, state, originalStateMap);
577
- if (locResult.error) {
578
- // It should not happen unless it is minify files, the SourceMap has some order problems
579
- logMappingErrors('branch', locResult);
580
- hasError = true;
581
- return newLoc;
582
- }
583
-
584
- // before new range
585
- newLoc.generatedStart = newLoc.start;
586
- newLoc.generatedEnd = newLoc.end;
587
- // mapping to new range
588
- newLoc.start = locResult.start;
589
- newLoc.end = locResult.end;
590
-
591
- return newLoc;
592
- });
593
-
594
- // ignored group when found error
595
- if (hasError) {
596
- return;
597
- }
598
-
599
- // add back to original ast
600
- result.originalState.astInfo.branches.push({
601
- type,
602
- start: groupStart,
603
- end: groupEnd,
604
- locations: newLocations
605
- });
606
-
607
- });
608
-
609
- };
610
-
611
- const handleOriginalStatementsCoverage = (state, originalStateMap) => {
612
-
613
- // statements only for js
614
- if (!state.js) {
615
- return;
616
- }
617
-
618
- // statement count
619
- state.astInfo.statements.forEach((it) => {
620
- const { start, end } = it;
621
-
622
- const result = findOriginalRange(start, end, state, originalStateMap);
623
- if (result.error) {
624
- logMappingErrors('statement', result);
625
- return;
626
- }
627
-
628
- // add back to original ast
629
- result.originalState.astInfo.statements.push({
630
- ... it,
631
- generatedStart: start,
632
- generatedEnd: end,
633
- start: result.start,
634
- end: result.end
635
- });
636
-
637
- });
638
-
639
- originalStateMap.forEach((originalState) => {
640
- const statements = originalState.astInfo.statements;
641
- if (Util.isList(statements)) {
642
- return;
643
- }
644
- // fake source nothing matched
645
- statements.push({
646
- start: 0,
647
- end: originalState.source.length,
648
- count: 1
649
- });
650
-
651
- });
652
-
653
- };
654
-
655
- const handleOriginalBytesCoverage = (state, originalStateMap) => {
656
-
657
- const { js, coverageList } = state;
658
-
659
- // const time_start_mapping = Date.now();
660
- if (js) {
661
- // v8 coverage
662
- coverageList.forEach((block) => {
663
- block.ranges.forEach((range, index) => {
664
-
665
- // remove wrap functions for original files
666
- if (range.wrap) {
667
- // console.log(range);
668
- return;
669
- }
670
-
671
- const {
672
- startOffset, endOffset, count
673
- } = range;
674
-
675
- const result = findOriginalRange(startOffset, endOffset, state, originalStateMap);
676
- if (result.error) {
677
- logMappingErrors('byte', result);
678
- return;
679
- }
680
-
681
- addJsBytesCoverage(result.originalState, {
682
- generatedStart: startOffset,
683
- generatedEnd: endOffset,
684
- startOffset: result.start,
685
- endOffset: result.end,
686
- count
687
- });
688
-
689
- });
690
- });
691
-
692
- } else {
693
- // support css later
694
- // current css no sourceMap, so never come in
695
- // coverageList.forEach((range) => {
696
-
697
- // });
698
- }
699
-
700
-
701
- const checkList = [];
702
-
703
- // check original bytes if fully in a wrapper range
704
- originalStateMap.forEach((originalState) => {
705
- const bytes = originalState.bytes;
706
- if (Util.isList(bytes)) {
707
- return;
708
- }
709
-
710
- const { decodedMappings } = originalState;
711
- const len = decodedMappings.length;
712
- if (len < 2) {
713
- return;
714
- }
715
- // sort by original line/column
716
- const startMapping = decodedMappings[0];
717
- const endMapping = decodedMappings[len - 1];
718
- // console.log(startMapping, endMapping);
719
- const startOffset = startMapping.generatedOffset;
720
-
721
- if (!endMapping.generatedEndOffset) {
722
- // line last one
723
- const line = state.locator.getLine(endMapping.generatedLine + 1);
724
- // last column
725
- const toEndLength = line.length - endMapping.generatedColumn;
726
- endMapping.generatedEndOffset = endMapping.generatedOffset + toEndLength;
727
- }
728
- const endOffset = endMapping.generatedEndOffset;
729
-
730
- // console.log('===========================================================', originalState.sourcePath);
731
- // console.log(originalState.source.length, endMapping);
732
-
733
- checkList.push({
734
- originalState,
735
- startOffset,
736
- endOffset
737
- });
738
-
739
- });
740
-
741
- if (!checkList.length) {
742
- return;
743
- }
744
-
745
- // there is no state.bytes if not in debug
746
- // should using coverageList to generate bytes first
747
- handleBytesCoverage(state);
748
-
749
- checkList.forEach((it) => {
750
-
751
- const {
752
- originalState, startOffset, endOffset
753
- } = it;
754
-
755
- // console.log('=============', 'no bytes', originalState.sourcePath);
756
-
757
- // only check uncovered range
758
- // because a uncovered range could be in a covered wrapper
759
- // { start: 0, end: 12137, count: 1 }, could be { start: > 0, end: < 12137, count: 0 }
760
- for (const range of state.bytes) {
761
- if (range.count > 0) {
762
- continue;
763
- }
764
- if (startOffset >= range.start && endOffset <= range.end) {
765
- // console.log('------------', 'added');
766
- originalState.bytes.push({
767
- start: 0,
768
- end: originalState.source.length,
769
- count: 0
770
- });
771
- break;
772
- }
773
- }
774
-
775
- });
776
-
777
-
778
- };
779
-
780
- // ========================================================================================================
781
-
782
- const decodeSourceMappings = (state, originalDecodedMap) => {
783
-
784
- const generatedLocator = state.locator;
785
-
786
- const { sources, mappings } = state.sourceMap;
787
-
788
- const decodedList = decode(mappings);
789
-
790
- sources.forEach((source, i) => {
791
- originalDecodedMap.set(i, []);
792
- });
793
-
794
- const allDecodedMappings = [];
795
- decodedList.forEach((segments, generatedLine) => {
796
- let info = null;
797
- segments.forEach((segment) => {
798
-
799
- // const COLUMN = 0;
800
- // const SOURCES_INDEX = 1;
801
- // const SOURCE_LINE = 2;
802
- // const SOURCE_COLUMN = 3;
803
- // const NAMES_INDEX = 4;
804
- const [generatedColumn, sourceIndex, originalLine, originalColumn] = segment;
805
- // the segment length could be 1, 4 or 5
806
-
807
- if (typeof sourceIndex === 'undefined') {
808
- return;
809
- }
810
-
811
- const generatedOffset = generatedLocator.locationToOffset({
812
- // 1-base
813
- line: generatedLine + 1,
814
- column: generatedColumn
815
- });
816
-
817
- info = {
818
- generatedOffset,
819
- generatedLine,
820
- generatedColumn,
821
-
822
- sourceIndex,
823
- originalLine,
824
- originalColumn
825
- };
826
-
827
- allDecodedMappings.push(info);
828
-
829
- originalDecodedMap.get(sourceIndex).push(info);
830
-
831
- });
832
-
833
- });
834
-
835
- // defaults to sort by generated offset, not need sort
836
- // allDecodedMappings.sort((a, b) => {
837
- // return a.generatedOffset - b.generatedOffset;
838
- // });
839
-
840
- return allDecodedMappings;
841
- };
842
-
843
- const getOriginalDecodedMappings = (originalDecodedMap, sourceIndex, locator) => {
844
- // all mappings for the original file sorted
845
- const decodedMappings = originalDecodedMap.get(sourceIndex);
846
-
847
- if (!decodedMappings) {
848
- return [];
849
- }
850
-
851
- // sort by original line/column
852
- decodedMappings.sort((a, b) => {
853
- if (a.originalLine === b.originalLine) {
854
- return a.originalColumn - b.originalColumn;
855
- }
856
- return a.originalLine - b.originalLine;
857
- });
858
-
859
- // add offset and index
860
- decodedMappings.forEach((item, i) => {
861
- item.originalOffset = locator.locationToOffset({
862
- line: item.originalLine + 1,
863
- column: item.originalColumn
864
- });
865
- });
866
-
867
- return decodedMappings;
868
- };
869
-
870
- // ========================================================================================================
871
-
872
- const getSourceFilter = (options) => {
873
- const input = options.sourceFilter;
874
-
875
- // for function handler
876
- if (typeof input === 'function') {
877
- return input;
878
- }
879
-
880
- // for single minimatch pattern
881
- if (input && typeof input === 'string') {
882
- return (sourcePath) => {
883
- return minimatch(sourcePath, input);
884
- };
885
- }
886
-
887
- // for patterns
888
- if (input && typeof input === 'object') {
889
- const patterns = Object.keys(input);
890
- return (sourcePath) => {
891
- for (const pattern of patterns) {
892
- if (minimatch(sourcePath, pattern)) {
893
- return input[pattern];
894
- }
895
- }
896
- // false if not matched
897
- };
898
- }
899
-
900
- // default
901
- return () => true;
902
- };
903
-
904
- const initOriginalList = (state, originalDecodedMap, options) => {
905
-
906
- // source filter
907
- const sourceFilter = getSourceFilter(options);
908
-
909
- // create original content mappings
910
- const originalStateMap = new Map();
911
-
912
- const { sources, sourcesContent } = state.sourceMap;
913
-
914
- sources.forEach((sourcePath, sourceIndex) => {
915
-
916
- // filter
917
- if (!sourceFilter(sourcePath)) {
918
- // console.log('-', sourcePath);
919
- return;
920
- }
921
- // console.log(sourcePath);
922
-
923
- // console.log(`add source: ${k}`);
924
- const sourceContent = sourcesContent[sourceIndex];
925
- if (typeof sourceContent !== 'string') {
926
- Util.logError(`not found source content: ${sourcePath}`);
927
- return;
928
- }
929
-
930
- const locator = new Locator(sourceContent);
931
-
932
- const decodedMappings = getOriginalDecodedMappings(originalDecodedMap, sourceIndex, locator);
933
-
934
- // unpacked file always is js
935
-
936
- const type = getSourceType(sourcePath);
937
-
938
- const originalState = {
939
- original: true,
940
- // original file is js
941
- js: true,
942
- type,
943
- source: sourceContent,
944
- sourcePath,
945
- locator,
946
- decodedMappings,
947
- // coverage info
948
- bytes: [],
949
- functions: [],
950
- branches: [],
951
- statements: [],
952
- astInfo: {
953
- functions: [],
954
- branches: [],
955
- statements: []
956
- },
957
- // coverage data
958
- v8Data: {}
959
- };
960
-
961
- originalStateMap.set(sourceIndex, originalState);
962
- });
963
-
964
- return originalStateMap;
965
- };
966
-
967
- const collectOriginalList = (state, originalStateMap) => {
968
-
969
- const { fileUrls } = state;
970
- const distFile = state.sourcePath;
971
-
972
- // collect original files
973
- originalStateMap.forEach((originalState) => {
974
-
975
- const {
976
- js, type, sourcePath, source
977
- } = originalState;
978
-
979
- // add file item
980
- const url = fileUrls[sourcePath] || sourcePath;
981
-
982
- // add dist for id
983
- const id = Util.calculateSha1(sourcePath + source);
984
-
985
- const sourceItem = {
986
- url,
987
- id,
988
- js,
989
- type,
990
- sourcePath,
991
- distFile,
992
- source
993
- };
994
-
995
- // save v8 data and add to originalList
996
- originalState.v8Data = sourceItem;
997
- state.originalList.push(originalState);
998
- });
999
-
1000
- };
1001
-
1002
- // ========================================================================================================
1003
-
1004
- const generateCoverageForDist = (state) => {
1005
-
1006
- handleFunctionsCoverage(state);
1007
- handleBranchesCoverage(state);
1008
- handleStatementsCoverage(state);
1009
- handleBytesCoverage(state);
1010
-
1011
- };
1012
-
1013
- const unpackSourceMap = (state, options) => {
1014
-
1015
- const { sourceMap } = state;
1016
-
1017
- // keep original urls
1018
- const fileUrls = {};
1019
- const sourcePathHandler = getSourcePathHandler(options);
1020
- initSourceMapSourcePath(sourceMap, fileUrls, sourcePathHandler);
1021
- state.fileUrls = fileUrls;
1022
-
1023
- // ===============================================
1024
- // decode mappings for each original file
1025
-
1026
- const originalDecodedMap = new Map();
1027
- // for find-original-range
1028
- state.decodedMappings = decodeSourceMappings(state, originalDecodedMap);
1029
-
1030
- // filter original list and init list
1031
- const originalStateMap = initOriginalList(state, originalDecodedMap, options);
1032
-
1033
- originalDecodedMap.clear();
1034
-
1035
- // ===============================================
1036
-
1037
- // handle functions before handle original state functions
1038
- handleOriginalFunctionsCoverage(state, originalStateMap);
1039
- handleOriginalBranchesCoverage(state, originalStateMap);
1040
- handleOriginalStatementsCoverage(state, originalStateMap);
1041
-
1042
- // handle lines info before handle ranges to update line count
1043
- originalStateMap.forEach((originalState) => {
1044
-
1045
- handleFunctionsCoverage(originalState);
1046
- handleBranchesCoverage(originalState);
1047
- handleStatementsCoverage(originalState);
1048
-
1049
- // if (originalState.sourcePath.endsWith('demo.js')) {
1050
- // console.log('=================================', originalState.sourcePath);
1051
- // }
1052
-
1053
- });
1054
-
1055
- // handle bytes ranges
1056
- handleOriginalBytesCoverage(state, originalStateMap);
1057
-
1058
- // collect coverage for original list
1059
- collectOriginalList(state, originalStateMap);
1060
-
1061
- };
1062
-
1063
- const unpackDistFile = (item, state, options) => {
1064
-
1065
- if (state.sourceMap) {
1066
- if (Util.isDebug()) {
1067
- // js self
1068
- item.debug = true;
1069
- generateCoverageForDist(state);
1070
- } else {
1071
- item.dedupe = true;
1072
- }
1073
-
1074
- // unpack source map
1075
- unpackSourceMap(state, options);
1076
-
1077
- } else {
1078
-
1079
- // css/js self
1080
- generateCoverageForDist(state);
1081
-
1082
- }
1083
-
1084
- };
1085
-
1086
- // ========================================================================================================
1087
-
1088
- const filterCoverageList = (item) => {
1089
- const {
1090
- functions, scriptOffset, source
1091
- } = item;
1092
-
1093
- // no script offset
1094
- if (!scriptOffset) {
1095
- return functions;
1096
- }
1097
-
1098
- // vm script offset
1099
- const minOffset = scriptOffset;
1100
- // the inline sourcemap could be removed
1101
- const maxOffset = source.length;
1102
-
1103
- const rootFunctionInfo = {
1104
- root: true,
1105
- ranges: [{
1106
- startOffset: minOffset,
1107
- endOffset: maxOffset,
1108
- count: 1
1109
- }]
1110
- };
1111
-
1112
- const coverageList = functions.filter((block) => {
1113
- const { ranges } = block;
1114
-
1115
- // first one is function coverage info
1116
- const functionRange = ranges[0];
1117
- const { startOffset, endOffset } = functionRange;
1118
- if (startOffset >= minOffset && endOffset <= maxOffset) {
1119
- return true;
1120
- }
1121
-
1122
- // blocks
1123
- const len = ranges.length;
1124
- if (len > 1) {
1125
- for (let i = 1; i < len; i++) {
1126
- const range = ranges[i];
1127
- if (range.startOffset >= minOffset && range.endOffset <= maxOffset) {
1128
- rootFunctionInfo.ranges.push(range);
1129
- }
1130
- }
1131
- }
1132
-
1133
- return false;
1134
- });
1135
-
1136
- // first one for root function
1137
- if (rootFunctionInfo.ranges.length > 1) {
1138
- coverageList.unshift(rootFunctionInfo);
1139
- }
1140
-
1141
- return coverageList;
1142
- };
1143
-
1144
- // ========================================================================================================
1145
-
1146
- const isCoveredInRanges = (range, uncoveredBytes) => {
1147
- if (!uncoveredBytes.length) {
1148
- return true;
1149
- }
1150
- for (const item of uncoveredBytes) {
1151
- if (range.start >= item.start && range.end <= item.end) {
1152
- return false;
1153
- }
1154
- }
1155
- return true;
1156
- };
1157
-
1158
- const mergeV8Data = (state, stateList) => {
1159
- // console.log(stateList);
1160
-
1161
- // const sourcePath = state.sourcePath;
1162
- // console.log(sourcePath);
1163
- // if (sourcePath.endsWith('scroll_zoom.ts')) {
1164
- // console.log('merge v8 data ===================================', sourcePath);
1165
- // console.log(stateList.map((it) => it.bytes));
1166
- // console.log(stateList.map((it) => it.functions));
1167
- // console.log(stateList.map((it) => it.branches.map((b) => [`${b.start}-${b.end}`, JSON.stringify(b.locations.map((l) => l.count))])));
1168
- // }
1169
-
1170
- // ===========================================================
1171
- // bytes
1172
- const mergedBytes = [];
1173
- const uncoveredList = [];
1174
- stateList.forEach((st) => {
1175
- const bytes = dedupeCountRanges(st.bytes);
1176
- const uncoveredBytes = [];
1177
- bytes.forEach((range) => {
1178
- if (range.count) {
1179
- mergedBytes.push(range);
1180
- } else {
1181
- uncoveredBytes.push(range);
1182
- }
1183
- });
1184
- uncoveredList.push(uncoveredBytes);
1185
- });
1186
- // just remove uncovered range
1187
- uncoveredList.forEach((currentBytes) => {
1188
- currentBytes.forEach((range) => {
1189
- for (const targetBytes of uncoveredList) {
1190
- if (targetBytes === currentBytes) {
1191
- continue;
1192
- }
1193
- if (isCoveredInRanges(range, targetBytes)) {
1194
- return;
1195
- }
1196
- }
1197
- mergedBytes.push(range);
1198
- });
1199
- });
1200
-
1201
- // will be dedupeCountRanges in collectFileCoverage
1202
- state.bytes = mergedBytes;
1203
-
1204
- // ===========================================================
1205
- // functions
1206
- const allFunctions = stateList.map((it) => it.functions).flat();
1207
- const functionComparer = (lastRange, range) => {
1208
- // if (lastRange.start === range.start && lastRange.end === range.end) {
1209
- // return true;
1210
- // }
1211
-
1212
- // function range could be from sourcemap, not exact matched
1213
-
1214
- // end is same
1215
- // {start: 2017, end: 2315, count: 481}
1216
- // {start: 2018, end: 2315, count: 14}
1217
-
1218
- // start is same
1219
- // {start: 10204, end: 10379, count: 0}
1220
- // {start: 10204, end: 10393, count: 5}
1221
-
1222
- // only one position matched could be same
1223
- if (lastRange.start === range.start || lastRange.end === range.end) {
1224
- // console.log(lastRange.start, range.start, lastRange.end, range.end);
1225
-
1226
- // if (lastRange.start === range.start) {
1227
- // console.log(range.end - lastRange.end, lastRange.start, lastRange.end, 'end', range.end, state.sourcePath);
1228
- // } else {
1229
- // console.log(range.start - lastRange.start, lastRange.start, lastRange.end, 'start', range.start, state.sourcePath);
1230
- // }
1231
-
1232
- return true;
1233
- }
1234
-
1235
- return false;
1236
- };
1237
- const functionHandler = (lastRange, range) => {
1238
- lastRange.count += range.count;
1239
- };
1240
- const mergedFunctions = mergeRangesWith(allFunctions, functionComparer, functionHandler);
1241
- state.functions = mergedFunctions;
1242
-
1243
- // ===========================================================
1244
- // branches
1245
- const allBranches = stateList.map((it) => it.branches).flat();
1246
- const branchComparer = (lastRange, range) => {
1247
- // exact matched because the branch range is generated from ast
1248
- return lastRange.start === range.start && lastRange.end === range.end;
1249
- };
1250
- const branchHandler = (lastRange, range) => {
1251
- // merge locations count
1252
- lastRange.locations.forEach((item, i) => {
1253
- const loc = range.locations[i];
1254
- if (loc) {
1255
- item.count += loc.count;
1256
- }
1257
- });
1258
- };
1259
- const mergedBranches = mergeRangesWith(allBranches, branchComparer, branchHandler);
1260
- state.branches = mergedBranches;
1261
-
1262
- // if (sourcePath.endsWith('scroll_zoom.ts')) {
1263
- // console.log(mergedBytes);
1264
- // console.log(mergedFunctions);
1265
- // console.log(mergedBranches.map((b) => [`${b.start}-${b.end}`, JSON.stringify(b.locations.map((l) => l.count))]));
1266
- // }
1267
-
1268
-
1269
- };
1270
-
1271
- const generateV8DataList = (stateList, options) => {
1272
-
1273
- const stateMap = new Map();
1274
-
1275
- // all original files from dist
1276
- const allOriginalList = [];
1277
- stateList.forEach((state) => {
1278
- const { v8Data, originalList } = state;
1279
- // dedupe dist file if not debug
1280
- if (!v8Data.dedupe) {
1281
- stateMap.set(v8Data.id, state);
1282
- }
1283
- allOriginalList.push(originalList);
1284
- });
1285
-
1286
- // merge istanbul and v8(converted)
1287
- const mergeMap = new Map();
1288
- allOriginalList.flat().forEach((originalState) => {
1289
- const { v8Data } = originalState;
1290
- const id = v8Data.id;
1291
- // exists item
1292
- const prevState = stateMap.get(id);
1293
- if (prevState) {
1294
- // ignore empty item, just override it
1295
- if (!prevState.v8Data.empty) {
1296
- if (mergeMap.has(id)) {
1297
- mergeMap.get(id).push(originalState);
1298
- } else {
1299
- mergeMap.set(id, [prevState, originalState]);
1300
- }
1301
- return;
1302
- }
1303
- }
1304
- stateMap.set(id, originalState);
1305
- });
1306
-
1307
- const mergeIds = mergeMap.keys();
1308
- for (const id of mergeIds) {
1309
- const state = stateMap.get(id);
1310
- // for source the type could be ts, so just use js (boolean)
1311
- if (state.js) {
1312
- mergeV8Data(state, mergeMap.get(id));
1313
- } else {
1314
- // should no css here, css can not be in sources
1315
- }
1316
- }
1317
-
1318
- // new v8 data list (includes sources)
1319
- const v8DataList = [];
1320
- // global file sources and istanbul coverage data
1321
- const fileSources = {};
1322
- const coverageData = {};
1323
- stateMap.forEach((state) => {
1324
- const { v8Data } = state;
1325
- const istanbulData = collectFileCoverage(v8Data, state, options);
1326
- const { sourcePath, source } = v8Data;
1327
- v8DataList.push(v8Data);
1328
- fileSources[sourcePath] = source;
1329
- coverageData[sourcePath] = istanbulData;
1330
- });
1331
-
1332
- return {
1333
- v8DataList,
1334
- fileSources,
1335
- coverageData
1336
- };
1337
- };
1338
-
1339
- const convertV8List = (v8list, options) => {
1340
-
1341
- const stateList = [];
1342
-
1343
- for (const item of v8list) {
1344
- // console.log([item.id]);
1345
-
1346
- const {
1347
- type, source, sourcePath
1348
- } = item;
1349
-
1350
- // for source file, type could be ts or vue as extname, but js = true
1351
- const js = type === 'js';
1352
- item.js = js;
1353
-
1354
- // source mapping
1355
- const locator = new Locator(source);
1356
-
1357
- // ============================
1358
- // move sourceMap
1359
- const sourceMap = item.sourceMap;
1360
- if (sourceMap) {
1361
- delete item.sourceMap;
1362
- }
1363
-
1364
- // ============================
1365
- const time_start_ast = Date.now();
1366
- // move functions and ranges to coverageList
1367
- let coverageList = [];
1368
- let astInfo;
1369
- if (js) {
1370
- coverageList = filterCoverageList(item);
1371
- // remove original functions
1372
- if (!Util.isDebug()) {
1373
- delete item.functions;
1374
- }
1375
- astInfo = getJsAstInfo(item, coverageList);
1376
- } else {
1377
- // convent css covered ranges to rules ranges and include uncovered ranges
1378
- astInfo = getCssAstInfo(item, coverageList);
1379
- // remove original ranges
1380
- if (!Util.isDebug()) {
1381
- delete item.ranges;
1382
- }
1383
- }
1384
- Util.logTime(`- parsed ast: ${sourcePath}`, time_start_ast);
1385
-
1386
- // console.log(sourcePath, astInfo.statements.length);
1387
- // ============================
1388
-
1389
- // current file and it's sources from sourceMap
1390
- // see const originalState
1391
- const state = {
1392
- js,
1393
- type,
1394
- source,
1395
- sourcePath,
1396
- sourceMap,
1397
- locator,
1398
- decodedMappings: [],
1399
- // coverage info
1400
- bytes: [],
1401
- functions: [],
1402
- branches: [],
1403
- statements: [],
1404
- astInfo,
1405
- // for sub source files
1406
- coverageList,
1407
- originalList: [],
1408
- // coverage data
1409
- v8Data: item
1410
- };
1411
-
1412
- const time_start_unpack = Date.now();
1413
- unpackDistFile(item, state, options);
1414
- Util.logTime(`- unpacked dist: ${sourcePath}`, time_start_unpack);
1415
-
1416
- stateList.push(state);
1417
-
1418
- }
1419
-
1420
- const time_start_convert = Date.now();
1421
- const dataList = generateV8DataList(stateList, options);
1422
- Util.logTime('- generated v8 data list', time_start_convert);
1423
-
1424
- return dataList;
1425
- };
1426
-
1427
- module.exports = {
1428
- convertV8List
1429
- };
1
+ /**
2
+ * V8 Coverage Data Converter
3
+ * @copyright https://github.com/cenfun/monocart-coverage-reports
4
+ * @author cenfun@gmail.com
5
+ */
6
+ const Util = require('../utils/util.js');
7
+
8
+ // position mapping for conversion between offset and line/column
9
+ const { Locator } = require('monocart-formatter');
10
+ const findOriginalRange = require('./find-original-range.js');
11
+ const { getJsAstInfo, getCssAstInfo } = require('./ast.js');
12
+ const { getIgnoredRanges } = require('./ignore.js');
13
+
14
+ const {
15
+ sortRanges, dedupeCountRanges, mergeRangesWith
16
+ } = require('../utils/dedupe.js');
17
+ const {
18
+ getSourceType, getSourcePathHandler, initSourceMapSourcePath
19
+ } = require('../utils/source-path.js');
20
+ const { decode, minimatch } = require('../packages/monocart-coverage-vendor.js');
21
+
22
+ const InfoBranch = require('./info-branch.js');
23
+ const InfoFunction = require('./info-function.js');
24
+ const InfoStatement = require('./info-statement.js');
25
+
26
+ // ========================================================================================================
27
+
28
+ // debug info
29
+ const logMappingErrors = (type, result) => {
30
+
31
+ // const {
32
+ // errors, start, end, sourcePath
33
+ // } = result;
34
+ // // if (!sourcePath.endsWith('monocart-v8.js') || type !== 'byte') {
35
+ // // return;
36
+ // // }
37
+ // const list = [
38
+ // Util.EC.red('[Mapping]'),
39
+ // Util.EC.magenta(type),
40
+ // errors.join(' -> '),
41
+ // Util.EC.blue(`${start} ~ ${end}`),
42
+ // sourcePath
43
+ // ];
44
+ // console.log(list.join(' '));
45
+
46
+ };
47
+
48
+ // ========================================================================================================
49
+
50
+ const handleIgnoredRanges = (list, ignoredRanges) => {
51
+ list.forEach((item) => {
52
+ const range = Util.findInRanges(item.start, item.end, ignoredRanges, 'start', 'end');
53
+ if (range) {
54
+ // console.log(item, range);
55
+ item.ignored = true;
56
+ }
57
+ });
58
+ };
59
+
60
+
61
+ const applyBytesToLines = (bytes, locator, lineMap) => {
62
+ bytes.forEach((range) => {
63
+ const {
64
+ start, end, count, ignored
65
+ } = range;
66
+
67
+ // no need handle ignored and uncovered byte
68
+ if (count === 0 && ignored) {
69
+ return;
70
+ }
71
+
72
+ const sLoc = locator.offsetToLocation(start);
73
+ const eLoc = locator.offsetToLocation(end);
74
+
75
+ // update lines coverage
76
+ const lines = Util.getRangeLines(sLoc, eLoc);
77
+
78
+ lines.forEach((it) => {
79
+ const lineItem = lineMap.get(it.line);
80
+ if (!lineItem) {
81
+ // not found line, could be comment or blank line
82
+ return;
83
+ }
84
+ if (lineItem.ignored) {
85
+ return;
86
+ }
87
+
88
+ it.count = count;
89
+
90
+ // default is covered, so only focus on
91
+ // 1, biggest covered count
92
+ // 2, uncovered entire and pieces
93
+ if (count > 0) {
94
+ lineItem.coveredCount = Math.max(lineItem.coveredCount, count);
95
+ } else {
96
+ if (it.entire) {
97
+ lineItem.uncoveredEntire = it;
98
+ } else {
99
+ lineItem.uncoveredPieces.push(it);
100
+ }
101
+ }
102
+ });
103
+
104
+ });
105
+
106
+ // no ignore items
107
+ lineMap.forEach((lineItem, line) => {
108
+
109
+ const {
110
+ uncoveredEntire, uncoveredPieces, coveredCount
111
+ } = lineItem;
112
+
113
+ // default count to 1, both js and css
114
+ let count = 1;
115
+ // full covered true/false for entire line
116
+ let covered = true;
117
+
118
+ if (uncoveredEntire) {
119
+ count = 0;
120
+ covered = false;
121
+ } else {
122
+ count = coveredCount;
123
+ const uncoveredLen = uncoveredPieces.length;
124
+ if (uncoveredLen > 0) {
125
+ covered = false;
126
+ // uncovered
127
+ count = `1/${uncoveredLen + 1}`;
128
+ }
129
+ }
130
+
131
+ lineItem.covered = covered;
132
+ lineItem.count = count;
133
+
134
+ });
135
+
136
+ };
137
+
138
+ const handleLinesCoverage = (bytes, locator, ignoredRanges) => {
139
+
140
+ // init lines
141
+ let blankCount = 0;
142
+ let commentCount = 0;
143
+ // line 1 based
144
+ const lineMap = new Map();
145
+ locator.lines.forEach((lineItem) => {
146
+ // exclude blank and comment
147
+ if (lineItem.blank) {
148
+ blankCount += 1;
149
+ return;
150
+ }
151
+ if (lineItem.comment) {
152
+ commentCount += 1;
153
+ return;
154
+ }
155
+ const ignored = Util.findInRanges(lineItem.start, lineItem.end, ignoredRanges);
156
+ if (ignored) {
157
+ return;
158
+ }
159
+ // line 1-base
160
+ const line = lineItem.line + 1;
161
+ lineItem.coveredCount = 1;
162
+ lineItem.uncoveredEntire = null;
163
+ lineItem.uncoveredPieces = [];
164
+ lineMap.set(line, lineItem);
165
+ });
166
+
167
+ applyBytesToLines(bytes, locator, lineMap);
168
+
169
+ const summaryLines = {
170
+ total: 0,
171
+ covered: 0,
172
+ blank: blankCount,
173
+ comment: commentCount
174
+ };
175
+ // data lines for codecov
176
+ // https://docs.codecov.com/docs/codecov-custom-coverage-format
177
+ const dataLines = {};
178
+
179
+ // no ignore items
180
+ lineMap.forEach((lineItem, line) => {
181
+ const { count, covered } = lineItem;
182
+ // data lines
183
+ dataLines[line] = count;
184
+
185
+ summaryLines.total += 1;
186
+ if (covered) {
187
+ summaryLines.covered += 1;
188
+ }
189
+ });
190
+
191
+ return {
192
+ dataLines,
193
+ summaryLines
194
+ };
195
+ };
196
+
197
+ // ========================================================================================================
198
+
199
+ const calculateV8Summary = (list) => {
200
+
201
+ const summary = {
202
+ total: 0,
203
+ covered: 0
204
+ };
205
+
206
+ list.forEach((item) => {
207
+ if (item.ignored) {
208
+ return;
209
+ }
210
+
211
+ summary.total += 1;
212
+ if (item.count > 0) {
213
+ summary.covered += 1;
214
+ }
215
+ });
216
+
217
+ return summary;
218
+ };
219
+
220
+ // ========================================================================================================
221
+ // istanbul coverage format
222
+ // https://github.com/istanbuljs/istanbuljs/blob/master/docs/raw-output.md
223
+ /**
224
+ * * `path` - the file path for which coverage is being tracked
225
+ * * `statementMap` - map of statement locations keyed by statement index
226
+ * * `fnMap` - map of function metadata keyed by function index
227
+ * * `branchMap` - map of branch metadata keyed by branch index
228
+ * * `s` - hit counts for statements
229
+ * * `f` - hit count for functions
230
+ * * `b` - hit count for branches
231
+ */
232
+ const collectFileCoverage = (v8Data, state, options) => {
233
+
234
+ const {
235
+ bytes,
236
+ functions,
237
+ branches,
238
+ statements,
239
+
240
+ sourcePath,
241
+ locator
242
+ } = state;
243
+
244
+ // ==========================================
245
+ // v8 data
246
+ const data = {
247
+ bytes: dedupeCountRanges(bytes),
248
+ functions: [],
249
+ branches: [],
250
+ statements: []
251
+ };
252
+
253
+ // ==========================================
254
+ // ignore
255
+ const ignoredRanges = getIgnoredRanges(locator, options);
256
+ if (ignoredRanges) {
257
+
258
+ data.ignores = ignoredRanges;
259
+
260
+ // data bytes is start/end/count object
261
+ handleIgnoredRanges(data.bytes, ignoredRanges);
262
+
263
+ // functions start/end/count instance
264
+ handleIgnoredRanges(functions, ignoredRanges);
265
+
266
+ // branches start/end/count instance
267
+ handleIgnoredRanges(branches, ignoredRanges);
268
+
269
+ // branch locations start/end/count object
270
+ branches.forEach((group) => {
271
+ if (group.ignored) {
272
+ // all branch group ignored
273
+ group.locations.forEach((it) => {
274
+ it.ignored = true;
275
+ });
276
+ } else {
277
+ handleIgnoredRanges(group.locations, ignoredRanges);
278
+ }
279
+ });
280
+
281
+ // statements start/end/count instance
282
+ handleIgnoredRanges(statements, ignoredRanges);
283
+
284
+ // console.log(ignoredRanges);
285
+
286
+ }
287
+
288
+ // ==========================================
289
+ // lines
290
+ // after bytes with ignored, before calculateV8Lines
291
+ const { dataLines, summaryLines } = handleLinesCoverage(data.bytes, locator, ignoredRanges);
292
+
293
+ data.lines = dataLines;
294
+
295
+ // console.log('statements', state.sourcePath, statements.length);
296
+
297
+ // ==========================================
298
+
299
+ data.functions = functions.map((info) => {
300
+ return info.getRange();
301
+ });
302
+ sortRanges(data.functions);
303
+
304
+ // branch group with locations to flat branches
305
+ data.branches = branches.map((info) => {
306
+ return info.getRanges();
307
+ }).flat();
308
+ sortRanges(data.branches);
309
+
310
+ data.statements = statements.map((info) => {
311
+ return info.getRange();
312
+ });
313
+ sortRanges(data.statements);
314
+
315
+ // ==========================================
316
+
317
+ const summary = {
318
+ functions: calculateV8Summary(data.functions),
319
+ branches: calculateV8Summary(data.branches),
320
+ statements: calculateV8Summary(data.statements),
321
+ lines: summaryLines
322
+ };
323
+
324
+ // ==========================================
325
+ // v8 data and summary
326
+ v8Data.data = data;
327
+ v8Data.summary = summary;
328
+
329
+ // ==========================================
330
+ // istanbul
331
+ const istanbulData = {
332
+ path: sourcePath,
333
+
334
+ statementMap: {},
335
+ fnMap: {},
336
+ branchMap: {},
337
+
338
+ s: {},
339
+ f: {},
340
+ b: {}
341
+ };
342
+
343
+ statements.filter((it) => !it.ignored).forEach((statement, index) => {
344
+ istanbulData.statementMap[`${index}`] = statement.generate(locator);
345
+ istanbulData.s[`${index}`] = statement.count;
346
+ });
347
+
348
+ functions.filter((it) => !it.ignored).forEach((fn, index) => {
349
+ istanbulData.fnMap[`${index}`] = fn.generate(locator, index);
350
+ istanbulData.f[`${index}`] = fn.count;
351
+ });
352
+
353
+ branches.filter((it) => !it.ignored).forEach((branch, index) => {
354
+ const { map, counts } = branch.generate(locator);
355
+ istanbulData.branchMap[`${index}`] = map;
356
+ istanbulData.b[`${index}`] = counts;
357
+ });
358
+
359
+ return istanbulData;
360
+
361
+ };
362
+
363
+ // ========================================================================================================
364
+
365
+ const addJsBytesCoverage = (state, range) => {
366
+ const {
367
+ startOffset, endOffset, count
368
+ } = range;
369
+ // add bytes range
370
+ const byte = {
371
+ start: startOffset,
372
+ end: endOffset,
373
+ count
374
+ };
375
+ // for debug
376
+ if (Util.isDebug() && state.original) {
377
+ byte.generatedStart = range.generatedStart;
378
+ byte.generatedEnd = range.generatedEnd;
379
+ }
380
+ state.bytes.push(byte);
381
+ };
382
+
383
+ const addCssBytesCoverage = (state, range) => {
384
+ const {
385
+ start, end, count
386
+ } = range;
387
+ // add css bytes range, already start, end
388
+ state.bytes.push({
389
+ start,
390
+ end,
391
+ count
392
+ });
393
+ };
394
+
395
+ // ========================================================================================================
396
+
397
+ const checkOriginalRangeCode = (range, state, startEndMap, type) => {
398
+
399
+ // only for original
400
+ if (!state.original) {
401
+ return true;
402
+ }
403
+
404
+ const { start, end } = range;
405
+ if (start >= end) {
406
+ // console.log('invalid branch', state.sourcePath, range);
407
+ return false;
408
+ }
409
+
410
+ // could be vue code
411
+ const text = state.locator.getSlice(start, end).trim();
412
+ if (!text) {
413
+ return false;
414
+ }
415
+
416
+ // invalid original code
417
+ // Matches any character that is not a word character from the basic Latin alphabet.
418
+ // Equivalent to [^A-Za-z0-9_]
419
+ if (text.length === 1 && (/\W/).test(text)) {
420
+ // ; } =
421
+ // console.log(text, type, state.sourcePath, range);
422
+ return false;
423
+ }
424
+
425
+ // type: bytes, functions, branches, statements
426
+
427
+ // do not check repeated range for `bytes`
428
+ // instead, use `dedupeCountRanges` to accumulate count
429
+ if (type === 'bytes') {
430
+ return true;
431
+ }
432
+
433
+ // check repeated range
434
+ const key = `${start}_${end}`;
435
+ if (startEndMap.has(key)) {
436
+ return false;
437
+ }
438
+ startEndMap.set(key, range);
439
+
440
+ return true;
441
+ };
442
+
443
+ const handleFunctionsCoverage = (state) => {
444
+
445
+ // functions only for js
446
+ if (!state.js) {
447
+ return;
448
+ }
449
+
450
+ const startEndMap = new Map();
451
+ const { functions, astInfo } = state;
452
+ astInfo.functions.forEach((it) => {
453
+
454
+ if (!checkOriginalRangeCode(it, state, startEndMap, 'functions')) {
455
+ return;
456
+ }
457
+
458
+ functions.push(new InfoFunction(it, state.original));
459
+ });
460
+
461
+ };
462
+
463
+ const handleBranchesCoverage = (state) => {
464
+
465
+ // functions only for js
466
+ if (!state.js) {
467
+ return;
468
+ }
469
+
470
+ const startEndMap = new Map();
471
+ const { branches, astInfo } = state;
472
+ astInfo.branches.forEach((it) => {
473
+
474
+ if (!checkOriginalRangeCode(it, state, startEndMap, 'branches')) {
475
+ return;
476
+ }
477
+
478
+ branches.push(new InfoBranch(it, state.original));
479
+ });
480
+
481
+ };
482
+
483
+ const handleStatementsCoverage = (state) => {
484
+
485
+ // statement only for js
486
+ if (!state.js) {
487
+ return;
488
+ }
489
+
490
+ const startEndMap = new Map();
491
+ const { statements, astInfo } = state;
492
+ astInfo.statements.forEach((it) => {
493
+
494
+ if (!checkOriginalRangeCode(it, state, startEndMap, 'statements')) {
495
+ return;
496
+ }
497
+
498
+ statements.push(new InfoStatement(it, state.original));
499
+ });
500
+
501
+ };
502
+
503
+ const handleOriginalBytesCoverage = (state) => {
504
+ const startEndMap = new Map();
505
+ state.bytes = state.bytes.filter((it) => {
506
+ return checkOriginalRangeCode(it, state, startEndMap, 'bytes');
507
+ });
508
+ };
509
+
510
+ const handleGeneratedBytesCoverage = (state) => {
511
+
512
+ const { js, coverageList } = state;
513
+
514
+ if (js) {
515
+ coverageList.forEach((block) => {
516
+ block.ranges.forEach((range) => {
517
+ addJsBytesCoverage(state, range);
518
+ });
519
+ });
520
+ } else {
521
+ coverageList.forEach((range) => {
522
+ addCssBytesCoverage(state, range);
523
+ });
524
+ }
525
+
526
+ };
527
+
528
+ // ========================================================================================================
529
+
530
+ const handleOriginalFunctionsCoverage = (state, originalStateMap) => {
531
+
532
+ // functions only for js
533
+ if (!state.js) {
534
+ return;
535
+ }
536
+
537
+ // console.log(state.astInfo.functions);
538
+
539
+ const updateFunctionBodyRange = (start, end, bodyStart, bodyEnd, originalFunction) => {
540
+ if (bodyStart !== start || bodyEnd !== end) {
541
+ const result = findOriginalRange(bodyStart, bodyEnd, state, originalStateMap);
542
+ if (result.error) {
543
+ logMappingErrors('function body', result);
544
+ } else {
545
+ originalFunction.bodyStart = result.start;
546
+ originalFunction.bodyEnd = result.end;
547
+ }
548
+ }
549
+ };
550
+
551
+ // function count
552
+ state.astInfo.functions.forEach((it) => {
553
+ const {
554
+ start, end, bodyStart, bodyEnd, wrap
555
+ } = it;
556
+
557
+ // remove webpack wrap functions for functions count, not for ranges here
558
+ if (wrap) {
559
+ return;
560
+ }
561
+
562
+ const result = findOriginalRange(start, end, state, originalStateMap);
563
+ if (result.error) {
564
+ logMappingErrors('function', result);
565
+ return;
566
+ }
567
+
568
+ const originalFunction = {
569
+ ... it,
570
+ generatedStart: start,
571
+ generatedEnd: end,
572
+ start: result.start,
573
+ end: result.end,
574
+ bodyStart: result.start,
575
+ bodyEnd: result.end
576
+ };
577
+
578
+ // body start and end
579
+ updateFunctionBodyRange(start, end, bodyStart, bodyEnd, originalFunction);
580
+
581
+ // add back to original ast
582
+ result.originalState.astInfo.functions.push(originalFunction);
583
+
584
+ });
585
+
586
+ };
587
+
588
+ const handleOriginalBranchesCoverage = (state, originalStateMap) => {
589
+
590
+ // branches only for js
591
+ if (!state.js) {
592
+ return;
593
+ }
594
+
595
+ // console.log(state.astInfo.branches);
596
+
597
+ // function count
598
+ state.astInfo.branches.forEach((group) => {
599
+
600
+ const { type, locations } = group;
601
+
602
+ // start
603
+ const result = findOriginalRange(group.start, group.end, state, originalStateMap);
604
+ if (result.error) {
605
+ logMappingErrors('branch group', result);
606
+ return;
607
+ }
608
+
609
+ // new group start and end
610
+ const groupStart = result.start;
611
+ const groupEnd = result.end;
612
+
613
+ let hasError;
614
+ const newLocations = locations.map((oLoc) => {
615
+
616
+ const newLoc = {
617
+ ... oLoc
618
+ };
619
+
620
+ if (newLoc.none) {
621
+ newLoc.start = groupStart;
622
+ newLoc.end = groupEnd;
623
+ return newLoc;
624
+ }
625
+
626
+ const locResult = findOriginalRange(newLoc.start, newLoc.end, state, originalStateMap);
627
+ if (locResult.error) {
628
+ // It should not happen unless it is minify files, the SourceMap has some order problems
629
+ logMappingErrors('branch', locResult);
630
+ hasError = true;
631
+ return newLoc;
632
+ }
633
+
634
+ // before new range
635
+ newLoc.generatedStart = newLoc.start;
636
+ newLoc.generatedEnd = newLoc.end;
637
+ // mapping to new range
638
+ newLoc.start = locResult.start;
639
+ newLoc.end = locResult.end;
640
+
641
+ return newLoc;
642
+ });
643
+
644
+ // ignored group when found error
645
+ if (hasError) {
646
+ return;
647
+ }
648
+
649
+ // add back to original ast
650
+ result.originalState.astInfo.branches.push({
651
+ type,
652
+ start: groupStart,
653
+ end: groupEnd,
654
+ locations: newLocations
655
+ });
656
+
657
+ });
658
+
659
+ };
660
+
661
+ const handleOriginalStatementsCoverage = (state, originalStateMap) => {
662
+
663
+ // statements only for js
664
+ if (!state.js) {
665
+ return;
666
+ }
667
+
668
+ // statement count
669
+ state.astInfo.statements.forEach((it) => {
670
+ const { start, end } = it;
671
+
672
+ const result = findOriginalRange(start, end, state, originalStateMap);
673
+ if (result.error) {
674
+ logMappingErrors('statement', result);
675
+ return;
676
+ }
677
+
678
+ // add back to original ast
679
+ result.originalState.astInfo.statements.push({
680
+ ... it,
681
+ generatedStart: start,
682
+ generatedEnd: end,
683
+ start: result.start,
684
+ end: result.end
685
+ });
686
+
687
+ });
688
+
689
+ originalStateMap.forEach((originalState) => {
690
+ const statements = originalState.astInfo.statements;
691
+ if (Util.isList(statements)) {
692
+ return;
693
+ }
694
+ // fake source nothing matched
695
+ statements.push({
696
+ start: 0,
697
+ end: originalState.source.length,
698
+ count: 1
699
+ });
700
+
701
+ });
702
+
703
+ };
704
+
705
+ // ========================================================================================================
706
+
707
+ const handleOriginalEmptyBytesCoverage = (state, originalStateMap) => {
708
+ const checkList = [];
709
+
710
+ // check original bytes if fully in a wrapper range
711
+ originalStateMap.forEach((originalState) => {
712
+ const bytes = originalState.bytes;
713
+ if (Util.isList(bytes)) {
714
+ return;
715
+ }
716
+
717
+ const { decodedMappings } = originalState;
718
+ const len = decodedMappings.length;
719
+ if (len < 2) {
720
+ return;
721
+ }
722
+ // sort by original line/column
723
+ const startMapping = decodedMappings[0];
724
+ const endMapping = decodedMappings[len - 1];
725
+ // console.log(startMapping, endMapping);
726
+ const startOffset = startMapping.generatedOffset;
727
+
728
+ if (!endMapping.generatedEndOffset) {
729
+ // line last one
730
+ const line = state.locator.getLine(endMapping.generatedLine + 1);
731
+ // could be no line found
732
+ if (line) {
733
+ // last column
734
+ endMapping.generatedEndOffset = line.end;
735
+ } else {
736
+ endMapping.generatedEndOffset = endMapping.generatedOffset;
737
+ }
738
+ }
739
+ const endOffset = endMapping.generatedEndOffset;
740
+
741
+ // console.log('===========================================================', originalState.sourcePath);
742
+ // console.log(originalState.source.length, endMapping);
743
+
744
+ checkList.push({
745
+ originalState,
746
+ startOffset,
747
+ endOffset
748
+ });
749
+
750
+ });
751
+
752
+ // no file to handle
753
+ if (!checkList.length) {
754
+ return;
755
+ }
756
+
757
+ // there is no state.bytes if not in debug
758
+ // should using coverageList to generate bytes first
759
+ handleGeneratedBytesCoverage(state);
760
+
761
+ checkList.forEach((it) => {
762
+
763
+ const {
764
+ originalState, startOffset, endOffset
765
+ } = it;
766
+
767
+ // console.log('=============', 'no bytes', originalState.sourcePath);
768
+
769
+ // only check uncovered range
770
+ // because a uncovered range could be in a covered wrapper
771
+ // { start: 0, end: 12137, count: 1 }, could be { start: > 0, end: < 12137, count: 0 }
772
+ for (const range of state.bytes) {
773
+ if (range.count > 0) {
774
+ continue;
775
+ }
776
+ if (startOffset >= range.start && endOffset <= range.end) {
777
+ // console.log('------------', 'added');
778
+ originalState.bytes.push({
779
+ start: 0,
780
+ end: originalState.source.length,
781
+ count: 0
782
+ });
783
+ break;
784
+ }
785
+ }
786
+
787
+ });
788
+ };
789
+
790
+ const handleAllOriginalBytesCoverage = (state, originalStateMap) => {
791
+
792
+ const { js, coverageList } = state;
793
+
794
+ // only for js, no sourcemap for css for now
795
+ if (!js) {
796
+ return;
797
+ }
798
+
799
+ // v8 coverage
800
+ coverageList.forEach((block) => {
801
+ block.ranges.forEach((range, index) => {
802
+
803
+ // remove wrap functions for original files
804
+ if (range.wrap) {
805
+ // console.log(range);
806
+ return;
807
+ }
808
+
809
+ const {
810
+ startOffset, endOffset, count
811
+ } = range;
812
+
813
+ const result = findOriginalRange(startOffset, endOffset, state, originalStateMap);
814
+ if (result.error) {
815
+ logMappingErrors('byte', result);
816
+ return;
817
+ }
818
+
819
+ addJsBytesCoverage(result.originalState, {
820
+ generatedStart: startOffset,
821
+ generatedEnd: endOffset,
822
+ startOffset: result.start,
823
+ endOffset: result.end,
824
+ count
825
+ });
826
+
827
+ });
828
+ });
829
+
830
+ handleOriginalEmptyBytesCoverage(state, originalStateMap);
831
+
832
+ };
833
+
834
+ // ========================================================================================================
835
+
836
+ const decodeSourceMappings = (state, originalDecodedMap) => {
837
+
838
+ const generatedLocator = state.locator;
839
+
840
+ const { sources, mappings } = state.sourceMap;
841
+
842
+ const decodedList = decode(mappings);
843
+
844
+ sources.forEach((source, i) => {
845
+ originalDecodedMap.set(i, []);
846
+ });
847
+
848
+ const allDecodedMappings = [];
849
+ decodedList.forEach((segments, generatedLine) => {
850
+ let info = null;
851
+ segments.forEach((segment) => {
852
+
853
+ // const COLUMN = 0;
854
+ // const SOURCES_INDEX = 1;
855
+ // const SOURCE_LINE = 2;
856
+ // const SOURCE_COLUMN = 3;
857
+ // const NAMES_INDEX = 4;
858
+ const [generatedColumn, sourceIndex, originalLine, originalColumn] = segment;
859
+ // the segment length could be 1, 4 or 5
860
+
861
+ if (typeof sourceIndex === 'undefined') {
862
+ return;
863
+ }
864
+
865
+ const generatedOffset = generatedLocator.locationToOffset({
866
+ // 1-base
867
+ line: generatedLine + 1,
868
+ column: generatedColumn
869
+ });
870
+
871
+ info = {
872
+ generatedOffset,
873
+ generatedLine,
874
+ generatedColumn,
875
+
876
+ sourceIndex,
877
+ originalLine,
878
+ originalColumn
879
+ };
880
+
881
+ allDecodedMappings.push(info);
882
+
883
+ originalDecodedMap.get(sourceIndex).push(info);
884
+
885
+ });
886
+
887
+ });
888
+
889
+ // defaults to sort by generated offset, not need sort
890
+ // allDecodedMappings.sort((a, b) => {
891
+ // return a.generatedOffset - b.generatedOffset;
892
+ // });
893
+
894
+ return allDecodedMappings;
895
+ };
896
+
897
+ const getOriginalDecodedMappings = (originalDecodedMap, sourceIndex, locator) => {
898
+ // all mappings for the original file sorted
899
+ const decodedMappings = originalDecodedMap.get(sourceIndex);
900
+
901
+ if (!decodedMappings) {
902
+ return [];
903
+ }
904
+
905
+ // sort by original line/column
906
+ decodedMappings.sort((a, b) => {
907
+ if (a.originalLine === b.originalLine) {
908
+ return a.originalColumn - b.originalColumn;
909
+ }
910
+ return a.originalLine - b.originalLine;
911
+ });
912
+
913
+ // add offset and index
914
+ decodedMappings.forEach((item, i) => {
915
+ item.originalOffset = locator.locationToOffset({
916
+ line: item.originalLine + 1,
917
+ column: item.originalColumn
918
+ });
919
+ });
920
+
921
+ return decodedMappings;
922
+ };
923
+
924
+ // ========================================================================================================
925
+
926
+ const getSourceFilter = (options) => {
927
+ const input = options.sourceFilter;
928
+
929
+ // for function handler
930
+ if (typeof input === 'function') {
931
+ return input;
932
+ }
933
+
934
+ // for single minimatch pattern
935
+ if (input && typeof input === 'string') {
936
+ return (sourcePath) => {
937
+ return minimatch(sourcePath, input);
938
+ };
939
+ }
940
+
941
+ // for patterns
942
+ if (input && typeof input === 'object') {
943
+ const patterns = Object.keys(input);
944
+ return (sourcePath) => {
945
+ for (const pattern of patterns) {
946
+ if (minimatch(sourcePath, pattern)) {
947
+ return input[pattern];
948
+ }
949
+ }
950
+ // false if not matched
951
+ };
952
+ }
953
+
954
+ // default
955
+ return () => true;
956
+ };
957
+
958
+ const initOriginalList = (state, originalDecodedMap, options) => {
959
+
960
+ // source filter
961
+ const sourceFilter = getSourceFilter(options);
962
+
963
+ // create original content mappings
964
+ const originalStateMap = new Map();
965
+
966
+ const { sources, sourcesContent } = state.sourceMap;
967
+
968
+ sources.forEach((sourcePath, sourceIndex) => {
969
+
970
+ // filter
971
+ if (!sourceFilter(sourcePath)) {
972
+ // console.log('-', sourcePath);
973
+ return;
974
+ }
975
+ // console.log(sourcePath);
976
+
977
+ // console.log(`add source: ${k}`);
978
+ const sourceContent = sourcesContent[sourceIndex];
979
+ if (typeof sourceContent !== 'string') {
980
+ Util.logError(`not found source content: ${sourcePath}`);
981
+ return;
982
+ }
983
+
984
+ const locator = new Locator(sourceContent);
985
+
986
+ const decodedMappings = getOriginalDecodedMappings(originalDecodedMap, sourceIndex, locator);
987
+
988
+ // unpacked file always is js
989
+
990
+ const type = getSourceType(sourcePath);
991
+
992
+ const originalState = {
993
+ original: true,
994
+ // original file is js
995
+ js: true,
996
+ type,
997
+ source: sourceContent,
998
+ sourcePath,
999
+ locator,
1000
+ decodedMappings,
1001
+ // coverage info
1002
+ bytes: [],
1003
+ functions: [],
1004
+ branches: [],
1005
+ statements: [],
1006
+ astInfo: {
1007
+ functions: [],
1008
+ branches: [],
1009
+ statements: []
1010
+ },
1011
+ // coverage data
1012
+ v8Data: {}
1013
+ };
1014
+
1015
+ originalStateMap.set(sourceIndex, originalState);
1016
+ });
1017
+
1018
+ return originalStateMap;
1019
+ };
1020
+
1021
+ const collectOriginalList = (state, originalStateMap) => {
1022
+
1023
+ const { fileUrls } = state;
1024
+ const distFile = state.sourcePath;
1025
+
1026
+ // collect original files
1027
+ originalStateMap.forEach((originalState) => {
1028
+
1029
+ const {
1030
+ js, type, sourcePath, source
1031
+ } = originalState;
1032
+
1033
+ // add file item
1034
+ const url = fileUrls[sourcePath] || sourcePath;
1035
+
1036
+ // add dist for id
1037
+ const id = Util.calculateSha1(sourcePath + source);
1038
+
1039
+ const sourceItem = {
1040
+ url,
1041
+ id,
1042
+ js,
1043
+ type,
1044
+ sourcePath,
1045
+ distFile,
1046
+ source
1047
+ };
1048
+
1049
+ // save v8 data and add to originalList
1050
+ originalState.v8Data = sourceItem;
1051
+ state.originalList.push(originalState);
1052
+ });
1053
+
1054
+ };
1055
+
1056
+ // ========================================================================================================
1057
+
1058
+ const generateCoverageForDist = (state) => {
1059
+
1060
+ handleFunctionsCoverage(state);
1061
+ handleBranchesCoverage(state);
1062
+ handleStatementsCoverage(state);
1063
+ handleGeneratedBytesCoverage(state);
1064
+
1065
+ };
1066
+
1067
+ const unpackSourceMap = (state, options) => {
1068
+
1069
+ const { sourceMap } = state;
1070
+
1071
+ // keep original urls
1072
+ const fileUrls = {};
1073
+ const sourcePathHandler = getSourcePathHandler(options);
1074
+ initSourceMapSourcePath(sourceMap, fileUrls, sourcePathHandler);
1075
+ state.fileUrls = fileUrls;
1076
+
1077
+ // ===============================================
1078
+ // decode mappings for each original file
1079
+
1080
+ const originalDecodedMap = new Map();
1081
+ // for find-original-range
1082
+ state.decodedMappings = decodeSourceMappings(state, originalDecodedMap);
1083
+
1084
+ // filter original list and init list
1085
+ const originalStateMap = initOriginalList(state, originalDecodedMap, options);
1086
+
1087
+ originalDecodedMap.clear();
1088
+
1089
+ // ===============================================
1090
+
1091
+ // handle functions before handle original state functions
1092
+ handleOriginalFunctionsCoverage(state, originalStateMap);
1093
+ handleOriginalBranchesCoverage(state, originalStateMap);
1094
+ handleOriginalStatementsCoverage(state, originalStateMap);
1095
+
1096
+ // handle lines info before handle ranges to update line count
1097
+ originalStateMap.forEach((originalState) => {
1098
+
1099
+ handleFunctionsCoverage(originalState);
1100
+ handleBranchesCoverage(originalState);
1101
+ handleStatementsCoverage(originalState);
1102
+
1103
+ // if (originalState.sourcePath.endsWith('demo.js')) {
1104
+ // console.log('=================================', originalState.sourcePath);
1105
+ // }
1106
+
1107
+ });
1108
+
1109
+ // handle bytes ranges
1110
+ handleAllOriginalBytesCoverage(state, originalStateMap);
1111
+
1112
+ originalStateMap.forEach((originalState) => {
1113
+ handleOriginalBytesCoverage(originalState);
1114
+ });
1115
+
1116
+ // collect coverage for original list
1117
+ collectOriginalList(state, originalStateMap);
1118
+
1119
+ };
1120
+
1121
+ const unpackDistFile = (item, state, options) => {
1122
+
1123
+ if (state.sourceMap) {
1124
+ if (Util.isDebug()) {
1125
+ // js self
1126
+ item.debug = true;
1127
+ generateCoverageForDist(state);
1128
+ } else {
1129
+ item.dedupe = true;
1130
+ }
1131
+
1132
+ // unpack source map
1133
+ unpackSourceMap(state, options);
1134
+
1135
+ } else {
1136
+
1137
+ // css/js self
1138
+ generateCoverageForDist(state);
1139
+
1140
+ }
1141
+
1142
+ };
1143
+
1144
+ // ========================================================================================================
1145
+
1146
+ const filterCoverageList = (item) => {
1147
+ const {
1148
+ functions, scriptOffset, source
1149
+ } = item;
1150
+
1151
+ // no script offset
1152
+ if (!scriptOffset) {
1153
+ return functions;
1154
+ }
1155
+
1156
+ // vm script offset
1157
+ const minOffset = scriptOffset;
1158
+ // the inline sourcemap could be removed
1159
+ const maxOffset = source.length;
1160
+
1161
+ const rootFunctionInfo = {
1162
+ root: true,
1163
+ ranges: [{
1164
+ startOffset: minOffset,
1165
+ endOffset: maxOffset,
1166
+ count: 1
1167
+ }]
1168
+ };
1169
+
1170
+ const coverageList = functions.filter((block) => {
1171
+ const { ranges } = block;
1172
+
1173
+ // first one is function coverage info
1174
+ const functionRange = ranges[0];
1175
+ const { startOffset, endOffset } = functionRange;
1176
+ if (startOffset >= minOffset && endOffset <= maxOffset) {
1177
+ return true;
1178
+ }
1179
+
1180
+ // blocks
1181
+ const len = ranges.length;
1182
+ if (len > 1) {
1183
+ for (let i = 1; i < len; i++) {
1184
+ const range = ranges[i];
1185
+ if (range.startOffset >= minOffset && range.endOffset <= maxOffset) {
1186
+ rootFunctionInfo.ranges.push(range);
1187
+ }
1188
+ }
1189
+ }
1190
+
1191
+ return false;
1192
+ });
1193
+
1194
+ // first one for root function
1195
+ if (rootFunctionInfo.ranges.length > 1) {
1196
+ coverageList.unshift(rootFunctionInfo);
1197
+ }
1198
+
1199
+ return coverageList;
1200
+ };
1201
+
1202
+ // ========================================================================================================
1203
+
1204
+ const isCoveredInRanges = (range, uncoveredBytes) => {
1205
+ if (!uncoveredBytes.length) {
1206
+ return true;
1207
+ }
1208
+ for (const item of uncoveredBytes) {
1209
+ if (range.start >= item.start && range.end <= item.end) {
1210
+ return false;
1211
+ }
1212
+ }
1213
+ return true;
1214
+ };
1215
+
1216
+ const mergeV8Data = (state, stateList) => {
1217
+ // console.log(stateList);
1218
+
1219
+ // const sourcePath = state.sourcePath;
1220
+ // console.log(sourcePath);
1221
+ // if (sourcePath.endsWith('scroll_zoom.ts')) {
1222
+ // console.log('merge v8 data ===================================', sourcePath);
1223
+ // console.log(stateList.map((it) => it.bytes));
1224
+ // console.log(stateList.map((it) => it.functions));
1225
+ // console.log(stateList.map((it) => it.branches.map((b) => [`${b.start}-${b.end}`, JSON.stringify(b.locations.map((l) => l.count))])));
1226
+ // }
1227
+
1228
+ // ===========================================================
1229
+ // bytes
1230
+ const mergedBytes = [];
1231
+ const uncoveredList = [];
1232
+ stateList.forEach((st) => {
1233
+ const bytes = dedupeCountRanges(st.bytes);
1234
+ const uncoveredBytes = [];
1235
+ bytes.forEach((range) => {
1236
+ if (range.count) {
1237
+ mergedBytes.push(range);
1238
+ } else {
1239
+ uncoveredBytes.push(range);
1240
+ }
1241
+ });
1242
+ uncoveredList.push(uncoveredBytes);
1243
+ });
1244
+ // just remove uncovered range
1245
+ uncoveredList.forEach((currentBytes) => {
1246
+ currentBytes.forEach((range) => {
1247
+ for (const targetBytes of uncoveredList) {
1248
+ if (targetBytes === currentBytes) {
1249
+ continue;
1250
+ }
1251
+ if (isCoveredInRanges(range, targetBytes)) {
1252
+ return;
1253
+ }
1254
+ }
1255
+ mergedBytes.push(range);
1256
+ });
1257
+ });
1258
+
1259
+ // will be dedupeCountRanges in collectFileCoverage
1260
+ state.bytes = mergedBytes;
1261
+
1262
+ // ===========================================================
1263
+ // functions
1264
+ const allFunctions = stateList.map((it) => it.functions).flat();
1265
+ const functionComparer = (lastRange, range) => {
1266
+ // if (lastRange.start === range.start && lastRange.end === range.end) {
1267
+ // return true;
1268
+ // }
1269
+
1270
+ // function range could be from sourcemap, not exact matched
1271
+
1272
+ // end is same
1273
+ // {start: 2017, end: 2315, count: 481}
1274
+ // {start: 2018, end: 2315, count: 14}
1275
+
1276
+ // start is same
1277
+ // {start: 10204, end: 10379, count: 0}
1278
+ // {start: 10204, end: 10393, count: 5}
1279
+
1280
+ // only one position matched could be same
1281
+ if (lastRange.start === range.start || lastRange.end === range.end) {
1282
+ // console.log(lastRange.start, range.start, lastRange.end, range.end);
1283
+
1284
+ // if (lastRange.start === range.start) {
1285
+ // console.log(range.end - lastRange.end, lastRange.start, lastRange.end, 'end', range.end, state.sourcePath);
1286
+ // } else {
1287
+ // console.log(range.start - lastRange.start, lastRange.start, lastRange.end, 'start', range.start, state.sourcePath);
1288
+ // }
1289
+
1290
+ return true;
1291
+ }
1292
+
1293
+ return false;
1294
+ };
1295
+ const functionHandler = (lastRange, range) => {
1296
+ lastRange.count += range.count;
1297
+ };
1298
+ const mergedFunctions = mergeRangesWith(allFunctions, functionComparer, functionHandler);
1299
+ state.functions = mergedFunctions;
1300
+
1301
+ // ===========================================================
1302
+ // branches
1303
+ const allBranches = stateList.map((it) => it.branches).flat();
1304
+ const branchComparer = (lastRange, range) => {
1305
+ // exact matched because the branch range is generated from ast
1306
+ return lastRange.start === range.start && lastRange.end === range.end;
1307
+ };
1308
+ const branchHandler = (lastRange, range) => {
1309
+ // merge locations count
1310
+ lastRange.locations.forEach((item, i) => {
1311
+ const loc = range.locations[i];
1312
+ if (loc) {
1313
+ item.count += loc.count;
1314
+ }
1315
+ });
1316
+ };
1317
+ const mergedBranches = mergeRangesWith(allBranches, branchComparer, branchHandler);
1318
+ state.branches = mergedBranches;
1319
+
1320
+ // if (sourcePath.endsWith('scroll_zoom.ts')) {
1321
+ // console.log(mergedBytes);
1322
+ // console.log(mergedFunctions);
1323
+ // console.log(mergedBranches.map((b) => [`${b.start}-${b.end}`, JSON.stringify(b.locations.map((l) => l.count))]));
1324
+ // }
1325
+
1326
+
1327
+ };
1328
+
1329
+ const generateV8DataList = (stateList, options) => {
1330
+
1331
+ const stateMap = new Map();
1332
+
1333
+ // all original files from dist
1334
+ const allOriginalList = [];
1335
+ stateList.forEach((state) => {
1336
+ const { v8Data, originalList } = state;
1337
+ // dedupe dist file if not debug
1338
+ if (!v8Data.dedupe) {
1339
+ stateMap.set(v8Data.id, state);
1340
+ }
1341
+ allOriginalList.push(originalList);
1342
+ });
1343
+
1344
+ // merge istanbul and v8(converted)
1345
+ const mergeMap = new Map();
1346
+ allOriginalList.flat().forEach((originalState) => {
1347
+ const { v8Data } = originalState;
1348
+ const id = v8Data.id;
1349
+ // exists item
1350
+ const prevState = stateMap.get(id);
1351
+ if (prevState) {
1352
+ // ignore empty item, just override it
1353
+ if (!prevState.v8Data.empty) {
1354
+ if (mergeMap.has(id)) {
1355
+ mergeMap.get(id).push(originalState);
1356
+ } else {
1357
+ mergeMap.set(id, [prevState, originalState]);
1358
+ }
1359
+ return;
1360
+ }
1361
+ }
1362
+ stateMap.set(id, originalState);
1363
+ });
1364
+
1365
+ const mergeIds = mergeMap.keys();
1366
+ for (const id of mergeIds) {
1367
+ const state = stateMap.get(id);
1368
+ // for source the type could be ts, so just use js (boolean)
1369
+ if (state.js) {
1370
+ mergeV8Data(state, mergeMap.get(id));
1371
+ } else {
1372
+ // should no css here, css can not be in sources
1373
+ }
1374
+ }
1375
+
1376
+ // new v8 data list (includes sources)
1377
+ const v8DataList = [];
1378
+ // global file sources and istanbul coverage data
1379
+ const fileSources = {};
1380
+ const coverageData = {};
1381
+ stateMap.forEach((state) => {
1382
+ const { v8Data } = state;
1383
+ const istanbulData = collectFileCoverage(v8Data, state, options);
1384
+ const { sourcePath, source } = v8Data;
1385
+ v8DataList.push(v8Data);
1386
+ fileSources[sourcePath] = source;
1387
+ coverageData[sourcePath] = istanbulData;
1388
+ });
1389
+
1390
+ return {
1391
+ v8DataList,
1392
+ fileSources,
1393
+ coverageData
1394
+ };
1395
+ };
1396
+
1397
+ const convertV8List = (v8list, options) => {
1398
+
1399
+ const stateList = [];
1400
+
1401
+ for (const item of v8list) {
1402
+ // console.log([item.id]);
1403
+
1404
+ const {
1405
+ type, source, sourcePath
1406
+ } = item;
1407
+
1408
+ // for source file, type could be ts or vue as extname, but js = true
1409
+ const js = type === 'js';
1410
+ item.js = js;
1411
+
1412
+ // source mapping
1413
+ const locator = new Locator(source);
1414
+
1415
+ // ============================
1416
+ // move sourceMap
1417
+ const sourceMap = item.sourceMap;
1418
+ if (sourceMap) {
1419
+ delete item.sourceMap;
1420
+ }
1421
+
1422
+ // ============================
1423
+ const time_start_ast = Date.now();
1424
+ // move functions and ranges to coverageList
1425
+ let coverageList = [];
1426
+ let astInfo;
1427
+ if (js) {
1428
+ coverageList = filterCoverageList(item);
1429
+ // remove original functions
1430
+ if (!Util.isDebug()) {
1431
+ delete item.functions;
1432
+ }
1433
+ astInfo = getJsAstInfo(item, coverageList);
1434
+ } else {
1435
+ // convent css covered ranges to rules ranges and include uncovered ranges
1436
+ astInfo = getCssAstInfo(item, coverageList);
1437
+ // remove original ranges
1438
+ if (!Util.isDebug()) {
1439
+ delete item.ranges;
1440
+ }
1441
+ }
1442
+ Util.logTime(`- parsed ast: ${sourcePath}`, time_start_ast);
1443
+
1444
+ // console.log(sourcePath, astInfo.statements.length);
1445
+ // ============================
1446
+
1447
+ // current file and it's sources from sourceMap
1448
+ // see const originalState
1449
+ const state = {
1450
+ js,
1451
+ type,
1452
+ source,
1453
+ sourcePath,
1454
+ sourceMap,
1455
+ locator,
1456
+ decodedMappings: [],
1457
+ // coverage info
1458
+ bytes: [],
1459
+ functions: [],
1460
+ branches: [],
1461
+ statements: [],
1462
+ astInfo,
1463
+ // for sub source files
1464
+ coverageList,
1465
+ originalList: [],
1466
+ // coverage data
1467
+ v8Data: item
1468
+ };
1469
+
1470
+ const time_start_unpack = Date.now();
1471
+ unpackDistFile(item, state, options);
1472
+ Util.logTime(`- unpacked dist: ${sourcePath}`, time_start_unpack);
1473
+
1474
+ stateList.push(state);
1475
+
1476
+ }
1477
+
1478
+ const time_start_convert = Date.now();
1479
+ const dataList = generateV8DataList(stateList, options);
1480
+ Util.logTime('- generated v8 data list', time_start_convert);
1481
+
1482
+ return dataList;
1483
+ };
1484
+
1485
+ module.exports = {
1486
+ convertV8List
1487
+ };