monocart-coverage-reports 2.13.2 → 3.1.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 +314 -116
- package/README.zh-Hans.md +315 -117
- 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 +29 -8
- package/lib/converter/collect-source-maps.js +42 -21
- package/lib/converter/converter.js +124 -129
- package/lib/converter/css-coverage.js +76 -0
- package/lib/converter/fake-source-coverage.js +64 -0
- package/lib/converter/find-original-range.js +214 -6
- 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 +30 -60
- 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/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 +8 -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 +26 -11
- package/lib/utils/util.js +67 -42
- 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 +54 -42
- package/skills/monocart-coverage-reports/SKILL.md +106 -0
- package/skills/monocart-coverage-reports/references/best-practices.md +218 -0
- package/skills/monocart-coverage-reports/references/cli.md +108 -0
- package/skills/monocart-coverage-reports/references/collection.md +154 -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 +59 -0
- package/lib/index.mjs +0 -9
- package/lib/register/hooks.mjs +0 -3
- package/lib/register/register.mjs +0 -3
|
@@ -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) => {
|
|
@@ -813,6 +813,44 @@ const checkSourceFileIndexes = (startIndexes, endIndexes) => {
|
|
|
813
813
|
|
|
814
814
|
};
|
|
815
815
|
|
|
816
|
+
const findMappingStartIndex = (mappings, offset) => {
|
|
817
|
+
let low = 0;
|
|
818
|
+
let high = mappings.length;
|
|
819
|
+
while (low < high) {
|
|
820
|
+
const mid = Math.floor((low + high) / 2);
|
|
821
|
+
if (mappings[mid].generatedOffset < offset) {
|
|
822
|
+
low = mid + 1;
|
|
823
|
+
} else {
|
|
824
|
+
high = mid;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return low;
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
const isFilteredRange = (start, end, mappingInfo, state) => {
|
|
831
|
+
const filtered = state.filteredSourceIndexes;
|
|
832
|
+
if (state.js || !filtered?.size) {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
// A mapping after the exclusive end can belong to the next rule. Only
|
|
836
|
+
// inspect the active start segment and mappings inside this range.
|
|
837
|
+
const [startMapping] = [].concat(mappingInfo.startMappings);
|
|
838
|
+
if (!filtered.has(startMapping.sourceIndex)) {
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const mappings = state.decodedMappings;
|
|
843
|
+
const low = findMappingStartIndex(mappings, start);
|
|
844
|
+
// Excluded endpoints alone are not enough: a retained source may have
|
|
845
|
+
// mappings in the middle of a rule that combines multiple source files.
|
|
846
|
+
for (let i = low; i < mappings.length && mappings[i].generatedOffset < end; i++) {
|
|
847
|
+
if (!filtered.has(mappings[i].sourceIndex)) {
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return true;
|
|
852
|
+
};
|
|
853
|
+
|
|
816
854
|
const getOriginalState = (start, end, state, originalMap) => {
|
|
817
855
|
|
|
818
856
|
const mappingInfo = getMappingInfo(start, end, state);
|
|
@@ -829,6 +867,7 @@ const getOriginalState = (start, end, state, originalMap) => {
|
|
|
829
867
|
if (!results) {
|
|
830
868
|
return {
|
|
831
869
|
error: true,
|
|
870
|
+
filtered: isFilteredRange(start, end, mappingInfo, state),
|
|
832
871
|
errors: [`invalid source indexes: ${EC.yellow(`${startIndexes} ~ ${endIndexes}`)}`]
|
|
833
872
|
};
|
|
834
873
|
}
|
|
@@ -845,6 +884,7 @@ const getOriginalState = (start, end, state, originalMap) => {
|
|
|
845
884
|
if (!originalState) {
|
|
846
885
|
return {
|
|
847
886
|
error: true,
|
|
887
|
+
filtered: state.filteredSourceIndexes?.has(sourceIndex) && isFilteredRange(start, end, mappingInfo, state),
|
|
848
888
|
errors: [`not found original file: ${EC.yellow(sourceIndex)}`]
|
|
849
889
|
};
|
|
850
890
|
}
|
|
@@ -904,7 +944,167 @@ const getMappingInfo = (start, end, state) => {
|
|
|
904
944
|
};
|
|
905
945
|
|
|
906
946
|
|
|
907
|
-
|
|
947
|
+
// CSS uses the shared mapping lookup, but owns its endpoint repair and validation.
|
|
948
|
+
const getCssOriginalEndPosition = (cache, state) => {
|
|
949
|
+
const { end, endMappings } = cache;
|
|
950
|
+
if (!Array.isArray(endMappings)) {
|
|
951
|
+
// Synthetic line ends are already exclusive, unlike real token mappings.
|
|
952
|
+
if (endMappings.end) {
|
|
953
|
+
return {
|
|
954
|
+
originalEnd: endMappings.originalOffset
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
return getOriginalEndPosition(cache, state);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const exclusiveMappings = findMapping(state.decodedMappings, end);
|
|
961
|
+
if (Array.isArray(exclusiveMappings) && !cache.crossEnd) {
|
|
962
|
+
const [mapping] = exclusiveMappings;
|
|
963
|
+
if (mapping.end && mapping.generatedOffset === end - 1) {
|
|
964
|
+
return {
|
|
965
|
+
originalEnd: mapping.originalOffset
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return getOriginalEndPosition(cache, state);
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
const isValidCssOriginalRange = (start, end, length) => Number.isInteger(start) && Number.isInteger(end)
|
|
973
|
+
&& start >= 0 && start < end && end <= length;
|
|
974
|
+
|
|
975
|
+
const getCssRangeMappingError = (start, end, state, cache) => {
|
|
976
|
+
// Matching endpoints do not guarantee a contiguous rule: a Sass mixin
|
|
977
|
+
// body can map to another file or an earlier location in the same file.
|
|
978
|
+
const mappings = state.decodedMappings;
|
|
979
|
+
const low = findMappingStartIndex(mappings, start);
|
|
980
|
+
for (let i = low; i < mappings.length && mappings[i].generatedOffset < end; i++) {
|
|
981
|
+
const mapping = mappings[i];
|
|
982
|
+
if (mapping.sourceIndex !== cache.sourceIndex) {
|
|
983
|
+
return {
|
|
984
|
+
error: true,
|
|
985
|
+
errors: ['cross-source CSS range']
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
// Synthetic line ends describe whole lines, not actual mapped tokens.
|
|
989
|
+
// Do not validate against the inferred end: indentation-based sources
|
|
990
|
+
// can map a generated closing brace back to the selector's line end.
|
|
991
|
+
if (!mapping.end && mapping.originalOffset < cache.originalStart) {
|
|
992
|
+
return {
|
|
993
|
+
error: true,
|
|
994
|
+
errors: ['mapped token before original CSS range']
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
const mapCssOriginalRange = (start, end, state, originalMap, options) => {
|
|
1001
|
+
const cache = getOriginalState(start, end, state, originalMap);
|
|
1002
|
+
if (cache.error) {
|
|
1003
|
+
return cache;
|
|
1004
|
+
}
|
|
1005
|
+
const { originalStart, originalName } = getOriginalStartPosition(cache, state, options);
|
|
1006
|
+
cache.originalStart = originalStart;
|
|
1007
|
+
const mappingError = getCssRangeMappingError(start, end, state, cache);
|
|
1008
|
+
if (mappingError) {
|
|
1009
|
+
return mappingError;
|
|
1010
|
+
}
|
|
1011
|
+
const endResult = getCssOriginalEndPosition(cache, state);
|
|
1012
|
+
if (endResult.error) {
|
|
1013
|
+
return endResult;
|
|
1014
|
+
}
|
|
1015
|
+
const { originalEnd } = endResult;
|
|
1016
|
+
const { originalState } = cache;
|
|
1017
|
+
if (!isValidCssOriginalRange(originalStart, originalEnd, originalState.source.length)) {
|
|
1018
|
+
return {
|
|
1019
|
+
error: true,
|
|
1020
|
+
errors: [`invalid original range: ${EC.yellow(`${originalStart} ~ ${originalEnd}`)}`]
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
if (originalState.locator.lineParser.commentParser.isComment(originalStart, originalEnd)) {
|
|
1024
|
+
return {
|
|
1025
|
+
error: true,
|
|
1026
|
+
errors: ['the range in a original comment']
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
return {
|
|
1030
|
+
start: originalStart,
|
|
1031
|
+
end: originalEnd,
|
|
1032
|
+
name: originalName,
|
|
1033
|
+
originalState
|
|
1034
|
+
};
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
const skipCssTrivia = (originalState, offset, direction) => {
|
|
1038
|
+
const { source, locator } = originalState;
|
|
1039
|
+
const { commentParser } = locator.lineParser;
|
|
1040
|
+
while (offset >= 0 && offset < source.length) {
|
|
1041
|
+
if ((/\s/).test(source[offset])) {
|
|
1042
|
+
offset += direction;
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
if (commentParser.isComment(offset, offset + 1)) {
|
|
1046
|
+
const comment = commentParser.findComment(offset);
|
|
1047
|
+
offset = direction > 0 ? comment.end : comment.start - 1;
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
return offset;
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
const fixCssOriginalRange = (result, state, end) => {
|
|
1056
|
+
if (result.error || result.filtered || state.source[end - 1] !== '}') {
|
|
1057
|
+
return result;
|
|
1058
|
+
}
|
|
1059
|
+
const { originalState } = result;
|
|
1060
|
+
const { source } = originalState;
|
|
1061
|
+
// A synthetic endpoint can include whitespace or comments after a brace
|
|
1062
|
+
// already mapped. Do not mistake the next enclosing brace for this rule's.
|
|
1063
|
+
const previousToken = skipCssTrivia(originalState, result.end - 1, -1);
|
|
1064
|
+
if (source[previousToken] === '}') {
|
|
1065
|
+
return result;
|
|
1066
|
+
}
|
|
1067
|
+
// Sass can omit both closing-brace mappings and intervening line comments.
|
|
1068
|
+
// Extend only when the next non-comment, non-whitespace token is a brace.
|
|
1069
|
+
const nextToken = skipCssTrivia(originalState, result.end, 1);
|
|
1070
|
+
if (source[nextToken] === '}') {
|
|
1071
|
+
return {
|
|
1072
|
+
... result, end: nextToken + 1
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
return result;
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
const findCssOriginalRange = (start, end, state, originalMap, options) => {
|
|
1079
|
+
const { sourcePath, rangeCache } = state;
|
|
1080
|
+
const key = `${start}_${end}_${Boolean(options.fixOriginalRange)}`;
|
|
1081
|
+
if (rangeCache.has(key)) {
|
|
1082
|
+
return rangeCache.get(key);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
let result = mapCssOriginalRange(start, end, state, originalMap, options);
|
|
1086
|
+
if (options.fixOriginalRange) {
|
|
1087
|
+
// A closing brace may map to a mixin caller in another source. Retry the
|
|
1088
|
+
// body when necessary, then repair using the full generated rule end.
|
|
1089
|
+
if (result.error && !result.filtered && state.source[end - 1] === '}') {
|
|
1090
|
+
result = mapCssOriginalRange(start, end - 1, state, originalMap, options);
|
|
1091
|
+
}
|
|
1092
|
+
result = fixCssOriginalRange(result, state, end);
|
|
1093
|
+
}
|
|
1094
|
+
if (result.error) {
|
|
1095
|
+
result = {
|
|
1096
|
+
... result, start, end, sourcePath
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Cache the final CSS range, error or deliberate exclusion.
|
|
1101
|
+
rangeCache.set(key, result);
|
|
1102
|
+
return result;
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
// ========================================================================================================
|
|
1106
|
+
|
|
1107
|
+
const findJsOriginalRange = (start, end, state, originalMap, options = {}) => {
|
|
908
1108
|
|
|
909
1109
|
const { sourcePath, rangeCache } = state;
|
|
910
1110
|
|
|
@@ -913,9 +1113,10 @@ const findOriginalRange = (start, end, state, originalMap, options = {}) => {
|
|
|
913
1113
|
return rangeCache.get(key);
|
|
914
1114
|
}
|
|
915
1115
|
|
|
916
|
-
const createMappingError = (errors) => {
|
|
1116
|
+
const createMappingError = (errors, filtered) => {
|
|
917
1117
|
const res = {
|
|
918
1118
|
error: true,
|
|
1119
|
+
filtered,
|
|
919
1120
|
start,
|
|
920
1121
|
end,
|
|
921
1122
|
sourcePath,
|
|
@@ -930,7 +1131,7 @@ const findOriginalRange = (start, end, state, originalMap, options = {}) => {
|
|
|
930
1131
|
|
|
931
1132
|
const cache = getOriginalState(start, end, state, originalMap);
|
|
932
1133
|
if (cache.error) {
|
|
933
|
-
return createMappingError(cache.errors);
|
|
1134
|
+
return createMappingError(cache.errors, cache.filtered);
|
|
934
1135
|
}
|
|
935
1136
|
|
|
936
1137
|
const originalStartResult = getOriginalStartPosition(cache, state, options);
|
|
@@ -977,4 +1178,11 @@ const findOriginalRange = (start, end, state, originalMap, options = {}) => {
|
|
|
977
1178
|
|
|
978
1179
|
};
|
|
979
1180
|
|
|
980
|
-
|
|
1181
|
+
const findOriginalRange = (start, end, state, originalMap, options = {}) => {
|
|
1182
|
+
if (state.js) {
|
|
1183
|
+
return findJsOriginalRange(start, end, state, originalMap, options);
|
|
1184
|
+
}
|
|
1185
|
+
return findCssOriginalRange(start, end, state, originalMap, options);
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
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 };
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Util } from '../utils/util.js';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import {
|
|
8
|
+
normalizeSourcePath, getSourcePathReplacer, getSourceType, cssTypes
|
|
9
|
+
} from '../utils/source-path.js';
|
|
8
10
|
|
|
9
11
|
// ========================================================================================================
|
|
10
12
|
|
|
@@ -20,41 +22,7 @@ const resolveAllDirList = (input) => {
|
|
|
20
22
|
return dirList;
|
|
21
23
|
};
|
|
22
24
|
|
|
23
|
-
const resolveAllFilter = (input) =>
|
|
24
|
-
// for function handler
|
|
25
|
-
if (typeof input === 'function') {
|
|
26
|
-
return input;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// for single minimatch pattern
|
|
30
|
-
if (input && typeof input === 'string') {
|
|
31
|
-
// string to multiple patterns "{...}"
|
|
32
|
-
const obj = Util.strToObj(input);
|
|
33
|
-
if (obj) {
|
|
34
|
-
input = obj;
|
|
35
|
-
} else {
|
|
36
|
-
return (filePath) => {
|
|
37
|
-
return Util.betterMinimatch(filePath, input);
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// for patterns
|
|
43
|
-
if (input && typeof input === 'object') {
|
|
44
|
-
const patterns = Object.keys(input);
|
|
45
|
-
return (filePath) => {
|
|
46
|
-
for (const pattern of patterns) {
|
|
47
|
-
if (Util.betterMinimatch(filePath, pattern)) {
|
|
48
|
-
return input[pattern];
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
// false if not matched
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// default
|
|
56
|
-
return () => true;
|
|
57
|
-
};
|
|
25
|
+
const resolveAllFilter = (input) => Util.createFilterHandler(input, (filePath) => filePath);
|
|
58
26
|
|
|
59
27
|
const resolveAllOptions = (input) => {
|
|
60
28
|
if (!input) {
|
|
@@ -101,11 +69,7 @@ const resolveFileType = (fileType, filePath) => {
|
|
|
101
69
|
if (fileType === 'js' || fileType === 'css') {
|
|
102
70
|
return fileType;
|
|
103
71
|
}
|
|
104
|
-
|
|
105
|
-
if (['.css', '.scss', '.sass', '.less'].includes(extname)) {
|
|
106
|
-
return 'css';
|
|
107
|
-
}
|
|
108
|
-
return 'js';
|
|
72
|
+
return cssTypes.has(getSourceType(filePath)) ? 'css' : 'js';
|
|
109
73
|
};
|
|
110
74
|
|
|
111
75
|
const saveUntestedFileSource = async (entryFile, options) => {
|
|
@@ -204,9 +168,22 @@ const getUntestedCoverageData = async (entryList, options, coverageType) => {
|
|
|
204
168
|
return emptyCoverageList;
|
|
205
169
|
};
|
|
206
170
|
|
|
171
|
+
const resolveUntestedPath = (entry, baseDir, sourcePathReplacer) => {
|
|
172
|
+
const sourcePath = normalizeSourcePath(entry.url, baseDir);
|
|
173
|
+
if (sourcePathReplacer) {
|
|
174
|
+
const replacement = sourcePathReplacer(sourcePath, entry);
|
|
175
|
+
if (typeof replacement === 'string' && replacement) {
|
|
176
|
+
return replacement;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return sourcePath;
|
|
180
|
+
};
|
|
181
|
+
|
|
207
182
|
const getEmptyCoverages = async (testedMap, options, coverageType, fileList, fileTransformer) => {
|
|
208
183
|
const sourceFilter = Util.getSourceFilter(options);
|
|
209
184
|
const sourcePathReplacer = getSourcePathReplacer(options);
|
|
185
|
+
// User callbacks may inspect or mutate entry.source; object replacements cannot.
|
|
186
|
+
const needsSourceForPath = typeof options.sourcePath === 'function';
|
|
210
187
|
const baseDir = options.baseDir;
|
|
211
188
|
|
|
212
189
|
// console.log(fileList);
|
|
@@ -217,24 +194,16 @@ const getEmptyCoverages = async (testedMap, options, coverageType, fileList, fil
|
|
|
217
194
|
const { fileType, filePath } = item;
|
|
218
195
|
const type = resolveFileType(fileType, filePath);
|
|
219
196
|
const url = pathToFileURL(filePath).toString();
|
|
220
|
-
const source = Util.readFileSync(filePath);
|
|
221
197
|
const entry = {
|
|
222
198
|
empty: true,
|
|
223
199
|
type,
|
|
224
|
-
url
|
|
225
|
-
source
|
|
200
|
+
url
|
|
226
201
|
};
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
let sourcePath = normalizeSourcePath(url, baseDir);
|
|
230
|
-
if (sourcePathReplacer) {
|
|
231
|
-
const newSourcePath = sourcePathReplacer(sourcePath, entry);
|
|
232
|
-
if (typeof newSourcePath === 'string' && newSourcePath) {
|
|
233
|
-
sourcePath = newSourcePath;
|
|
234
|
-
}
|
|
202
|
+
if (needsSourceForPath) {
|
|
203
|
+
entry.source = Util.readFileSync(filePath);
|
|
235
204
|
}
|
|
236
205
|
|
|
237
|
-
|
|
206
|
+
const sourcePath = resolveUntestedPath(entry, baseDir, sourcePathReplacer);
|
|
238
207
|
|
|
239
208
|
if (testedMap.has(sourcePath)) {
|
|
240
209
|
continue;
|
|
@@ -244,6 +213,9 @@ const getEmptyCoverages = async (testedMap, options, coverageType, fileList, fil
|
|
|
244
213
|
continue;
|
|
245
214
|
}
|
|
246
215
|
|
|
216
|
+
if (!needsSourceForPath) {
|
|
217
|
+
entry.source = Util.readFileSync(filePath);
|
|
218
|
+
}
|
|
247
219
|
entry.sourcePath = sourcePath;
|
|
248
220
|
|
|
249
221
|
await fileTransformer(entry, coverageType);
|
|
@@ -302,6 +274,4 @@ const getUntestedList = (testedMap, options, coverageType = 'v8') => {
|
|
|
302
274
|
};
|
|
303
275
|
|
|
304
276
|
|
|
305
|
-
|
|
306
|
-
getUntestedList
|
|
307
|
-
};
|
|
277
|
+
export { getUntestedList };
|