@t09tanaka/test-gates 0.4.1 → 0.4.3
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/CHANGELOG.md +69 -0
- package/README.md +66 -19
- package/dist/cli.js +2392 -786
- package/dist/index.cjs +61 -44
- package/dist/index.d.cts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +61 -44
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -691,14 +691,17 @@ function parseImports(source) {
|
|
|
691
691
|
const clause = match[3];
|
|
692
692
|
const module2 = match[5];
|
|
693
693
|
const braces = /\{([^}]*)\}/.exec(clause);
|
|
694
|
-
const
|
|
694
|
+
const entries = braces ? braces[1].split(",").map((part) => part.trim()).filter((part) => part !== "") : [];
|
|
695
|
+
const names = entries.filter((part) => !/^type\s/.test(part)).map((part) => part.split(/\s+as\s+/)[0].trim());
|
|
695
696
|
const outsideBraces = clause.replace(/\{[^}]*\}/, "").replace(/,/g, "").trim();
|
|
696
697
|
found.push({
|
|
697
698
|
module: module2,
|
|
698
699
|
line: lineAt(source, match.index + match[0].lastIndexOf(module2)),
|
|
699
700
|
typeOnly: match[2] !== void 0,
|
|
700
701
|
names,
|
|
701
|
-
whole: outsideBraces !== ""
|
|
702
|
+
whole: outsideBraces !== "",
|
|
703
|
+
...match[2] === void 0 && outsideBraces === "" && entries.length > 0 && // `type as kind` imports the value named `type`.
|
|
704
|
+
entries.every((part) => /^type\s+(?!as\s)[\w$]/.test(part)) ? { inlineTypes: true } : {}
|
|
702
705
|
});
|
|
703
706
|
}
|
|
704
707
|
for (const match of source.matchAll(SIDE_EFFECT_IMPORT)) {
|
|
@@ -713,6 +716,9 @@ function parseImports(source) {
|
|
|
713
716
|
}
|
|
714
717
|
for (const match of source.matchAll(CALL_IMPORT)) {
|
|
715
718
|
const module2 = match[2];
|
|
719
|
+
if (match[1] === "`" && module2.includes("${")) {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
716
722
|
found.push({
|
|
717
723
|
module: module2,
|
|
718
724
|
line: lineAt(source, match.index + match[0].lastIndexOf(module2)),
|
|
@@ -723,7 +729,7 @@ function parseImports(source) {
|
|
|
723
729
|
}
|
|
724
730
|
return found.sort((a, b) => a.line - b.line);
|
|
725
731
|
}
|
|
726
|
-
var COMPUTED_CALL_IMPORT = /(?<![.\w$])(?:require|import)\s*\(
|
|
732
|
+
var COMPUTED_CALL_IMPORT = /(?<![.\w$])(?:require|import)\s*\((?!\s*(?:\)|(?:'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"|`(?:[^`\\$]|\\.|\$(?!\{))*`)\s*\)))/g;
|
|
727
733
|
function findUncheckableImports(source) {
|
|
728
734
|
return [...source.matchAll(COMPUTED_CALL_IMPORT)].map((match) => lineAt(source, match.index));
|
|
729
735
|
}
|
|
@@ -750,7 +756,11 @@ function resolveSpecifier(specifier, fromFile, aliases) {
|
|
|
750
756
|
const segments = [];
|
|
751
757
|
for (const segment of joined.split("/")) {
|
|
752
758
|
if (segment === "..") {
|
|
753
|
-
segments.
|
|
759
|
+
if (segments.length === 0 || segments[segments.length - 1] === "..") {
|
|
760
|
+
segments.push(segment);
|
|
761
|
+
} else {
|
|
762
|
+
segments.pop();
|
|
763
|
+
}
|
|
754
764
|
} else if (segment !== "." && segment !== "") {
|
|
755
765
|
segments.push(segment);
|
|
756
766
|
}
|
|
@@ -1121,6 +1131,41 @@ function sliceByLocation(source, location) {
|
|
|
1121
1131
|
function keyOf(mutator, original, replacement) {
|
|
1122
1132
|
return JSON.stringify([mutator, normalizeCode(original), normalizeCode(replacement)]);
|
|
1123
1133
|
}
|
|
1134
|
+
function locateMutants(source, mutants) {
|
|
1135
|
+
const located = [];
|
|
1136
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1137
|
+
const outside = [];
|
|
1138
|
+
const sorted = [...mutants].sort(
|
|
1139
|
+
(a, b) => a.location.start.line - b.location.start.line || a.location.start.column - b.location.start.column
|
|
1140
|
+
);
|
|
1141
|
+
for (const mutant of sorted) {
|
|
1142
|
+
const original = sliceByLocation(normalizeNewlines(source), mutant.location);
|
|
1143
|
+
if (original === null) {
|
|
1144
|
+
outside.push(mutant);
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
const replacement = mutant.replacement ?? "";
|
|
1148
|
+
const key = keyOf(mutant.mutatorName, original, replacement);
|
|
1149
|
+
const group = byKey.get(key) ?? [];
|
|
1150
|
+
const entry = { mutant, original, replacement, occurrence: group.length + 1, sameKeyCount: 0 };
|
|
1151
|
+
group.push(entry);
|
|
1152
|
+
byKey.set(key, group);
|
|
1153
|
+
located.push(entry);
|
|
1154
|
+
}
|
|
1155
|
+
for (const group of byKey.values()) {
|
|
1156
|
+
for (const entry of group) {
|
|
1157
|
+
entry.sameKeyCount = group.length;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return { located, byKey, outside };
|
|
1161
|
+
}
|
|
1162
|
+
function resolveEntry(entry, byKey) {
|
|
1163
|
+
const group = byKey.get(keyOf(entry.mutator, entry.original, entry.replacement)) ?? [];
|
|
1164
|
+
if (entry.occurrence === void 0 && group.length > 1) {
|
|
1165
|
+
return { ambiguous: group.length };
|
|
1166
|
+
}
|
|
1167
|
+
return { target: group[(entry.occurrence ?? 1) - 1] };
|
|
1168
|
+
}
|
|
1124
1169
|
function describeMutant(file, entry) {
|
|
1125
1170
|
const { mutant } = entry;
|
|
1126
1171
|
return {
|
|
@@ -1194,43 +1239,15 @@ function judgeMutationReport(input) {
|
|
|
1194
1239
|
});
|
|
1195
1240
|
continue;
|
|
1196
1241
|
}
|
|
1197
|
-
const located =
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
const original = sliceByLocation(normalizeNewlines(source), mutant.location);
|
|
1205
|
-
if (original === null) {
|
|
1206
|
-
violations.push({
|
|
1207
|
-
file: gate.path,
|
|
1208
|
-
line: mutant.location.start.line,
|
|
1209
|
-
message: `the report location of a ${mutant.mutatorName} mutant is outside the source`
|
|
1210
|
-
});
|
|
1211
|
-
locationsValid = false;
|
|
1212
|
-
continue;
|
|
1213
|
-
}
|
|
1214
|
-
const replacement = mutant.replacement ?? "";
|
|
1215
|
-
const key = keyOf(mutant.mutatorName, original, replacement);
|
|
1216
|
-
const group = byKey.get(key) ?? [];
|
|
1217
|
-
const entry = {
|
|
1218
|
-
mutant,
|
|
1219
|
-
original,
|
|
1220
|
-
replacement,
|
|
1221
|
-
occurrence: group.length + 1,
|
|
1222
|
-
sameKeyCount: 0
|
|
1223
|
-
};
|
|
1224
|
-
group.push(entry);
|
|
1225
|
-
byKey.set(key, group);
|
|
1226
|
-
located.push(entry);
|
|
1227
|
-
}
|
|
1228
|
-
for (const group of byKey.values()) {
|
|
1229
|
-
for (const entry of group) {
|
|
1230
|
-
entry.sameKeyCount = group.length;
|
|
1231
|
-
}
|
|
1242
|
+
const { located, byKey, outside } = locateMutants(source, result.mutants);
|
|
1243
|
+
for (const mutant of outside) {
|
|
1244
|
+
violations.push({
|
|
1245
|
+
file: gate.path,
|
|
1246
|
+
line: mutant.location.start.line,
|
|
1247
|
+
message: `the report location of a ${mutant.mutatorName} mutant is outside the source`
|
|
1248
|
+
});
|
|
1232
1249
|
}
|
|
1233
|
-
if (
|
|
1250
|
+
if (outside.length > 0) {
|
|
1234
1251
|
continue;
|
|
1235
1252
|
}
|
|
1236
1253
|
const claim = (field, entries, noun, accept) => {
|
|
@@ -1243,15 +1260,15 @@ function judgeMutationReport(input) {
|
|
|
1243
1260
|
return;
|
|
1244
1261
|
}
|
|
1245
1262
|
const at = `${gate.path}: ${field}[${index}] (${describeAllowance(entry)})`;
|
|
1246
|
-
const
|
|
1247
|
-
if (
|
|
1263
|
+
const found = resolveEntry(entry, byKey);
|
|
1264
|
+
if ("ambiguous" in found) {
|
|
1248
1265
|
violations.push({
|
|
1249
1266
|
file: MANIFEST_FILE,
|
|
1250
|
-
message: `${at}: matches ${
|
|
1267
|
+
message: `${at}: matches ${found.ambiguous} mutants in the file. Add "occurrence" (1-based) to pick one`
|
|
1251
1268
|
});
|
|
1252
1269
|
return;
|
|
1253
1270
|
}
|
|
1254
|
-
const target =
|
|
1271
|
+
const target = found.target;
|
|
1255
1272
|
const refusal = target ? accept(target.mutant.status) : `stale ${noun} (no such mutant in this run). Remove it`;
|
|
1256
1273
|
if (!target || refusal !== null) {
|
|
1257
1274
|
violations.push({ file: MANIFEST_FILE, message: `${at}: ${refusal}` });
|
package/dist/index.d.cts
CHANGED
|
@@ -247,11 +247,17 @@ interface MutationReport {
|
|
|
247
247
|
files?: Record<string, ReportFile>;
|
|
248
248
|
/** The spec files, with the ids of their tests (read by `mutation --affected`). */
|
|
249
249
|
testFiles?: Record<string, {
|
|
250
|
-
tests?:
|
|
251
|
-
id: string;
|
|
252
|
-
}>;
|
|
250
|
+
tests?: ReportTest[];
|
|
253
251
|
}>;
|
|
254
252
|
}
|
|
253
|
+
/** A test of a spec file in the report. Stryker numbers them anew on every run. */
|
|
254
|
+
interface ReportTest {
|
|
255
|
+
id: string;
|
|
256
|
+
name?: string;
|
|
257
|
+
location?: {
|
|
258
|
+
start: Position;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
255
261
|
/** A survivor that no allowance covers. `allowance` can be pasted into `equivalentMutants`. */
|
|
256
262
|
interface UnallowedSurvivor {
|
|
257
263
|
file: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -247,11 +247,17 @@ interface MutationReport {
|
|
|
247
247
|
files?: Record<string, ReportFile>;
|
|
248
248
|
/** The spec files, with the ids of their tests (read by `mutation --affected`). */
|
|
249
249
|
testFiles?: Record<string, {
|
|
250
|
-
tests?:
|
|
251
|
-
id: string;
|
|
252
|
-
}>;
|
|
250
|
+
tests?: ReportTest[];
|
|
253
251
|
}>;
|
|
254
252
|
}
|
|
253
|
+
/** A test of a spec file in the report. Stryker numbers them anew on every run. */
|
|
254
|
+
interface ReportTest {
|
|
255
|
+
id: string;
|
|
256
|
+
name?: string;
|
|
257
|
+
location?: {
|
|
258
|
+
start: Position;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
255
261
|
/** A survivor that no allowance covers. `allowance` can be pasted into `equivalentMutants`. */
|
|
256
262
|
interface UnallowedSurvivor {
|
|
257
263
|
file: string;
|
package/dist/index.js
CHANGED
|
@@ -45,14 +45,17 @@ function parseImports(source) {
|
|
|
45
45
|
const clause = match[3];
|
|
46
46
|
const module = match[5];
|
|
47
47
|
const braces = /\{([^}]*)\}/.exec(clause);
|
|
48
|
-
const
|
|
48
|
+
const entries = braces ? braces[1].split(",").map((part) => part.trim()).filter((part) => part !== "") : [];
|
|
49
|
+
const names = entries.filter((part) => !/^type\s/.test(part)).map((part) => part.split(/\s+as\s+/)[0].trim());
|
|
49
50
|
const outsideBraces = clause.replace(/\{[^}]*\}/, "").replace(/,/g, "").trim();
|
|
50
51
|
found.push({
|
|
51
52
|
module,
|
|
52
53
|
line: lineAt(source, match.index + match[0].lastIndexOf(module)),
|
|
53
54
|
typeOnly: match[2] !== void 0,
|
|
54
55
|
names,
|
|
55
|
-
whole: outsideBraces !== ""
|
|
56
|
+
whole: outsideBraces !== "",
|
|
57
|
+
...match[2] === void 0 && outsideBraces === "" && entries.length > 0 && // `type as kind` imports the value named `type`.
|
|
58
|
+
entries.every((part) => /^type\s+(?!as\s)[\w$]/.test(part)) ? { inlineTypes: true } : {}
|
|
56
59
|
});
|
|
57
60
|
}
|
|
58
61
|
for (const match of source.matchAll(SIDE_EFFECT_IMPORT)) {
|
|
@@ -67,6 +70,9 @@ function parseImports(source) {
|
|
|
67
70
|
}
|
|
68
71
|
for (const match of source.matchAll(CALL_IMPORT)) {
|
|
69
72
|
const module = match[2];
|
|
73
|
+
if (match[1] === "`" && module.includes("${")) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
70
76
|
found.push({
|
|
71
77
|
module,
|
|
72
78
|
line: lineAt(source, match.index + match[0].lastIndexOf(module)),
|
|
@@ -77,7 +83,7 @@ function parseImports(source) {
|
|
|
77
83
|
}
|
|
78
84
|
return found.sort((a, b) => a.line - b.line);
|
|
79
85
|
}
|
|
80
|
-
var COMPUTED_CALL_IMPORT = /(?<![.\w$])(?:require|import)\s*\(
|
|
86
|
+
var COMPUTED_CALL_IMPORT = /(?<![.\w$])(?:require|import)\s*\((?!\s*(?:\)|(?:'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"|`(?:[^`\\$]|\\.|\$(?!\{))*`)\s*\)))/g;
|
|
81
87
|
function findUncheckableImports(source) {
|
|
82
88
|
return [...source.matchAll(COMPUTED_CALL_IMPORT)].map((match) => lineAt(source, match.index));
|
|
83
89
|
}
|
|
@@ -104,7 +110,11 @@ function resolveSpecifier(specifier, fromFile, aliases) {
|
|
|
104
110
|
const segments = [];
|
|
105
111
|
for (const segment of joined.split("/")) {
|
|
106
112
|
if (segment === "..") {
|
|
107
|
-
segments.
|
|
113
|
+
if (segments.length === 0 || segments[segments.length - 1] === "..") {
|
|
114
|
+
segments.push(segment);
|
|
115
|
+
} else {
|
|
116
|
+
segments.pop();
|
|
117
|
+
}
|
|
108
118
|
} else if (segment !== "." && segment !== "") {
|
|
109
119
|
segments.push(segment);
|
|
110
120
|
}
|
|
@@ -475,6 +485,41 @@ function sliceByLocation(source, location) {
|
|
|
475
485
|
function keyOf(mutator, original, replacement) {
|
|
476
486
|
return JSON.stringify([mutator, normalizeCode(original), normalizeCode(replacement)]);
|
|
477
487
|
}
|
|
488
|
+
function locateMutants(source, mutants) {
|
|
489
|
+
const located = [];
|
|
490
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
491
|
+
const outside = [];
|
|
492
|
+
const sorted = [...mutants].sort(
|
|
493
|
+
(a, b) => a.location.start.line - b.location.start.line || a.location.start.column - b.location.start.column
|
|
494
|
+
);
|
|
495
|
+
for (const mutant of sorted) {
|
|
496
|
+
const original = sliceByLocation(normalizeNewlines(source), mutant.location);
|
|
497
|
+
if (original === null) {
|
|
498
|
+
outside.push(mutant);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const replacement = mutant.replacement ?? "";
|
|
502
|
+
const key = keyOf(mutant.mutatorName, original, replacement);
|
|
503
|
+
const group = byKey.get(key) ?? [];
|
|
504
|
+
const entry = { mutant, original, replacement, occurrence: group.length + 1, sameKeyCount: 0 };
|
|
505
|
+
group.push(entry);
|
|
506
|
+
byKey.set(key, group);
|
|
507
|
+
located.push(entry);
|
|
508
|
+
}
|
|
509
|
+
for (const group of byKey.values()) {
|
|
510
|
+
for (const entry of group) {
|
|
511
|
+
entry.sameKeyCount = group.length;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return { located, byKey, outside };
|
|
515
|
+
}
|
|
516
|
+
function resolveEntry(entry, byKey) {
|
|
517
|
+
const group = byKey.get(keyOf(entry.mutator, entry.original, entry.replacement)) ?? [];
|
|
518
|
+
if (entry.occurrence === void 0 && group.length > 1) {
|
|
519
|
+
return { ambiguous: group.length };
|
|
520
|
+
}
|
|
521
|
+
return { target: group[(entry.occurrence ?? 1) - 1] };
|
|
522
|
+
}
|
|
478
523
|
function describeMutant(file, entry) {
|
|
479
524
|
const { mutant } = entry;
|
|
480
525
|
return {
|
|
@@ -548,43 +593,15 @@ function judgeMutationReport(input) {
|
|
|
548
593
|
});
|
|
549
594
|
continue;
|
|
550
595
|
}
|
|
551
|
-
const located =
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const original = sliceByLocation(normalizeNewlines(source), mutant.location);
|
|
559
|
-
if (original === null) {
|
|
560
|
-
violations.push({
|
|
561
|
-
file: gate.path,
|
|
562
|
-
line: mutant.location.start.line,
|
|
563
|
-
message: `the report location of a ${mutant.mutatorName} mutant is outside the source`
|
|
564
|
-
});
|
|
565
|
-
locationsValid = false;
|
|
566
|
-
continue;
|
|
567
|
-
}
|
|
568
|
-
const replacement = mutant.replacement ?? "";
|
|
569
|
-
const key = keyOf(mutant.mutatorName, original, replacement);
|
|
570
|
-
const group = byKey.get(key) ?? [];
|
|
571
|
-
const entry = {
|
|
572
|
-
mutant,
|
|
573
|
-
original,
|
|
574
|
-
replacement,
|
|
575
|
-
occurrence: group.length + 1,
|
|
576
|
-
sameKeyCount: 0
|
|
577
|
-
};
|
|
578
|
-
group.push(entry);
|
|
579
|
-
byKey.set(key, group);
|
|
580
|
-
located.push(entry);
|
|
581
|
-
}
|
|
582
|
-
for (const group of byKey.values()) {
|
|
583
|
-
for (const entry of group) {
|
|
584
|
-
entry.sameKeyCount = group.length;
|
|
585
|
-
}
|
|
596
|
+
const { located, byKey, outside } = locateMutants(source, result.mutants);
|
|
597
|
+
for (const mutant of outside) {
|
|
598
|
+
violations.push({
|
|
599
|
+
file: gate.path,
|
|
600
|
+
line: mutant.location.start.line,
|
|
601
|
+
message: `the report location of a ${mutant.mutatorName} mutant is outside the source`
|
|
602
|
+
});
|
|
586
603
|
}
|
|
587
|
-
if (
|
|
604
|
+
if (outside.length > 0) {
|
|
588
605
|
continue;
|
|
589
606
|
}
|
|
590
607
|
const claim = (field, entries, noun, accept) => {
|
|
@@ -597,15 +614,15 @@ function judgeMutationReport(input) {
|
|
|
597
614
|
return;
|
|
598
615
|
}
|
|
599
616
|
const at = `${gate.path}: ${field}[${index}] (${describeAllowance(entry)})`;
|
|
600
|
-
const
|
|
601
|
-
if (
|
|
617
|
+
const found = resolveEntry(entry, byKey);
|
|
618
|
+
if ("ambiguous" in found) {
|
|
602
619
|
violations.push({
|
|
603
620
|
file: MANIFEST_FILE,
|
|
604
|
-
message: `${at}: matches ${
|
|
621
|
+
message: `${at}: matches ${found.ambiguous} mutants in the file. Add "occurrence" (1-based) to pick one`
|
|
605
622
|
});
|
|
606
623
|
return;
|
|
607
624
|
}
|
|
608
|
-
const target =
|
|
625
|
+
const target = found.target;
|
|
609
626
|
const refusal = target ? accept(target.mutant.status) : `stale ${noun} (no such mutant in this run). Remove it`;
|
|
610
627
|
if (!target || refusal !== null) {
|
|
611
628
|
violations.push({ file: MANIFEST_FILE, message: `${at}: ${refusal}` });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@t09tanaka/test-gates",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Test gates for the code that must not be wrong: per-file 100% coverage as the floor, mutation testing with a per-mutant allow list as the real check.",
|
|
5
5
|
"author": "t09tanaka",
|
|
6
6
|
"license": "MIT",
|