monocart-coverage-reports 2.13.1 → 3.0.0
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.
- package/README.md +267 -114
- package/README.zh-Hans.md +268 -115
- package/lib/assets.js +6 -6
- package/lib/cli.js +191 -95
- package/lib/client/cdp-client.js +74 -43
- package/lib/client/coverage-client.js +24 -8
- package/lib/client/ws-session.js +115 -37
- package/lib/converter/ast-visitor.js +5 -7
- package/lib/converter/ast.js +6 -6
- package/lib/converter/collect-source-maps.js +119 -92
- package/lib/converter/converter.js +49 -117
- package/lib/converter/fake-source-coverage.js +64 -0
- package/lib/converter/find-original-range.js +3 -3
- package/lib/converter/flatten-source-maps.js +2 -4
- package/lib/converter/ignore.js +2 -4
- package/lib/converter/info-branch.js +5 -3
- package/lib/converter/info-function.js +5 -3
- package/lib/converter/info-statement.js +5 -3
- package/lib/converter/range-index.js +78 -0
- package/lib/converter/source-range-merge.js +103 -0
- package/lib/converter/untested.js +27 -55
- package/lib/default/options.js +13 -1
- package/lib/generate.js +44 -26
- package/lib/index.d.ts +52 -4
- package/lib/index.js +35 -41
- package/lib/istanbul/istanbul-summary.js +2 -2
- package/lib/istanbul/istanbul.js +35 -13
- package/lib/packages/chunks/chunk-RVOSNMVU.js +2 -0
- package/lib/packages/monocart-coverage-assets.js +4 -3
- package/lib/packages/monocart-coverage-converter.js +27 -0
- package/lib/packages/monocart-coverage-vendor.js +2 -41
- package/lib/packages/monocart-coverage-vendor.js.LICENSE +3 -221
- package/lib/platform/concurrency.js +47 -43
- package/lib/platform/share.js +1 -1
- package/lib/register/hooks.js +19 -13
- package/lib/register/register.js +5 -3
- package/lib/reports/codacy.js +3 -5
- package/lib/reports/codecov.js +3 -5
- package/lib/reports/console-details.js +5 -5
- package/lib/reports/console-summary.js +4 -4
- package/lib/reports/custom.js +8 -24
- package/lib/reports/markdown-details.js +4 -6
- package/lib/reports/markdown-summary.js +4 -6
- package/lib/reports/raw.js +10 -12
- package/lib/reports/v8-json.js +3 -5
- package/lib/reports/v8.js +3 -5
- package/lib/utils/dedupe.js +1 -1
- package/lib/utils/gc.js +30 -14
- package/lib/utils/markdown.js +2 -2
- package/lib/utils/merge/merge.js +2 -4
- package/lib/utils/merge/range-tree.js +1 -1
- package/lib/utils/request.js +85 -73
- package/lib/utils/snapshot.js +4 -4
- package/lib/utils/source-path.js +5 -5
- package/lib/utils/util.js +61 -41
- package/lib/v8/v8-summary.js +2 -4
- package/lib/v8/v8.js +11 -11
- package/migrate-to-3.md +175 -0
- package/package.json +153 -149
- package/skills/monocart-coverage-reports/SKILL.md +106 -0
- package/skills/monocart-coverage-reports/references/cli.md +108 -0
- package/skills/monocart-coverage-reports/references/collection.md +152 -0
- package/skills/monocart-coverage-reports/references/filtering-and-sourcemaps.md +170 -0
- package/skills/monocart-coverage-reports/references/reports-and-ci.md +219 -0
- package/skills/monocart-coverage-reports/references/repository-map.md +55 -0
- package/lib/index.mjs +0 -9
- package/lib/register/hooks.mjs +0 -3
- package/lib/register/register.mjs +0 -3
|
@@ -4,27 +4,29 @@
|
|
|
4
4
|
* @author cenfun@gmail.com
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import EC from 'eight-colors';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import { Locator } from 'monocart-locator';
|
|
10
|
+
import { Util } from '../utils/util.js';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
import { findOriginalRange } from './find-original-range.js';
|
|
13
|
+
import { isUncoveredRange } from './source-range-merge.js';
|
|
14
|
+
import { handleUncoveredInCovered } from './fake-source-coverage.js';
|
|
15
|
+
import { getJsAstInfo, getCssAstInfo } from './ast.js';
|
|
16
|
+
import { getIgnoredRanges } from './ignore.js';
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
import { getUntestedList } from './untested.js';
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
import {
|
|
19
21
|
sortRanges, dedupeCountRanges, mergeRangesWith
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
+
} from '../utils/dedupe.js';
|
|
23
|
+
import { getSourceType, initSourceMapSourcesPath } from '../utils/source-path.js';
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
import { decode } from '@jridgewell/sourcemap-codec';
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
import { InfoBranch } from './info-branch.js';
|
|
28
|
+
import { InfoFunction } from './info-function.js';
|
|
29
|
+
import { InfoStatement } from './info-statement.js';
|
|
28
30
|
|
|
29
31
|
// ========================================================================================================
|
|
30
32
|
|
|
@@ -212,7 +214,7 @@ const calculateV8Summary = (list) => {
|
|
|
212
214
|
* * `f` - hit count for functions
|
|
213
215
|
* * `b` - hit count for branches
|
|
214
216
|
*/
|
|
215
|
-
const collectFileCoverage = (v8Data, state, options) => {
|
|
217
|
+
const collectFileCoverage = (v8Data, state, options, needsIstanbul) => {
|
|
216
218
|
|
|
217
219
|
const {
|
|
218
220
|
bytes,
|
|
@@ -220,7 +222,6 @@ const collectFileCoverage = (v8Data, state, options) => {
|
|
|
220
222
|
branches,
|
|
221
223
|
statements,
|
|
222
224
|
|
|
223
|
-
sourcePath,
|
|
224
225
|
locator
|
|
225
226
|
} = state;
|
|
226
227
|
|
|
@@ -313,7 +314,19 @@ const collectFileCoverage = (v8Data, state, options) => {
|
|
|
313
314
|
v8Data.summary = summary;
|
|
314
315
|
|
|
315
316
|
// ==========================================
|
|
316
|
-
//
|
|
317
|
+
// Istanbul location maps are not needed by V8-only reports. Keep the
|
|
318
|
+
// V8 ranges, counts and summaries above identical in either mode.
|
|
319
|
+
if (!needsIstanbul) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return collectIstanbulCoverage(state);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const collectIstanbulCoverage = (state) => {
|
|
327
|
+
const {
|
|
328
|
+
sourcePath, locator, statements, functions, branches
|
|
329
|
+
} = state;
|
|
317
330
|
const istanbulData = {
|
|
318
331
|
path: sourcePath,
|
|
319
332
|
|
|
@@ -906,6 +919,12 @@ const decodeSourceMappings = (state, originalDecodedMap) => {
|
|
|
906
919
|
// console.log(segment);
|
|
907
920
|
return;
|
|
908
921
|
}
|
|
922
|
+
// A malformed map may refer to a source outside its sources array.
|
|
923
|
+
// Check when mappings are already decoded, not during map selection.
|
|
924
|
+
const sourceMappings = originalDecodedMap.get(sourceIndex);
|
|
925
|
+
if (!sourceMappings) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
909
928
|
|
|
910
929
|
// 1-base
|
|
911
930
|
const generatedSN = generatedLine + 1;
|
|
@@ -936,7 +955,7 @@ const decodeSourceMappings = (state, originalDecodedMap) => {
|
|
|
936
955
|
}
|
|
937
956
|
|
|
938
957
|
allDecodedMappings.push(info);
|
|
939
|
-
|
|
958
|
+
sourceMappings.push(info);
|
|
940
959
|
|
|
941
960
|
// calculate line end column
|
|
942
961
|
if (!info.last) {
|
|
@@ -971,7 +990,7 @@ const decodeSourceMappings = (state, originalDecodedMap) => {
|
|
|
971
990
|
// console.log(generatedSN, generatedColumn, info, endColumn);
|
|
972
991
|
|
|
973
992
|
allDecodedMappings.push(endColumn);
|
|
974
|
-
|
|
993
|
+
sourceMappings.push(endColumn);
|
|
975
994
|
|
|
976
995
|
});
|
|
977
996
|
|
|
@@ -1151,56 +1170,6 @@ const generateCoverageForDist = (state) => {
|
|
|
1151
1170
|
|
|
1152
1171
|
};
|
|
1153
1172
|
|
|
1154
|
-
const handleUncoveredInCovered = (state, originalStateMap) => {
|
|
1155
|
-
if (!state.fake) {
|
|
1156
|
-
return;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
// fake statements/branches/functions
|
|
1160
|
-
originalStateMap.forEach((originalState) => {
|
|
1161
|
-
const { bytes } = originalState;
|
|
1162
|
-
const list = [];
|
|
1163
|
-
originalState.bytes = bytes.filter((item) => {
|
|
1164
|
-
if (item.count === 0) {
|
|
1165
|
-
const range = bytes.find((it) => {
|
|
1166
|
-
if (it.count === 0) {
|
|
1167
|
-
return false;
|
|
1168
|
-
}
|
|
1169
|
-
if (it.start > item.end) {
|
|
1170
|
-
return false;
|
|
1171
|
-
}
|
|
1172
|
-
if (it.end < item.start) {
|
|
1173
|
-
return false;
|
|
1174
|
-
}
|
|
1175
|
-
return true;
|
|
1176
|
-
});
|
|
1177
|
-
if (range) {
|
|
1178
|
-
list.push(item);
|
|
1179
|
-
return false;
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
return true;
|
|
1183
|
-
});
|
|
1184
|
-
|
|
1185
|
-
if (!list.length) {
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
list.forEach((item) => {
|
|
1190
|
-
['statements', 'branches', 'functions'].forEach((key) => {
|
|
1191
|
-
originalState[key] = originalState[key].filter((it) => {
|
|
1192
|
-
if (it.start === item.start && it.end === item.end) {
|
|
1193
|
-
return false;
|
|
1194
|
-
}
|
|
1195
|
-
return true;
|
|
1196
|
-
});
|
|
1197
|
-
});
|
|
1198
|
-
});
|
|
1199
|
-
|
|
1200
|
-
});
|
|
1201
|
-
|
|
1202
|
-
};
|
|
1203
|
-
|
|
1204
1173
|
const unpackSourceMap = (state, options) => {
|
|
1205
1174
|
|
|
1206
1175
|
const { sourceMap, sourcePath } = state;
|
|
@@ -1519,44 +1488,6 @@ const convertCoverages = (list, options, untested) => {
|
|
|
1519
1488
|
|
|
1520
1489
|
// ========================================================================================================
|
|
1521
1490
|
|
|
1522
|
-
const isUncoveredRange = (range, key, uncoveredGroups) => {
|
|
1523
|
-
|
|
1524
|
-
let uncovered = true;
|
|
1525
|
-
uncoveredGroups.forEach((group) => {
|
|
1526
|
-
const { groupMap, state } = group;
|
|
1527
|
-
|
|
1528
|
-
// ========================================================
|
|
1529
|
-
// self key
|
|
1530
|
-
if (groupMap.has(key)) {
|
|
1531
|
-
return;
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
// ========================================================
|
|
1535
|
-
// in all group
|
|
1536
|
-
for (const item of groupMap.values()) {
|
|
1537
|
-
if (range.start >= item.start && range.end <= item.end) {
|
|
1538
|
-
return;
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
// ========================================================
|
|
1543
|
-
// no sourcemap mappings in range
|
|
1544
|
-
// check original range in decodedMappings
|
|
1545
|
-
const { decodedMappings } = state;
|
|
1546
|
-
const item = decodedMappings.find((it) => it.originalOffset >= range.start && it.originalOffset <= range.end);
|
|
1547
|
-
if (!item) {
|
|
1548
|
-
return;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
// ========================================================
|
|
1552
|
-
uncovered = false;
|
|
1553
|
-
|
|
1554
|
-
});
|
|
1555
|
-
|
|
1556
|
-
return uncovered;
|
|
1557
|
-
|
|
1558
|
-
};
|
|
1559
|
-
|
|
1560
1491
|
const mergeV8Data = (state, stateList) => {
|
|
1561
1492
|
// console.log(stateList);
|
|
1562
1493
|
|
|
@@ -1741,7 +1672,7 @@ const addUntestedFiles = async (stateList, options) => {
|
|
|
1741
1672
|
|
|
1742
1673
|
};
|
|
1743
1674
|
|
|
1744
|
-
const generateV8DataList = (stateList, options) => {
|
|
1675
|
+
const generateV8DataList = (stateList, options, needsIstanbul) => {
|
|
1745
1676
|
|
|
1746
1677
|
const stateMap = new Map();
|
|
1747
1678
|
|
|
@@ -1795,11 +1726,13 @@ const generateV8DataList = (stateList, options) => {
|
|
|
1795
1726
|
const coverageData = {};
|
|
1796
1727
|
stateMap.forEach((state) => {
|
|
1797
1728
|
const { v8Data } = state;
|
|
1798
|
-
const istanbulData = collectFileCoverage(v8Data, state, options);
|
|
1799
|
-
const { sourcePath, source } = v8Data;
|
|
1729
|
+
const istanbulData = collectFileCoverage(v8Data, state, options, needsIstanbul);
|
|
1800
1730
|
v8DataList.push(v8Data);
|
|
1801
|
-
|
|
1802
|
-
|
|
1731
|
+
if (needsIstanbul) {
|
|
1732
|
+
const { sourcePath, source } = v8Data;
|
|
1733
|
+
fileSources[sourcePath] = source;
|
|
1734
|
+
coverageData[sourcePath] = istanbulData;
|
|
1735
|
+
}
|
|
1803
1736
|
});
|
|
1804
1737
|
|
|
1805
1738
|
// sort v8DataList (files)
|
|
@@ -1817,7 +1750,8 @@ const generateV8DataList = (stateList, options) => {
|
|
|
1817
1750
|
};
|
|
1818
1751
|
};
|
|
1819
1752
|
|
|
1820
|
-
|
|
1753
|
+
// Direct users of the converter retain the full result by default.
|
|
1754
|
+
const convertV8List = async (v8list, options, needsIstanbul = true) => {
|
|
1821
1755
|
|
|
1822
1756
|
// for tested files
|
|
1823
1757
|
const stateList = convertCoverages(v8list, options);
|
|
@@ -1826,13 +1760,11 @@ const convertV8List = async (v8list, options) => {
|
|
|
1826
1760
|
await addUntestedFiles(stateList, options);
|
|
1827
1761
|
|
|
1828
1762
|
const time_start_convert = Date.now();
|
|
1829
|
-
const dataList = generateV8DataList(stateList, options);
|
|
1763
|
+
const dataList = generateV8DataList(stateList, options, needsIstanbul);
|
|
1830
1764
|
const dataFiles = EC.cyan(`${dataList.v8DataList.length} files`);
|
|
1831
1765
|
Util.logTime(`${EC.magenta('│ ')}${EC.cyan('├')} [convert] converted data list (${dataFiles})`, time_start_convert);
|
|
1832
1766
|
|
|
1833
1767
|
return dataList;
|
|
1834
1768
|
};
|
|
1835
1769
|
|
|
1836
|
-
|
|
1837
|
-
convertV8List
|
|
1838
|
-
};
|
|
1770
|
+
export { convertV8List };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const rangeKey = (range) => `${range.start}_${range.end}`;
|
|
2
|
+
|
|
3
|
+
const handleUncoveredInCovered = (state, originalStateMap) => {
|
|
4
|
+
if (!state.fake) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
originalStateMap.forEach((originalState) => {
|
|
9
|
+
const { bytes } = originalState;
|
|
10
|
+
// The old check treats every non-zero count as covered, not just > 0.
|
|
11
|
+
const covered = bytes.filter((range) => range.count !== 0);
|
|
12
|
+
if (!covered.length || covered.length === bytes.length) {
|
|
13
|
+
originalState.bytes = bytes.slice();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Sort a separate array so byte order and original range objects stay
|
|
18
|
+
// unchanged. Prefix maxima also handle nested/overlapping coverage.
|
|
19
|
+
covered.sort((a, b) => a.start - b.start);
|
|
20
|
+
const starts = new Float64Array(covered.length);
|
|
21
|
+
const maxEnds = new Float64Array(covered.length);
|
|
22
|
+
let maxEnd = -Infinity;
|
|
23
|
+
for (let i = 0; i < covered.length; i++) {
|
|
24
|
+
starts[i] = covered[i].start;
|
|
25
|
+
maxEnd = Math.max(maxEnd, covered[i].end);
|
|
26
|
+
maxEnds[i] = maxEnd;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const removed = new Set();
|
|
30
|
+
originalState.bytes = bytes.filter((range) => {
|
|
31
|
+
if (range.count !== 0) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
let low = 0;
|
|
35
|
+
let high = starts.length;
|
|
36
|
+
while (low < high) {
|
|
37
|
+
const mid = Math.floor((low + high) / 2);
|
|
38
|
+
// Preserve the original inclusive overlap test: touching
|
|
39
|
+
// endpoints count as overlap even though ranges are half-open.
|
|
40
|
+
if (starts[mid] <= range.end) {
|
|
41
|
+
low = mid + 1;
|
|
42
|
+
} else {
|
|
43
|
+
high = mid;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (low > 0 && maxEnds[low - 1] >= range.start) {
|
|
47
|
+
removed.add(rangeKey(range));
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!removed.size) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Remove exact start/end matches, not every overlapping AST range.
|
|
57
|
+
// Each metadata array is filtered once, regardless of removal count.
|
|
58
|
+
for (const key of ['statements', 'branches', 'functions']) {
|
|
59
|
+
originalState[key] = originalState[key].filter((range) => !removed.has(rangeKey(range)));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export { handleUncoveredInCovered };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { diffSequence } from '../packages/monocart-coverage-converter.js';
|
|
2
|
+
import { Util } from '../utils/util.js';
|
|
3
3
|
const EC = Util.EC;
|
|
4
4
|
|
|
5
5
|
const findMapping = (list, offset) => {
|
|
@@ -977,4 +977,4 @@ const findOriginalRange = (start, end, state, originalMap, options = {}) => {
|
|
|
977
977
|
|
|
978
978
|
};
|
|
979
979
|
|
|
980
|
-
|
|
980
|
+
export { findOriginalRange };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { decode } from '@jridgewell/sourcemap-codec';
|
|
2
2
|
|
|
3
3
|
const COLUMN = 0;
|
|
4
4
|
const SOURCES_INDEX = 1;
|
|
@@ -181,6 +181,4 @@ const flattenSourceMaps = function(indexedMap, mapUrl) {
|
|
|
181
181
|
return indexedMap;
|
|
182
182
|
};
|
|
183
183
|
|
|
184
|
-
|
|
185
|
-
flattenSourceMaps
|
|
186
|
-
};
|
|
184
|
+
export { flattenSourceMaps };
|
package/lib/converter/ignore.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { Util } from '../utils/util.js';
|
|
2
2
|
|
|
3
3
|
const getNextN = (content, currentLine, item, locator) => {
|
|
4
4
|
|
|
@@ -220,6 +220,4 @@ const getIgnoredRanges = (locator, options) => {
|
|
|
220
220
|
};
|
|
221
221
|
|
|
222
222
|
|
|
223
|
-
|
|
224
|
-
getIgnoredRanges
|
|
225
|
-
};
|
|
223
|
+
export { getIgnoredRanges };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { Util } from '../utils/util.js';
|
|
2
|
+
class InfoBranch {
|
|
3
3
|
constructor(data, original) {
|
|
4
4
|
const {
|
|
5
5
|
start, end, locations, type
|
|
@@ -90,4 +90,6 @@ module.exports = class InfoBranch {
|
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { InfoBranch };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { Util } from '../utils/util.js';
|
|
2
|
+
class InfoFunction {
|
|
3
3
|
constructor(data, original) {
|
|
4
4
|
|
|
5
5
|
const {
|
|
@@ -63,4 +63,6 @@ module.exports = class InfoFunction {
|
|
|
63
63
|
line: line
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
|
-
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { InfoFunction };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { Util } from '../utils/util.js';
|
|
2
|
+
class InfoStatement {
|
|
3
3
|
constructor(data, original) {
|
|
4
4
|
const {
|
|
5
5
|
start, end, count
|
|
@@ -40,4 +40,6 @@ module.exports = class InfoStatement {
|
|
|
40
40
|
|
|
41
41
|
return loc;
|
|
42
42
|
}
|
|
43
|
-
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { InfoStatement };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// AST block arrays are immutable during a visit. Weak keys release the index
|
|
2
|
+
// with the array, and rebuilding coverage info creates fresh arrays/indices.
|
|
3
|
+
const indices = new WeakMap();
|
|
4
|
+
const linearSearchLimit = 16;
|
|
5
|
+
|
|
6
|
+
const createIndex = (ranges) => {
|
|
7
|
+
// V8 ranges are in start-offset order. Preserve last-in-input semantics for
|
|
8
|
+
// custom, unsorted coverage rather than sorting it and changing the result.
|
|
9
|
+
for (let i = 1; i < ranges.length; i++) {
|
|
10
|
+
if (ranges[i].startOffset < ranges[i - 1].startOffset) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let size = 1;
|
|
16
|
+
while (size < ranges.length) {
|
|
17
|
+
size *= 2;
|
|
18
|
+
}
|
|
19
|
+
const maxEnds = new Float64Array(size * 2);
|
|
20
|
+
maxEnds.fill(-Infinity);
|
|
21
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
22
|
+
maxEnds[size + i] = ranges[i].endOffset;
|
|
23
|
+
}
|
|
24
|
+
for (let i = size - 1; i > 0; i--) {
|
|
25
|
+
maxEnds[i] = Math.max(maxEnds[i * 2], maxEnds[i * 2 + 1]);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
size, maxEnds
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const findRightmost = (maxEnds, node, left, right, limit, end) => {
|
|
33
|
+
if (left >= limit || maxEnds[node] < end) {
|
|
34
|
+
return -1;
|
|
35
|
+
}
|
|
36
|
+
if (right - left === 1) {
|
|
37
|
+
return left;
|
|
38
|
+
}
|
|
39
|
+
const mid = (left + right) / 2;
|
|
40
|
+
const found = findRightmost(maxEnds, node * 2 + 1, mid, right, limit, end);
|
|
41
|
+
if (found !== -1) {
|
|
42
|
+
return found;
|
|
43
|
+
}
|
|
44
|
+
return findRightmost(maxEnds, node * 2, left, mid, limit, end);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Return the last enclosing range, including equal-start ranges. For sorted
|
|
48
|
+
// arrays the O(n) index is built once; subsequent queries take O(log n).
|
|
49
|
+
const findContainingRange = (ranges, start, end) => {
|
|
50
|
+
if (ranges.length <= linearSearchLimit) {
|
|
51
|
+
return ranges.findLast((range) => range.startOffset <= start && range.endOffset >= end);
|
|
52
|
+
}
|
|
53
|
+
if (!indices.has(ranges)) {
|
|
54
|
+
indices.set(ranges, createIndex(ranges));
|
|
55
|
+
}
|
|
56
|
+
const index = indices.get(ranges);
|
|
57
|
+
if (!index) {
|
|
58
|
+
return ranges.findLast((range) => range.startOffset <= start && range.endOffset >= end);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Only the prefix with startOffset <= start can contain the query.
|
|
62
|
+
let low = 0;
|
|
63
|
+
let high = ranges.length;
|
|
64
|
+
while (low < high) {
|
|
65
|
+
const mid = Math.floor((low + high) / 2);
|
|
66
|
+
if (ranges[mid].startOffset <= start) {
|
|
67
|
+
low = mid + 1;
|
|
68
|
+
} else {
|
|
69
|
+
high = mid;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const found = findRightmost(index.maxEnds, 1, 0, index.size, low, end);
|
|
73
|
+
if (found !== -1) {
|
|
74
|
+
return ranges[found];
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export { findContainingRange };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Each group belongs to one source-file merge and is immutable while queried.
|
|
2
|
+
// Weak keys keep indices from retaining coverage after that merge finishes.
|
|
3
|
+
const rangeIndices = new WeakMap();
|
|
4
|
+
const mappingIndices = new WeakMap();
|
|
5
|
+
const mappingScanLimit = 32;
|
|
6
|
+
|
|
7
|
+
const createRangeIndex = (groupMap) => {
|
|
8
|
+
// Copy before sorting: custom group order must not be changed.
|
|
9
|
+
const ranges = Array.from(groupMap.values()).sort((a, b) => a.start - b.start);
|
|
10
|
+
const starts = new Float64Array(ranges.length);
|
|
11
|
+
const maxEnds = new Float64Array(ranges.length);
|
|
12
|
+
let maxEnd = -Infinity;
|
|
13
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
14
|
+
starts[i] = ranges[i].start;
|
|
15
|
+
maxEnd = Math.max(maxEnd, ranges[i].end);
|
|
16
|
+
maxEnds[i] = maxEnd;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
starts, maxEnds
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const containsRange = (group, start, end) => {
|
|
24
|
+
let index = rangeIndices.get(group);
|
|
25
|
+
if (!index) {
|
|
26
|
+
index = createRangeIndex(group.groupMap);
|
|
27
|
+
rangeIndices.set(group, index);
|
|
28
|
+
}
|
|
29
|
+
const { starts, maxEnds } = index;
|
|
30
|
+
let low = 0;
|
|
31
|
+
let high = starts.length;
|
|
32
|
+
// The prefix ending at low contains every possible enclosing range.
|
|
33
|
+
while (low < high) {
|
|
34
|
+
const mid = Math.floor((low + high) / 2);
|
|
35
|
+
if (starts[mid] <= start) {
|
|
36
|
+
low = mid + 1;
|
|
37
|
+
} else {
|
|
38
|
+
high = mid;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return low > 0 && maxEnds[low - 1] >= end;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const scanMappingPrefix = (mappings, start, end) => {
|
|
45
|
+
const limit = Math.min(mappingScanLimit, mappings.length);
|
|
46
|
+
for (let i = 0; i < limit; i++) {
|
|
47
|
+
const offset = mappings[i].originalOffset;
|
|
48
|
+
if (offset >= start && offset <= end) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// A short list was exhausted; otherwise the answer is still unknown.
|
|
53
|
+
return mappings.length <= limit ? false : null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const hasMapping = (group, start, end) => {
|
|
57
|
+
let offsets = mappingIndices.get(group);
|
|
58
|
+
if (!offsets) {
|
|
59
|
+
const mappings = group.state.decodedMappings;
|
|
60
|
+
// Early hits (or exhausted short lists) need no full copy/sort. Probe
|
|
61
|
+
// only a bounded prefix so many misses cannot become quadratic scans.
|
|
62
|
+
const match = scanMappingPrefix(mappings, start, end);
|
|
63
|
+
if (match !== null) {
|
|
64
|
+
return match;
|
|
65
|
+
}
|
|
66
|
+
// Sort a separate numeric index, not the shared decoded mappings. The
|
|
67
|
+
// upstream order is by line/column; malformed columns can produce
|
|
68
|
+
// non-monotonic offsets, so do not assume offset order here.
|
|
69
|
+
offsets = Float64Array.from(mappings, (item) => item.originalOffset).sort();
|
|
70
|
+
mappingIndices.set(group, offsets);
|
|
71
|
+
}
|
|
72
|
+
let low = 0;
|
|
73
|
+
let high = offsets.length;
|
|
74
|
+
while (low < high) {
|
|
75
|
+
const mid = Math.floor((low + high) / 2);
|
|
76
|
+
if (offsets[mid] < start) {
|
|
77
|
+
low = mid + 1;
|
|
78
|
+
} else {
|
|
79
|
+
high = mid;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Preserve the old inclusive endpoints, including a mapping at range.end.
|
|
83
|
+
return low < offsets.length && offsets[low] <= end;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const isUncoveredRange = (range, key, uncoveredGroups) => {
|
|
87
|
+
for (const group of uncoveredGroups) {
|
|
88
|
+
if (group.groupMap.has(key)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (containsRange(group, range.start, range.end)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (hasMapping(group, range.start, range.end)) {
|
|
95
|
+
// Another group maps this range without marking it uncovered.
|
|
96
|
+
// No later group can change that decision.
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export { isUncoveredRange };
|