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.
Files changed (68) hide show
  1. package/README.md +267 -114
  2. package/README.zh-Hans.md +268 -115
  3. package/lib/assets.js +6 -6
  4. package/lib/cli.js +191 -95
  5. package/lib/client/cdp-client.js +74 -43
  6. package/lib/client/coverage-client.js +24 -8
  7. package/lib/client/ws-session.js +115 -37
  8. package/lib/converter/ast-visitor.js +5 -7
  9. package/lib/converter/ast.js +6 -6
  10. package/lib/converter/collect-source-maps.js +119 -92
  11. package/lib/converter/converter.js +49 -117
  12. package/lib/converter/fake-source-coverage.js +64 -0
  13. package/lib/converter/find-original-range.js +3 -3
  14. package/lib/converter/flatten-source-maps.js +2 -4
  15. package/lib/converter/ignore.js +2 -4
  16. package/lib/converter/info-branch.js +5 -3
  17. package/lib/converter/info-function.js +5 -3
  18. package/lib/converter/info-statement.js +5 -3
  19. package/lib/converter/range-index.js +78 -0
  20. package/lib/converter/source-range-merge.js +103 -0
  21. package/lib/converter/untested.js +27 -55
  22. package/lib/default/options.js +13 -1
  23. package/lib/generate.js +44 -26
  24. package/lib/index.d.ts +52 -4
  25. package/lib/index.js +35 -41
  26. package/lib/istanbul/istanbul-summary.js +2 -2
  27. package/lib/istanbul/istanbul.js +35 -13
  28. package/lib/packages/chunks/chunk-RVOSNMVU.js +2 -0
  29. package/lib/packages/monocart-coverage-assets.js +4 -3
  30. package/lib/packages/monocart-coverage-converter.js +27 -0
  31. package/lib/packages/monocart-coverage-vendor.js +2 -41
  32. package/lib/packages/monocart-coverage-vendor.js.LICENSE +3 -221
  33. package/lib/platform/concurrency.js +47 -43
  34. package/lib/platform/share.js +1 -1
  35. package/lib/register/hooks.js +19 -13
  36. package/lib/register/register.js +5 -3
  37. package/lib/reports/codacy.js +3 -5
  38. package/lib/reports/codecov.js +3 -5
  39. package/lib/reports/console-details.js +5 -5
  40. package/lib/reports/console-summary.js +4 -4
  41. package/lib/reports/custom.js +8 -24
  42. package/lib/reports/markdown-details.js +4 -6
  43. package/lib/reports/markdown-summary.js +4 -6
  44. package/lib/reports/raw.js +10 -12
  45. package/lib/reports/v8-json.js +3 -5
  46. package/lib/reports/v8.js +3 -5
  47. package/lib/utils/dedupe.js +1 -1
  48. package/lib/utils/gc.js +30 -14
  49. package/lib/utils/markdown.js +2 -2
  50. package/lib/utils/merge/merge.js +2 -4
  51. package/lib/utils/merge/range-tree.js +1 -1
  52. package/lib/utils/request.js +85 -73
  53. package/lib/utils/snapshot.js +4 -4
  54. package/lib/utils/source-path.js +5 -5
  55. package/lib/utils/util.js +61 -41
  56. package/lib/v8/v8-summary.js +2 -4
  57. package/lib/v8/v8.js +11 -11
  58. package/migrate-to-3.md +175 -0
  59. package/package.json +153 -149
  60. package/skills/monocart-coverage-reports/SKILL.md +106 -0
  61. package/skills/monocart-coverage-reports/references/cli.md +108 -0
  62. package/skills/monocart-coverage-reports/references/collection.md +152 -0
  63. package/skills/monocart-coverage-reports/references/filtering-and-sourcemaps.md +170 -0
  64. package/skills/monocart-coverage-reports/references/reports-and-ci.md +219 -0
  65. package/skills/monocart-coverage-reports/references/repository-map.md +55 -0
  66. package/lib/index.mjs +0 -9
  67. package/lib/register/hooks.mjs +0 -3
  68. package/lib/register/register.mjs +0 -3
@@ -4,27 +4,29 @@
4
4
  * @author cenfun@gmail.com
5
5
  */
6
6
 
7
- const EC = require('eight-colors');
7
+ import EC from 'eight-colors';
8
8
 
9
- const { Locator } = require('monocart-locator');
10
- const Util = require('../utils/util.js');
9
+ import { Locator } from 'monocart-locator';
10
+ import { Util } from '../utils/util.js';
11
11
 
12
- const findOriginalRange = require('./find-original-range.js');
13
- const { getJsAstInfo, getCssAstInfo } = require('./ast.js');
14
- const { getIgnoredRanges } = require('./ignore.js');
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
- const { getUntestedList } = require('./untested.js');
18
+ import { getUntestedList } from './untested.js';
17
19
 
18
- const {
20
+ import {
19
21
  sortRanges, dedupeCountRanges, mergeRangesWith
20
- } = require('../utils/dedupe.js');
21
- const { getSourceType, initSourceMapSourcesPath } = require('../utils/source-path.js');
22
+ } from '../utils/dedupe.js';
23
+ import { getSourceType, initSourceMapSourcesPath } from '../utils/source-path.js';
22
24
 
23
- const { decode } = require('../packages/monocart-coverage-vendor.js');
25
+ import { decode } from '@jridgewell/sourcemap-codec';
24
26
 
25
- const InfoBranch = require('./info-branch.js');
26
- const InfoFunction = require('./info-function.js');
27
- const InfoStatement = require('./info-statement.js');
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
- // istanbul
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
- originalDecodedMap.get(sourceIndex).push(info);
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
- originalDecodedMap.get(sourceIndex).push(endColumn);
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
- fileSources[sourcePath] = source;
1802
- coverageData[sourcePath] = istanbulData;
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
- const convertV8List = async (v8list, options) => {
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
- module.exports = {
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
- const { diffSequence } = require('../packages/monocart-coverage-vendor.js');
2
- const Util = require('../utils/util.js');
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
- module.exports = findOriginalRange;
980
+ export { findOriginalRange };
@@ -1,4 +1,4 @@
1
- const { decode } = require('../packages/monocart-coverage-vendor.js');
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
- module.exports = {
185
- flattenSourceMaps
186
- };
184
+ export { flattenSourceMaps };
@@ -1,4 +1,4 @@
1
- const Util = require('../utils/util.js');
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
- module.exports = {
224
- getIgnoredRanges
225
- };
223
+ export { getIgnoredRanges };
@@ -1,5 +1,5 @@
1
- const Util = require('../utils/util.js');
2
- module.exports = class InfoBranch {
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
- const Util = require('../utils/util.js');
2
- module.exports = class InfoFunction {
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
- const Util = require('../utils/util.js');
2
- module.exports = class InfoStatement {
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 };