@t09tanaka/test-gates 0.1.0 → 0.2.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/dist/index.cjs CHANGED
@@ -92,17 +92,17 @@ function allowanceProblems(entry) {
92
92
  }
93
93
  return problems;
94
94
  }
95
- function validateEquivalentMutants(gatePath, value) {
95
+ function validateEquivalentMutants(gatePath, value, field = "equivalentMutants") {
96
96
  if (value === void 0) {
97
97
  return [];
98
98
  }
99
99
  if (!Array.isArray(value)) {
100
- return [{ file: MANIFEST_FILE, message: `${gatePath}: equivalentMutants must be an array` }];
100
+ return [{ file: MANIFEST_FILE, message: `${gatePath}: ${field} must be an array` }];
101
101
  }
102
102
  return value.flatMap(
103
103
  (entry, index) => allowanceProblems(entry).map((problem) => ({
104
104
  file: MANIFEST_FILE,
105
- message: `${gatePath}: equivalentMutants[${index}]${problem}`
105
+ message: `${gatePath}: ${field}[${index}]${problem}`
106
106
  }))
107
107
  );
108
108
  }
@@ -185,6 +185,9 @@ function parseManifest(text) {
185
185
  }
186
186
  for (const gate of gates) {
187
187
  violations.push(...validateEquivalentMutants(gate.path, gate.equivalentMutants));
188
+ violations.push(
189
+ ...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
190
+ );
188
191
  }
189
192
  return { gates, candidates, settings: raw.settings, violations };
190
193
  }
@@ -334,7 +337,9 @@ function resolveSettings(raw) {
334
337
  "gateCommand",
335
338
  "selfcheck",
336
339
  "stryker",
337
- "lcov"
340
+ "lcov",
341
+ "imports",
342
+ "mutation"
338
343
  ],
339
344
  "settings"
340
345
  );
@@ -435,6 +440,42 @@ function resolveSettings(raw) {
435
440
  "settings.selfcheck.failurePattern"
436
441
  );
437
442
  }
443
+ const importsSection = section(settings.imports, ["mode", "allow"], "settings.imports");
444
+ if (importsSection.mode !== void 0 && importsSection.mode !== "blocklist" && importsSection.mode !== "allowlist") {
445
+ throw new SettingsError('settings.imports.mode: must be "blocklist" or "allowlist"');
446
+ }
447
+ const importAllowRaw = importsSection.allow ?? [];
448
+ if (!Array.isArray(importAllowRaw)) {
449
+ throw new SettingsError("settings.imports.allow: must be an array");
450
+ }
451
+ const importAllow = importAllowRaw.map((item, index) => {
452
+ const at = `settings.imports.allow[${index}]`;
453
+ if (typeof item === "string" && item !== "") {
454
+ return { module: item };
455
+ }
456
+ if (isRecord2(item) && typeof item.pattern === "string" && item.pattern !== "") {
457
+ rejectUnknownKeys(item, ["pattern", "flags"], at);
458
+ if (item.flags !== void 0 && typeof item.flags !== "string") {
459
+ throw new SettingsError(`${at}.flags: must be a string`);
460
+ }
461
+ return { regex: compile(item.pattern, item.flags, at) };
462
+ }
463
+ if (isRecord2(item) && typeof item.module === "string" && item.module !== "") {
464
+ rejectUnknownKeys(item, ["module", "names"], at);
465
+ const names = stringArray(item.names, `${at}.names`);
466
+ if (names.length === 0) {
467
+ throw new SettingsError(`${at}.names: must list at least one name`);
468
+ }
469
+ return { module: item.module, names };
470
+ }
471
+ throw new SettingsError(
472
+ `${at}: must be a package name, { pattern, flags? } or { module, names }`
473
+ );
474
+ });
475
+ const mutation = section(settings.mutation, ["maxTimeouts"], "settings.mutation");
476
+ if (mutation.maxTimeouts !== void 0 && !(Number.isInteger(mutation.maxTimeouts) && mutation.maxTimeouts >= 0)) {
477
+ throw new SettingsError("settings.mutation.maxTimeouts: must be an integer >= 0");
478
+ }
438
479
  const stryker = section(settings.stryker, ["configFile", "reportFile"], "settings.stryker");
439
480
  const lcov = section(settings.lcov, ["file", "summaryExclude"], "settings.lcov");
440
481
  return {
@@ -456,6 +497,9 @@ function resolveSettings(raw) {
456
497
  selfcheckFailurePattern,
457
498
  strykerConfigFile: optionalString(stryker.configFile, "settings.stryker.configFile"),
458
499
  strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
500
+ importMode: importsSection.mode ?? "blocklist",
501
+ importAllow,
502
+ maxTimeouts: mutation.maxTimeouts ?? null,
459
503
  lcovFile: optionalString(lcov.file, "settings.lcov.file"),
460
504
  lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
461
505
  (pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
@@ -650,6 +694,70 @@ function parseImports(source) {
650
694
  }
651
695
  return found.sort((a, b) => a.line - b.line);
652
696
  }
697
+ var COMPUTED_CALL_IMPORT = /(?<![.\w$])(?:require|import)\s*\(\s*(?!['"`\s)])/g;
698
+ function findUncheckableImports(source) {
699
+ return [...source.matchAll(COMPUTED_CALL_IMPORT)].map((match) => lineAt(source, match.index));
700
+ }
701
+
702
+ // src/core/import-policy.ts
703
+ var SCRIPT_EXTENSION = /\.[cm]?[jt]sx?$/;
704
+ function isProjectImport(specifier, aliases) {
705
+ return specifier.startsWith(".") || aliases.some((alias) => specifier.startsWith(alias.prefix));
706
+ }
707
+ function resolveSpecifier(specifier, fromFile, aliases) {
708
+ let joined = null;
709
+ if (specifier.startsWith(".")) {
710
+ const slash = fromFile.lastIndexOf("/");
711
+ joined = slash === -1 ? specifier : `${fromFile.slice(0, slash)}/${specifier}`;
712
+ } else {
713
+ const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
714
+ if (alias) {
715
+ joined = alias.target + specifier.slice(alias.prefix.length);
716
+ }
717
+ }
718
+ if (joined === null) {
719
+ return null;
720
+ }
721
+ const segments = [];
722
+ for (const segment of joined.split("/")) {
723
+ if (segment === "..") {
724
+ segments.pop();
725
+ } else if (segment !== "." && segment !== "") {
726
+ segments.push(segment);
727
+ }
728
+ }
729
+ return segments.join("/").replace(SCRIPT_EXTENSION, "");
730
+ }
731
+ function matches(rule, specifier) {
732
+ if (rule.regex) {
733
+ return rule.regex.test(specifier);
734
+ }
735
+ return specifier === rule.module || specifier.startsWith(`${rule.module}/`);
736
+ }
737
+ function judgePackageImport(ref, allow) {
738
+ const rules = allow.filter((rule) => matches(rule, ref.module));
739
+ if (rules.length === 0) {
740
+ return `runtime import of "${ref.module}" is not in settings.imports.allow (allowlist mode allows relative imports, import aliases and listed packages only)`;
741
+ }
742
+ if (rules.some((rule) => rule.names === void 0)) {
743
+ return null;
744
+ }
745
+ const allowedNames = rules.flatMap((rule) => rule.names);
746
+ if (ref.whole) {
747
+ return `only ${allowedNames.join(", ")} may be imported from "${ref.module}", as named imports (settings.imports.allow)`;
748
+ }
749
+ const extra = ref.names.filter((name) => !allowedNames.includes(name));
750
+ return extra.length === 0 ? null : `runtime import of ${extra.join(", ")} from "${ref.module}" is not in settings.imports.allow (allowed: ${allowedNames.join(", ")})`;
751
+ }
752
+ function judgeProjectImport(target, impurePaths, gateExtensions) {
753
+ const candidates = [target, ...gateExtensions.map((extension) => target + extension)];
754
+ for (const rule of impurePaths) {
755
+ if (candidates.some((candidate) => rule.regex.test(candidate))) {
756
+ return rule.reason;
757
+ }
758
+ }
759
+ return null;
760
+ }
653
761
 
654
762
  // src/core/scan.ts
655
763
  var IGNORE_DIRECTIVE = /\b(istanbul|c8|v8)\s+ignore\b/;
@@ -667,7 +775,7 @@ var DISABLED_TESTS = [
667
775
  }
668
776
  ];
669
777
  var MODULE_MOCK = /\b(?:jest|vi)\s*\.\s*(?:mock|doMock|unstable_mockModule|setMock)\s*\(\s*(?:import\s*\(\s*)?(['"`])([^'"`\n]+)\1/g;
670
- var SCRIPT_EXTENSION = /\.[cm]?[jt]sx?$/;
778
+ var SCRIPT_EXTENSION2 = /\.[cm]?[jt]sx?$/;
671
779
  function eachLine(source, visit) {
672
780
  source.split("\n").forEach((line, index) => visit(line, index + 1));
673
781
  }
@@ -704,6 +812,7 @@ function scanGateSource(gatePath, source, settings) {
704
812
  if (imported.typeOnly) {
705
813
  continue;
706
814
  }
815
+ const before = violations.length;
707
816
  if (!settings.allowedImports.some((regex) => regex.test(imported.module))) {
708
817
  for (const rule of settings.impureImports) {
709
818
  if (rule.regex.test(imported.module)) {
@@ -730,34 +839,31 @@ function scanGateSource(gatePath, source, settings) {
730
839
  });
731
840
  }
732
841
  }
733
- }
734
- return violations.sort((a, b) => a.line - b.line);
735
- }
736
- function resolveSpecifier(specifier, fromFile, aliases) {
737
- let joined = null;
738
- if (specifier.startsWith(".")) {
739
- const directory = fromFile.includes("/") ? fromFile.slice(0, fromFile.lastIndexOf("/")) : "";
740
- joined = directory === "" ? specifier : `${directory}/${specifier}`;
741
- } else {
742
- for (const alias of aliases) {
743
- if (specifier.startsWith(alias.prefix)) {
744
- joined = alias.target + specifier.slice(alias.prefix.length);
745
- break;
842
+ if (settings.importMode === "allowlist" && violations.length === before) {
843
+ const reason = isProjectImport(imported.module, settings.importAliases) ? judgeProjectImport(
844
+ resolveSpecifier(imported.module, gatePath, settings.importAliases),
845
+ settings.impurePaths,
846
+ settings.gateExtensions
847
+ ) : judgePackageImport(imported, settings.importAllow);
848
+ if (reason !== null) {
849
+ violations.push({
850
+ file: gatePath,
851
+ line: imported.line,
852
+ message: isProjectImport(imported.module, settings.importAliases) ? `runtime import of "${imported.module}" (${reason})` : reason
853
+ });
746
854
  }
747
855
  }
748
856
  }
749
- if (joined === null) {
750
- return null;
751
- }
752
- const segments = [];
753
- for (const segment of joined.split("/")) {
754
- if (segment === "..") {
755
- segments.pop();
756
- } else if (segment !== "." && segment !== "") {
757
- segments.push(segment);
857
+ if (settings.importMode === "allowlist") {
858
+ for (const line of findUncheckableImports(source)) {
859
+ violations.push({
860
+ file: gatePath,
861
+ line,
862
+ message: "require() / import() with a computed specifier cannot be checked (allowlist mode)"
863
+ });
758
864
  }
759
865
  }
760
- return segments.join("/").replace(SCRIPT_EXTENSION, "");
866
+ return violations.sort((a, b) => a.line - b.line);
761
867
  }
762
868
  function scanSpecSource(gatePath, specPath, source, settings) {
763
869
  const violations = [];
@@ -775,7 +881,7 @@ function scanSpecSource(gatePath, specPath, source, settings) {
775
881
  }
776
882
  }
777
883
  });
778
- const gateModule = gatePath.replace(SCRIPT_EXTENSION, "");
884
+ const gateModule = gatePath.replace(SCRIPT_EXTENSION2, "");
779
885
  const gateDirectory = gateModule.replace(/\/index$/, "");
780
886
  for (const match of source.matchAll(MODULE_MOCK)) {
781
887
  const specifier = match[2];
@@ -904,6 +1010,9 @@ function checkGates(manifest) {
904
1010
  }
905
1011
  return violations;
906
1012
  }
1013
+ var VITEST_GATE_CONFIGS = ["ts", "mts", "cts", "js", "mjs", "cjs"].map(
1014
+ (extension) => `vitest.gates.config.${extension}`
1015
+ );
907
1016
 
908
1017
  // src/core/mutation-result.ts
909
1018
  var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
@@ -938,6 +1047,27 @@ function sliceByLocation(source, location) {
938
1047
  function keyOf(mutator, original, replacement) {
939
1048
  return JSON.stringify([mutator, normalizeCode(original), normalizeCode(replacement)]);
940
1049
  }
1050
+ function describeMutant(file, entry) {
1051
+ const { mutant } = entry;
1052
+ return {
1053
+ file,
1054
+ line: mutant.location.start.line,
1055
+ column: mutant.location.start.column,
1056
+ status: mutant.status,
1057
+ mutator: mutant.mutatorName,
1058
+ original: normalizeCode(entry.original),
1059
+ replacement: normalizeCode(entry.replacement),
1060
+ occurrence: entry.occurrence,
1061
+ sameKeyCount: entry.sameKeyCount,
1062
+ allowance: {
1063
+ mutator: mutant.mutatorName,
1064
+ original: normalizeCode(entry.original),
1065
+ replacement: normalizeCode(entry.replacement),
1066
+ ...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
1067
+ reason: ""
1068
+ }
1069
+ };
1070
+ }
941
1071
  function describeAllowance(entry) {
942
1072
  const occurrence = entry.occurrence === void 0 ? "" : ` / occurrence ${entry.occurrence}`;
943
1073
  return `${entry.mutator} / ${normalizeCode(entry.original)} \u2192 ${normalizeCode(entry.replacement)}${occurrence}`;
@@ -945,13 +1075,15 @@ function describeAllowance(entry) {
945
1075
  function judgeMutationReport(input) {
946
1076
  const violations = [];
947
1077
  const survivors = [];
1078
+ const unexpectedTimeouts = [];
948
1079
  const summary = {
949
1080
  total: 0,
950
1081
  detected: 0,
951
1082
  timeout: 0,
952
1083
  allowed: 0,
953
1084
  notEvaluable: 0,
954
- unallowed: 0
1085
+ unallowed: 0,
1086
+ expectedTimeout: 0
955
1087
  };
956
1088
  const files = /* @__PURE__ */ new Map();
957
1089
  for (const [file, result] of Object.entries(input.report.files ?? {})) {
@@ -961,7 +1093,9 @@ function judgeMutationReport(input) {
961
1093
  const gatePath = normalizePath(gate.path);
962
1094
  const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
963
1095
  violations.push(...shapeViolations);
964
- const allowances = Array.isArray(gate.equivalentMutants) ? gate.equivalentMutants : [];
1096
+ violations.push(
1097
+ ...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
1098
+ );
965
1099
  const result = files.get(gatePath);
966
1100
  if (!result || !Array.isArray(result.mutants) || result.mutants.length === 0) {
967
1101
  violations.push({
@@ -1025,38 +1159,53 @@ function judgeMutationReport(input) {
1025
1159
  if (!locationsValid) {
1026
1160
  continue;
1027
1161
  }
1028
- const allowed = /* @__PURE__ */ new Set();
1029
- allowances.forEach((allowance, index) => {
1030
- if (!isUsableEquivalentMutant(allowance)) {
1031
- return;
1162
+ const claim = (field, entries, noun, accept) => {
1163
+ const claimed = /* @__PURE__ */ new Set();
1164
+ if (!Array.isArray(entries)) {
1165
+ return claimed;
1032
1166
  }
1033
- const at = `${gate.path}: equivalentMutants[${index}] (${describeAllowance(allowance)})`;
1034
- const group = byKey.get(keyOf(allowance.mutator, allowance.original, allowance.replacement)) ?? [];
1035
- if (allowance.occurrence === void 0 && group.length > 1) {
1036
- violations.push({
1037
- file: MANIFEST_FILE,
1038
- message: `${at}: matches ${group.length} mutants in the file. Add "occurrence" (1-based) to pick one`
1039
- });
1040
- return;
1041
- }
1042
- const target = group[(allowance.occurrence ?? 1) - 1];
1043
- if (!target || !SURVIVED.has(target.mutant.status)) {
1044
- const why = target ? `the mutant is now ${target.mutant.status}` : "no such mutant in this run";
1045
- violations.push({
1046
- file: MANIFEST_FILE,
1047
- message: `${at}: stale allowance (${why}). Remove it`
1048
- });
1049
- return;
1050
- }
1051
- if (allowed.has(target)) {
1052
- violations.push({
1053
- file: MANIFEST_FILE,
1054
- message: `${at}: another allowance already covers this mutant`
1055
- });
1056
- return;
1057
- }
1058
- allowed.add(target);
1059
- });
1167
+ entries.forEach((entry, index) => {
1168
+ if (!isUsableEquivalentMutant(entry)) {
1169
+ return;
1170
+ }
1171
+ const at = `${gate.path}: ${field}[${index}] (${describeAllowance(entry)})`;
1172
+ const group = byKey.get(keyOf(entry.mutator, entry.original, entry.replacement)) ?? [];
1173
+ if (entry.occurrence === void 0 && group.length > 1) {
1174
+ violations.push({
1175
+ file: MANIFEST_FILE,
1176
+ message: `${at}: matches ${group.length} mutants in the file. Add "occurrence" (1-based) to pick one`
1177
+ });
1178
+ return;
1179
+ }
1180
+ const target = group[(entry.occurrence ?? 1) - 1];
1181
+ const refusal = target ? accept(target.mutant.status) : `stale ${noun} (no such mutant in this run). Remove it`;
1182
+ if (!target || refusal !== null) {
1183
+ violations.push({ file: MANIFEST_FILE, message: `${at}: ${refusal}` });
1184
+ return;
1185
+ }
1186
+ if (claimed.has(target)) {
1187
+ violations.push({
1188
+ file: MANIFEST_FILE,
1189
+ message: `${at}: another ${noun} already covers this mutant`
1190
+ });
1191
+ return;
1192
+ }
1193
+ claimed.add(target);
1194
+ });
1195
+ return claimed;
1196
+ };
1197
+ const allowed = claim(
1198
+ "equivalentMutants",
1199
+ gate.equivalentMutants,
1200
+ "allowance",
1201
+ (status) => SURVIVED.has(status) ? null : `stale allowance (the mutant is now ${status}). Remove it`
1202
+ );
1203
+ const expected = claim(
1204
+ "expectedTimeouts",
1205
+ gate.expectedTimeouts,
1206
+ "entry",
1207
+ (status) => SURVIVED.has(status) ? `the mutant ${status === "NoCoverage" ? "has no coverage" : "survived"} instead of timing out. Detect it with a test` : null
1208
+ );
1060
1209
  for (const entry of located) {
1061
1210
  const { mutant } = entry;
1062
1211
  summary.total += 1;
@@ -1064,6 +1213,11 @@ function judgeMutationReport(input) {
1064
1213
  summary.detected += 1;
1065
1214
  if (mutant.status === "Timeout") {
1066
1215
  summary.timeout += 1;
1216
+ if (expected.has(entry)) {
1217
+ summary.expectedTimeout += 1;
1218
+ } else {
1219
+ unexpectedTimeouts.push(describeMutant(gate.path, entry));
1220
+ }
1067
1221
  }
1068
1222
  } else if (NOT_EVALUABLE.has(mutant.status)) {
1069
1223
  summary.notEvaluable += 1;
@@ -1073,24 +1227,7 @@ function judgeMutationReport(input) {
1073
1227
  continue;
1074
1228
  }
1075
1229
  summary.unallowed += 1;
1076
- survivors.push({
1077
- file: gate.path,
1078
- line: mutant.location.start.line,
1079
- column: mutant.location.start.column,
1080
- status: mutant.status,
1081
- mutator: mutant.mutatorName,
1082
- original: normalizeCode(entry.original),
1083
- replacement: normalizeCode(entry.replacement),
1084
- occurrence: entry.occurrence,
1085
- sameKeyCount: entry.sameKeyCount,
1086
- allowance: {
1087
- mutator: mutant.mutatorName,
1088
- original: normalizeCode(entry.original),
1089
- replacement: normalizeCode(entry.replacement),
1090
- ...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
1091
- reason: ""
1092
- }
1093
- });
1230
+ survivors.push(describeMutant(gate.path, entry));
1094
1231
  } else {
1095
1232
  violations.push({
1096
1233
  file: gate.path,
@@ -1106,7 +1243,14 @@ function judgeMutationReport(input) {
1106
1243
  message: "no mutants were evaluated (the mutation test ran on nothing)"
1107
1244
  });
1108
1245
  }
1109
- return { violations, survivors, summary };
1246
+ const maxTimeouts = input.maxTimeouts ?? null;
1247
+ if (maxTimeouts !== null && unexpectedTimeouts.length > maxTimeouts) {
1248
+ violations.push({
1249
+ file: MANIFEST_FILE,
1250
+ message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
1251
+ });
1252
+ }
1253
+ return { violations, survivors, unexpectedTimeouts, summary };
1110
1254
  }
1111
1255
  function formatSurvivor(survivor) {
1112
1256
  const occurrence = survivor.sameKeyCount > 1 ? ` / occurrence ${survivor.occurrence} of ${survivor.sameKeyCount}` : "";
package/dist/index.d.cts CHANGED
@@ -12,6 +12,11 @@ interface Gate {
12
12
  decides: string;
13
13
  impact: string;
14
14
  equivalentMutants?: EquivalentMutant[];
15
+ /**
16
+ * Mutants that always time out (a loop that never ends). Same key as `equivalentMutants`.
17
+ * They are not counted against `settings.mutation.maxTimeouts`.
18
+ */
19
+ expectedTimeouts?: EquivalentMutant[];
15
20
  }
16
21
  interface Candidate {
17
22
  path: string;
@@ -75,6 +80,21 @@ interface Settings {
75
80
  file?: string;
76
81
  summaryExclude?: string[];
77
82
  };
83
+ /** Since 0.2.0. */
84
+ imports?: {
85
+ mode?: 'blocklist' | 'allowlist';
86
+ allow?: (string | {
87
+ pattern: string;
88
+ flags?: string;
89
+ } | {
90
+ module: string;
91
+ names: string[];
92
+ })[];
93
+ };
94
+ /** Since 0.2.0. */
95
+ mutation?: {
96
+ maxTimeouts?: number;
97
+ };
78
98
  }
79
99
  interface Manifest {
80
100
  $schema?: string;
@@ -108,6 +128,14 @@ interface ResolvedSettings {
108
128
  strykerReportFile: string;
109
129
  lcovFile: string | null;
110
130
  lcovSummaryExclude: RegExp[];
131
+ importMode: 'blocklist' | 'allowlist';
132
+ importAllow: {
133
+ module?: string;
134
+ regex?: RegExp;
135
+ names?: string[];
136
+ }[];
137
+ /** `null`: no limit. */
138
+ maxTimeouts: number | null;
111
139
  }
112
140
  /** One finding. Printed as `file:line: message`. */
113
141
  interface Violation {
@@ -244,10 +272,17 @@ interface MutationSummary {
244
272
  notEvaluable: number;
245
273
  /** Survived / NoCoverage mutants with no allowance. */
246
274
  unallowed: number;
275
+ /** The part of `timeout` that `expectedTimeouts` entries cover. */
276
+ expectedTimeout: number;
247
277
  }
248
278
  interface MutationVerdict {
249
279
  violations: Violation[];
250
280
  survivors: UnallowedSurvivor[];
281
+ /**
282
+ * Timed-out mutants that no `expectedTimeouts` entry covers. Informational unless
283
+ * `maxTimeouts` is set and exceeded, in which case `violations` says so.
284
+ */
285
+ unexpectedTimeouts: UnallowedSurvivor[];
251
286
  summary: MutationSummary;
252
287
  }
253
288
  /** Whitespace is not part of the key, so reformatting a gate does not invalidate an allowance. */
@@ -275,6 +310,11 @@ declare function judgeMutationReport(input: {
275
310
  gates: Gate[];
276
311
  report: MutationReport;
277
312
  readSource: (gatePath: string) => string | undefined;
313
+ /**
314
+ * `settings.mutation.maxTimeouts`: how many timeouts outside `expectedTimeouts` are
315
+ * tolerated. `null` or absent means no limit.
316
+ */
317
+ maxTimeouts?: number | null;
278
318
  }): MutationVerdict;
279
319
  /** `file:line:column / mutator / original → replacement / occurrence n of m`. */
280
320
  declare function formatSurvivor(survivor: UnallowedSurvivor): string;
package/dist/index.d.ts CHANGED
@@ -12,6 +12,11 @@ interface Gate {
12
12
  decides: string;
13
13
  impact: string;
14
14
  equivalentMutants?: EquivalentMutant[];
15
+ /**
16
+ * Mutants that always time out (a loop that never ends). Same key as `equivalentMutants`.
17
+ * They are not counted against `settings.mutation.maxTimeouts`.
18
+ */
19
+ expectedTimeouts?: EquivalentMutant[];
15
20
  }
16
21
  interface Candidate {
17
22
  path: string;
@@ -75,6 +80,21 @@ interface Settings {
75
80
  file?: string;
76
81
  summaryExclude?: string[];
77
82
  };
83
+ /** Since 0.2.0. */
84
+ imports?: {
85
+ mode?: 'blocklist' | 'allowlist';
86
+ allow?: (string | {
87
+ pattern: string;
88
+ flags?: string;
89
+ } | {
90
+ module: string;
91
+ names: string[];
92
+ })[];
93
+ };
94
+ /** Since 0.2.0. */
95
+ mutation?: {
96
+ maxTimeouts?: number;
97
+ };
78
98
  }
79
99
  interface Manifest {
80
100
  $schema?: string;
@@ -108,6 +128,14 @@ interface ResolvedSettings {
108
128
  strykerReportFile: string;
109
129
  lcovFile: string | null;
110
130
  lcovSummaryExclude: RegExp[];
131
+ importMode: 'blocklist' | 'allowlist';
132
+ importAllow: {
133
+ module?: string;
134
+ regex?: RegExp;
135
+ names?: string[];
136
+ }[];
137
+ /** `null`: no limit. */
138
+ maxTimeouts: number | null;
111
139
  }
112
140
  /** One finding. Printed as `file:line: message`. */
113
141
  interface Violation {
@@ -244,10 +272,17 @@ interface MutationSummary {
244
272
  notEvaluable: number;
245
273
  /** Survived / NoCoverage mutants with no allowance. */
246
274
  unallowed: number;
275
+ /** The part of `timeout` that `expectedTimeouts` entries cover. */
276
+ expectedTimeout: number;
247
277
  }
248
278
  interface MutationVerdict {
249
279
  violations: Violation[];
250
280
  survivors: UnallowedSurvivor[];
281
+ /**
282
+ * Timed-out mutants that no `expectedTimeouts` entry covers. Informational unless
283
+ * `maxTimeouts` is set and exceeded, in which case `violations` says so.
284
+ */
285
+ unexpectedTimeouts: UnallowedSurvivor[];
251
286
  summary: MutationSummary;
252
287
  }
253
288
  /** Whitespace is not part of the key, so reformatting a gate does not invalidate an allowance. */
@@ -275,6 +310,11 @@ declare function judgeMutationReport(input: {
275
310
  gates: Gate[];
276
311
  report: MutationReport;
277
312
  readSource: (gatePath: string) => string | undefined;
313
+ /**
314
+ * `settings.mutation.maxTimeouts`: how many timeouts outside `expectedTimeouts` are
315
+ * tolerated. `null` or absent means no limit.
316
+ */
317
+ maxTimeouts?: number | null;
278
318
  }): MutationVerdict;
279
319
  /** `file:line:column / mutator / original → replacement / occurrence n of m`. */
280
320
  declare function formatSurvivor(survivor: UnallowedSurvivor): string;