@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.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  readText,
21
21
  resolveSettings,
22
22
  validateEquivalentMutants
23
- } from "./chunk-LMD3ANJ2.js";
23
+ } from "./chunk-XEKQKLKY.js";
24
24
 
25
25
  // src/commands/check.ts
26
26
  import fs from "fs";
@@ -77,6 +77,70 @@ function parseImports(source) {
77
77
  }
78
78
  return found.sort((a, b) => a.line - b.line);
79
79
  }
80
+ var COMPUTED_CALL_IMPORT = /(?<![.\w$])(?:require|import)\s*\(\s*(?!['"`\s)])/g;
81
+ function findUncheckableImports(source) {
82
+ return [...source.matchAll(COMPUTED_CALL_IMPORT)].map((match) => lineAt(source, match.index));
83
+ }
84
+
85
+ // src/core/import-policy.ts
86
+ var SCRIPT_EXTENSION = /\.[cm]?[jt]sx?$/;
87
+ function isProjectImport(specifier, aliases) {
88
+ return specifier.startsWith(".") || aliases.some((alias) => specifier.startsWith(alias.prefix));
89
+ }
90
+ function resolveSpecifier(specifier, fromFile, aliases) {
91
+ let joined = null;
92
+ if (specifier.startsWith(".")) {
93
+ const slash = fromFile.lastIndexOf("/");
94
+ joined = slash === -1 ? specifier : `${fromFile.slice(0, slash)}/${specifier}`;
95
+ } else {
96
+ const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
97
+ if (alias) {
98
+ joined = alias.target + specifier.slice(alias.prefix.length);
99
+ }
100
+ }
101
+ if (joined === null) {
102
+ return null;
103
+ }
104
+ const segments = [];
105
+ for (const segment of joined.split("/")) {
106
+ if (segment === "..") {
107
+ segments.pop();
108
+ } else if (segment !== "." && segment !== "") {
109
+ segments.push(segment);
110
+ }
111
+ }
112
+ return segments.join("/").replace(SCRIPT_EXTENSION, "");
113
+ }
114
+ function matches(rule, specifier) {
115
+ if (rule.regex) {
116
+ return rule.regex.test(specifier);
117
+ }
118
+ return specifier === rule.module || specifier.startsWith(`${rule.module}/`);
119
+ }
120
+ function judgePackageImport(ref, allow) {
121
+ const rules = allow.filter((rule) => matches(rule, ref.module));
122
+ if (rules.length === 0) {
123
+ return `runtime import of "${ref.module}" is not in settings.imports.allow (allowlist mode allows relative imports, import aliases and listed packages only)`;
124
+ }
125
+ if (rules.some((rule) => rule.names === void 0)) {
126
+ return null;
127
+ }
128
+ const allowedNames = rules.flatMap((rule) => rule.names);
129
+ if (ref.whole) {
130
+ return `only ${allowedNames.join(", ")} may be imported from "${ref.module}", as named imports (settings.imports.allow)`;
131
+ }
132
+ const extra = ref.names.filter((name) => !allowedNames.includes(name));
133
+ return extra.length === 0 ? null : `runtime import of ${extra.join(", ")} from "${ref.module}" is not in settings.imports.allow (allowed: ${allowedNames.join(", ")})`;
134
+ }
135
+ function judgeProjectImport(target, impurePaths, gateExtensions) {
136
+ const candidates = [target, ...gateExtensions.map((extension) => target + extension)];
137
+ for (const rule of impurePaths) {
138
+ if (candidates.some((candidate) => rule.regex.test(candidate))) {
139
+ return rule.reason;
140
+ }
141
+ }
142
+ return null;
143
+ }
80
144
 
81
145
  // src/core/scan.ts
82
146
  var IGNORE_DIRECTIVE = /\b(istanbul|c8|v8)\s+ignore\b/;
@@ -94,7 +158,7 @@ var DISABLED_TESTS = [
94
158
  }
95
159
  ];
96
160
  var MODULE_MOCK = /\b(?:jest|vi)\s*\.\s*(?:mock|doMock|unstable_mockModule|setMock)\s*\(\s*(?:import\s*\(\s*)?(['"`])([^'"`\n]+)\1/g;
97
- var SCRIPT_EXTENSION = /\.[cm]?[jt]sx?$/;
161
+ var SCRIPT_EXTENSION2 = /\.[cm]?[jt]sx?$/;
98
162
  function eachLine(source, visit) {
99
163
  source.split("\n").forEach((line, index) => visit(line, index + 1));
100
164
  }
@@ -131,6 +195,7 @@ function scanGateSource(gatePath, source, settings) {
131
195
  if (imported.typeOnly) {
132
196
  continue;
133
197
  }
198
+ const before = violations.length;
134
199
  if (!settings.allowedImports.some((regex) => regex.test(imported.module))) {
135
200
  for (const rule of settings.impureImports) {
136
201
  if (rule.regex.test(imported.module)) {
@@ -157,34 +222,31 @@ function scanGateSource(gatePath, source, settings) {
157
222
  });
158
223
  }
159
224
  }
160
- }
161
- return violations.sort((a, b) => a.line - b.line);
162
- }
163
- function resolveSpecifier(specifier, fromFile, aliases) {
164
- let joined = null;
165
- if (specifier.startsWith(".")) {
166
- const directory = fromFile.includes("/") ? fromFile.slice(0, fromFile.lastIndexOf("/")) : "";
167
- joined = directory === "" ? specifier : `${directory}/${specifier}`;
168
- } else {
169
- for (const alias of aliases) {
170
- if (specifier.startsWith(alias.prefix)) {
171
- joined = alias.target + specifier.slice(alias.prefix.length);
172
- break;
225
+ if (settings.importMode === "allowlist" && violations.length === before) {
226
+ const reason = isProjectImport(imported.module, settings.importAliases) ? judgeProjectImport(
227
+ resolveSpecifier(imported.module, gatePath, settings.importAliases),
228
+ settings.impurePaths,
229
+ settings.gateExtensions
230
+ ) : judgePackageImport(imported, settings.importAllow);
231
+ if (reason !== null) {
232
+ violations.push({
233
+ file: gatePath,
234
+ line: imported.line,
235
+ message: isProjectImport(imported.module, settings.importAliases) ? `runtime import of "${imported.module}" (${reason})` : reason
236
+ });
173
237
  }
174
238
  }
175
239
  }
176
- if (joined === null) {
177
- return null;
178
- }
179
- const segments = [];
180
- for (const segment of joined.split("/")) {
181
- if (segment === "..") {
182
- segments.pop();
183
- } else if (segment !== "." && segment !== "") {
184
- segments.push(segment);
240
+ if (settings.importMode === "allowlist") {
241
+ for (const line of findUncheckableImports(source)) {
242
+ violations.push({
243
+ file: gatePath,
244
+ line,
245
+ message: "require() / import() with a computed specifier cannot be checked (allowlist mode)"
246
+ });
185
247
  }
186
248
  }
187
- return segments.join("/").replace(SCRIPT_EXTENSION, "");
249
+ return violations.sort((a, b) => a.line - b.line);
188
250
  }
189
251
  function scanSpecSource(gatePath, specPath, source, settings) {
190
252
  const violations = [];
@@ -202,7 +264,7 @@ function scanSpecSource(gatePath, specPath, source, settings) {
202
264
  }
203
265
  }
204
266
  });
205
- const gateModule = gatePath.replace(SCRIPT_EXTENSION, "");
267
+ const gateModule = gatePath.replace(SCRIPT_EXTENSION2, "");
206
268
  const gateDirectory = gateModule.replace(/\/index$/, "");
207
269
  for (const match of source.matchAll(MODULE_MOCK)) {
208
270
  const specifier = match[2];
@@ -331,6 +393,9 @@ function checkGates(manifest) {
331
393
  }
332
394
  return violations;
333
395
  }
396
+ var VITEST_GATE_CONFIGS = ["ts", "mts", "cts", "js", "mjs", "cjs"].map(
397
+ (extension) => `vitest.gates.config.${extension}`
398
+ );
334
399
 
335
400
  // src/core/mutation-result.ts
336
401
  var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
@@ -365,6 +430,27 @@ function sliceByLocation(source, location) {
365
430
  function keyOf(mutator, original, replacement) {
366
431
  return JSON.stringify([mutator, normalizeCode(original), normalizeCode(replacement)]);
367
432
  }
433
+ function describeMutant(file, entry) {
434
+ const { mutant } = entry;
435
+ return {
436
+ file,
437
+ line: mutant.location.start.line,
438
+ column: mutant.location.start.column,
439
+ status: mutant.status,
440
+ mutator: mutant.mutatorName,
441
+ original: normalizeCode(entry.original),
442
+ replacement: normalizeCode(entry.replacement),
443
+ occurrence: entry.occurrence,
444
+ sameKeyCount: entry.sameKeyCount,
445
+ allowance: {
446
+ mutator: mutant.mutatorName,
447
+ original: normalizeCode(entry.original),
448
+ replacement: normalizeCode(entry.replacement),
449
+ ...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
450
+ reason: ""
451
+ }
452
+ };
453
+ }
368
454
  function describeAllowance(entry) {
369
455
  const occurrence = entry.occurrence === void 0 ? "" : ` / occurrence ${entry.occurrence}`;
370
456
  return `${entry.mutator} / ${normalizeCode(entry.original)} \u2192 ${normalizeCode(entry.replacement)}${occurrence}`;
@@ -372,13 +458,15 @@ function describeAllowance(entry) {
372
458
  function judgeMutationReport(input) {
373
459
  const violations = [];
374
460
  const survivors = [];
461
+ const unexpectedTimeouts = [];
375
462
  const summary = {
376
463
  total: 0,
377
464
  detected: 0,
378
465
  timeout: 0,
379
466
  allowed: 0,
380
467
  notEvaluable: 0,
381
- unallowed: 0
468
+ unallowed: 0,
469
+ expectedTimeout: 0
382
470
  };
383
471
  const files = /* @__PURE__ */ new Map();
384
472
  for (const [file, result] of Object.entries(input.report.files ?? {})) {
@@ -388,7 +476,9 @@ function judgeMutationReport(input) {
388
476
  const gatePath = normalizePath(gate.path);
389
477
  const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
390
478
  violations.push(...shapeViolations);
391
- const allowances = Array.isArray(gate.equivalentMutants) ? gate.equivalentMutants : [];
479
+ violations.push(
480
+ ...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
481
+ );
392
482
  const result = files.get(gatePath);
393
483
  if (!result || !Array.isArray(result.mutants) || result.mutants.length === 0) {
394
484
  violations.push({
@@ -452,38 +542,53 @@ function judgeMutationReport(input) {
452
542
  if (!locationsValid) {
453
543
  continue;
454
544
  }
455
- const allowed = /* @__PURE__ */ new Set();
456
- allowances.forEach((allowance, index) => {
457
- if (!isUsableEquivalentMutant(allowance)) {
458
- return;
459
- }
460
- const at = `${gate.path}: equivalentMutants[${index}] (${describeAllowance(allowance)})`;
461
- const group = byKey.get(keyOf(allowance.mutator, allowance.original, allowance.replacement)) ?? [];
462
- if (allowance.occurrence === void 0 && group.length > 1) {
463
- violations.push({
464
- file: MANIFEST_FILE,
465
- message: `${at}: matches ${group.length} mutants in the file. Add "occurrence" (1-based) to pick one`
466
- });
467
- return;
468
- }
469
- const target = group[(allowance.occurrence ?? 1) - 1];
470
- if (!target || !SURVIVED.has(target.mutant.status)) {
471
- const why = target ? `the mutant is now ${target.mutant.status}` : "no such mutant in this run";
472
- violations.push({
473
- file: MANIFEST_FILE,
474
- message: `${at}: stale allowance (${why}). Remove it`
475
- });
476
- return;
545
+ const claim = (field, entries, noun, accept) => {
546
+ const claimed = /* @__PURE__ */ new Set();
547
+ if (!Array.isArray(entries)) {
548
+ return claimed;
477
549
  }
478
- if (allowed.has(target)) {
479
- violations.push({
480
- file: MANIFEST_FILE,
481
- message: `${at}: another allowance already covers this mutant`
482
- });
483
- return;
484
- }
485
- allowed.add(target);
486
- });
550
+ entries.forEach((entry, index) => {
551
+ if (!isUsableEquivalentMutant(entry)) {
552
+ return;
553
+ }
554
+ const at = `${gate.path}: ${field}[${index}] (${describeAllowance(entry)})`;
555
+ const group = byKey.get(keyOf(entry.mutator, entry.original, entry.replacement)) ?? [];
556
+ if (entry.occurrence === void 0 && group.length > 1) {
557
+ violations.push({
558
+ file: MANIFEST_FILE,
559
+ message: `${at}: matches ${group.length} mutants in the file. Add "occurrence" (1-based) to pick one`
560
+ });
561
+ return;
562
+ }
563
+ const target = group[(entry.occurrence ?? 1) - 1];
564
+ const refusal = target ? accept(target.mutant.status) : `stale ${noun} (no such mutant in this run). Remove it`;
565
+ if (!target || refusal !== null) {
566
+ violations.push({ file: MANIFEST_FILE, message: `${at}: ${refusal}` });
567
+ return;
568
+ }
569
+ if (claimed.has(target)) {
570
+ violations.push({
571
+ file: MANIFEST_FILE,
572
+ message: `${at}: another ${noun} already covers this mutant`
573
+ });
574
+ return;
575
+ }
576
+ claimed.add(target);
577
+ });
578
+ return claimed;
579
+ };
580
+ const allowed = claim(
581
+ "equivalentMutants",
582
+ gate.equivalentMutants,
583
+ "allowance",
584
+ (status) => SURVIVED.has(status) ? null : `stale allowance (the mutant is now ${status}). Remove it`
585
+ );
586
+ const expected = claim(
587
+ "expectedTimeouts",
588
+ gate.expectedTimeouts,
589
+ "entry",
590
+ (status) => SURVIVED.has(status) ? `the mutant ${status === "NoCoverage" ? "has no coverage" : "survived"} instead of timing out. Detect it with a test` : null
591
+ );
487
592
  for (const entry of located) {
488
593
  const { mutant } = entry;
489
594
  summary.total += 1;
@@ -491,6 +596,11 @@ function judgeMutationReport(input) {
491
596
  summary.detected += 1;
492
597
  if (mutant.status === "Timeout") {
493
598
  summary.timeout += 1;
599
+ if (expected.has(entry)) {
600
+ summary.expectedTimeout += 1;
601
+ } else {
602
+ unexpectedTimeouts.push(describeMutant(gate.path, entry));
603
+ }
494
604
  }
495
605
  } else if (NOT_EVALUABLE.has(mutant.status)) {
496
606
  summary.notEvaluable += 1;
@@ -500,24 +610,7 @@ function judgeMutationReport(input) {
500
610
  continue;
501
611
  }
502
612
  summary.unallowed += 1;
503
- survivors.push({
504
- file: gate.path,
505
- line: mutant.location.start.line,
506
- column: mutant.location.start.column,
507
- status: mutant.status,
508
- mutator: mutant.mutatorName,
509
- original: normalizeCode(entry.original),
510
- replacement: normalizeCode(entry.replacement),
511
- occurrence: entry.occurrence,
512
- sameKeyCount: entry.sameKeyCount,
513
- allowance: {
514
- mutator: mutant.mutatorName,
515
- original: normalizeCode(entry.original),
516
- replacement: normalizeCode(entry.replacement),
517
- ...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
518
- reason: ""
519
- }
520
- });
613
+ survivors.push(describeMutant(gate.path, entry));
521
614
  } else {
522
615
  violations.push({
523
616
  file: gate.path,
@@ -533,7 +626,14 @@ function judgeMutationReport(input) {
533
626
  message: "no mutants were evaluated (the mutation test ran on nothing)"
534
627
  });
535
628
  }
536
- return { violations, survivors, summary };
629
+ const maxTimeouts = input.maxTimeouts ?? null;
630
+ if (maxTimeouts !== null && unexpectedTimeouts.length > maxTimeouts) {
631
+ violations.push({
632
+ file: MANIFEST_FILE,
633
+ message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
634
+ });
635
+ }
636
+ return { violations, survivors, unexpectedTimeouts, summary };
537
637
  }
538
638
  function formatSurvivor(survivor) {
539
639
  const occurrence = survivor.sameKeyCount > 1 ? ` / occurrence ${survivor.occurrence} of ${survivor.sameKeyCount}` : "";
package/dist/jest.cjs CHANGED
@@ -71,17 +71,17 @@ function allowanceProblems(entry) {
71
71
  }
72
72
  return problems;
73
73
  }
74
- function validateEquivalentMutants(gatePath, value) {
74
+ function validateEquivalentMutants(gatePath, value, field = "equivalentMutants") {
75
75
  if (value === void 0) {
76
76
  return [];
77
77
  }
78
78
  if (!Array.isArray(value)) {
79
- return [{ file: MANIFEST_FILE, message: `${gatePath}: equivalentMutants must be an array` }];
79
+ return [{ file: MANIFEST_FILE, message: `${gatePath}: ${field} must be an array` }];
80
80
  }
81
81
  return value.flatMap(
82
82
  (entry, index) => allowanceProblems(entry).map((problem) => ({
83
83
  file: MANIFEST_FILE,
84
- message: `${gatePath}: equivalentMutants[${index}]${problem}`
84
+ message: `${gatePath}: ${field}[${index}]${problem}`
85
85
  }))
86
86
  );
87
87
  }
@@ -161,6 +161,9 @@ function parseManifest(text) {
161
161
  }
162
162
  for (const gate of gates) {
163
163
  violations.push(...validateEquivalentMutants(gate.path, gate.equivalentMutants));
164
+ violations.push(
165
+ ...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
166
+ );
164
167
  }
165
168
  return { gates, candidates, settings: raw.settings, violations };
166
169
  }
@@ -310,7 +313,9 @@ function resolveSettings(raw) {
310
313
  "gateCommand",
311
314
  "selfcheck",
312
315
  "stryker",
313
- "lcov"
316
+ "lcov",
317
+ "imports",
318
+ "mutation"
314
319
  ],
315
320
  "settings"
316
321
  );
@@ -411,6 +416,42 @@ function resolveSettings(raw) {
411
416
  "settings.selfcheck.failurePattern"
412
417
  );
413
418
  }
419
+ const importsSection = section(settings.imports, ["mode", "allow"], "settings.imports");
420
+ if (importsSection.mode !== void 0 && importsSection.mode !== "blocklist" && importsSection.mode !== "allowlist") {
421
+ throw new SettingsError('settings.imports.mode: must be "blocklist" or "allowlist"');
422
+ }
423
+ const importAllowRaw = importsSection.allow ?? [];
424
+ if (!Array.isArray(importAllowRaw)) {
425
+ throw new SettingsError("settings.imports.allow: must be an array");
426
+ }
427
+ const importAllow = importAllowRaw.map((item, index) => {
428
+ const at = `settings.imports.allow[${index}]`;
429
+ if (typeof item === "string" && item !== "") {
430
+ return { module: item };
431
+ }
432
+ if (isRecord2(item) && typeof item.pattern === "string" && item.pattern !== "") {
433
+ rejectUnknownKeys(item, ["pattern", "flags"], at);
434
+ if (item.flags !== void 0 && typeof item.flags !== "string") {
435
+ throw new SettingsError(`${at}.flags: must be a string`);
436
+ }
437
+ return { regex: compile(item.pattern, item.flags, at) };
438
+ }
439
+ if (isRecord2(item) && typeof item.module === "string" && item.module !== "") {
440
+ rejectUnknownKeys(item, ["module", "names"], at);
441
+ const names = stringArray(item.names, `${at}.names`);
442
+ if (names.length === 0) {
443
+ throw new SettingsError(`${at}.names: must list at least one name`);
444
+ }
445
+ return { module: item.module, names };
446
+ }
447
+ throw new SettingsError(
448
+ `${at}: must be a package name, { pattern, flags? } or { module, names }`
449
+ );
450
+ });
451
+ const mutation = section(settings.mutation, ["maxTimeouts"], "settings.mutation");
452
+ if (mutation.maxTimeouts !== void 0 && !(Number.isInteger(mutation.maxTimeouts) && mutation.maxTimeouts >= 0)) {
453
+ throw new SettingsError("settings.mutation.maxTimeouts: must be an integer >= 0");
454
+ }
414
455
  const stryker = section(settings.stryker, ["configFile", "reportFile"], "settings.stryker");
415
456
  const lcov = section(settings.lcov, ["file", "summaryExclude"], "settings.lcov");
416
457
  return {
@@ -432,6 +473,9 @@ function resolveSettings(raw) {
432
473
  selfcheckFailurePattern,
433
474
  strykerConfigFile: optionalString(stryker.configFile, "settings.stryker.configFile"),
434
475
  strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
476
+ importMode: importsSection.mode ?? "blocklist",
477
+ importAllow,
478
+ maxTimeouts: mutation.maxTimeouts ?? null,
435
479
  lcovFile: optionalString(lcov.file, "settings.lcov.file"),
436
480
  lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
437
481
  (pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
package/dist/jest.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  NO_SPEC_PATTERN,
7
7
  gateSpecs,
8
8
  loadGates
9
- } from "./chunk-LMD3ANJ2.js";
9
+ } from "./chunk-XEKQKLKY.js";
10
10
 
11
11
  // src/jest.ts
12
12
  import path from "path";
package/dist/stryker.cjs CHANGED
@@ -70,17 +70,17 @@ function allowanceProblems(entry) {
70
70
  }
71
71
  return problems;
72
72
  }
73
- function validateEquivalentMutants(gatePath, value) {
73
+ function validateEquivalentMutants(gatePath, value, field = "equivalentMutants") {
74
74
  if (value === void 0) {
75
75
  return [];
76
76
  }
77
77
  if (!Array.isArray(value)) {
78
- return [{ file: MANIFEST_FILE, message: `${gatePath}: equivalentMutants must be an array` }];
78
+ return [{ file: MANIFEST_FILE, message: `${gatePath}: ${field} must be an array` }];
79
79
  }
80
80
  return value.flatMap(
81
81
  (entry, index) => allowanceProblems(entry).map((problem) => ({
82
82
  file: MANIFEST_FILE,
83
- message: `${gatePath}: equivalentMutants[${index}]${problem}`
83
+ message: `${gatePath}: ${field}[${index}]${problem}`
84
84
  }))
85
85
  );
86
86
  }
@@ -160,6 +160,9 @@ function parseManifest(text) {
160
160
  }
161
161
  for (const gate of gates) {
162
162
  violations.push(...validateEquivalentMutants(gate.path, gate.equivalentMutants));
163
+ violations.push(
164
+ ...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
165
+ );
163
166
  }
164
167
  return { gates, candidates, settings: raw.settings, violations };
165
168
  }
@@ -309,7 +312,9 @@ function resolveSettings(raw) {
309
312
  "gateCommand",
310
313
  "selfcheck",
311
314
  "stryker",
312
- "lcov"
315
+ "lcov",
316
+ "imports",
317
+ "mutation"
313
318
  ],
314
319
  "settings"
315
320
  );
@@ -410,6 +415,42 @@ function resolveSettings(raw) {
410
415
  "settings.selfcheck.failurePattern"
411
416
  );
412
417
  }
418
+ const importsSection = section(settings.imports, ["mode", "allow"], "settings.imports");
419
+ if (importsSection.mode !== void 0 && importsSection.mode !== "blocklist" && importsSection.mode !== "allowlist") {
420
+ throw new SettingsError('settings.imports.mode: must be "blocklist" or "allowlist"');
421
+ }
422
+ const importAllowRaw = importsSection.allow ?? [];
423
+ if (!Array.isArray(importAllowRaw)) {
424
+ throw new SettingsError("settings.imports.allow: must be an array");
425
+ }
426
+ const importAllow = importAllowRaw.map((item, index) => {
427
+ const at = `settings.imports.allow[${index}]`;
428
+ if (typeof item === "string" && item !== "") {
429
+ return { module: item };
430
+ }
431
+ if (isRecord2(item) && typeof item.pattern === "string" && item.pattern !== "") {
432
+ rejectUnknownKeys(item, ["pattern", "flags"], at);
433
+ if (item.flags !== void 0 && typeof item.flags !== "string") {
434
+ throw new SettingsError(`${at}.flags: must be a string`);
435
+ }
436
+ return { regex: compile(item.pattern, item.flags, at) };
437
+ }
438
+ if (isRecord2(item) && typeof item.module === "string" && item.module !== "") {
439
+ rejectUnknownKeys(item, ["module", "names"], at);
440
+ const names = stringArray(item.names, `${at}.names`);
441
+ if (names.length === 0) {
442
+ throw new SettingsError(`${at}.names: must list at least one name`);
443
+ }
444
+ return { module: item.module, names };
445
+ }
446
+ throw new SettingsError(
447
+ `${at}: must be a package name, { pattern, flags? } or { module, names }`
448
+ );
449
+ });
450
+ const mutation = section(settings.mutation, ["maxTimeouts"], "settings.mutation");
451
+ if (mutation.maxTimeouts !== void 0 && !(Number.isInteger(mutation.maxTimeouts) && mutation.maxTimeouts >= 0)) {
452
+ throw new SettingsError("settings.mutation.maxTimeouts: must be an integer >= 0");
453
+ }
413
454
  const stryker = section(settings.stryker, ["configFile", "reportFile"], "settings.stryker");
414
455
  const lcov = section(settings.lcov, ["file", "summaryExclude"], "settings.lcov");
415
456
  return {
@@ -431,6 +472,9 @@ function resolveSettings(raw) {
431
472
  selfcheckFailurePattern,
432
473
  strykerConfigFile: optionalString(stryker.configFile, "settings.stryker.configFile"),
433
474
  strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
475
+ importMode: importsSection.mode ?? "blocklist",
476
+ importAllow,
477
+ maxTimeouts: mutation.maxTimeouts ?? null,
434
478
  lcovFile: optionalString(lcov.file, "settings.lcov.file"),
435
479
  lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
436
480
  (pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
package/dist/stryker.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-4HAJIKDZ.js";
4
4
  import {
5
5
  loadGates
6
- } from "./chunk-LMD3ANJ2.js";
6
+ } from "./chunk-XEKQKLKY.js";
7
7
 
8
8
  // src/stryker.ts
9
9
  var WEAKENING_OPTIONS = ["ignoreStatic", "ignorers"];
package/dist/vitest.cjs CHANGED
@@ -70,17 +70,17 @@ function allowanceProblems(entry) {
70
70
  }
71
71
  return problems;
72
72
  }
73
- function validateEquivalentMutants(gatePath, value) {
73
+ function validateEquivalentMutants(gatePath, value, field = "equivalentMutants") {
74
74
  if (value === void 0) {
75
75
  return [];
76
76
  }
77
77
  if (!Array.isArray(value)) {
78
- return [{ file: MANIFEST_FILE, message: `${gatePath}: equivalentMutants must be an array` }];
78
+ return [{ file: MANIFEST_FILE, message: `${gatePath}: ${field} must be an array` }];
79
79
  }
80
80
  return value.flatMap(
81
81
  (entry, index) => allowanceProblems(entry).map((problem) => ({
82
82
  file: MANIFEST_FILE,
83
- message: `${gatePath}: equivalentMutants[${index}]${problem}`
83
+ message: `${gatePath}: ${field}[${index}]${problem}`
84
84
  }))
85
85
  );
86
86
  }
@@ -160,6 +160,9 @@ function parseManifest(text) {
160
160
  }
161
161
  for (const gate of gates) {
162
162
  violations.push(...validateEquivalentMutants(gate.path, gate.equivalentMutants));
163
+ violations.push(
164
+ ...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
165
+ );
163
166
  }
164
167
  return { gates, candidates, settings: raw.settings, violations };
165
168
  }
@@ -309,7 +312,9 @@ function resolveSettings(raw) {
309
312
  "gateCommand",
310
313
  "selfcheck",
311
314
  "stryker",
312
- "lcov"
315
+ "lcov",
316
+ "imports",
317
+ "mutation"
313
318
  ],
314
319
  "settings"
315
320
  );
@@ -410,6 +415,42 @@ function resolveSettings(raw) {
410
415
  "settings.selfcheck.failurePattern"
411
416
  );
412
417
  }
418
+ const importsSection = section(settings.imports, ["mode", "allow"], "settings.imports");
419
+ if (importsSection.mode !== void 0 && importsSection.mode !== "blocklist" && importsSection.mode !== "allowlist") {
420
+ throw new SettingsError('settings.imports.mode: must be "blocklist" or "allowlist"');
421
+ }
422
+ const importAllowRaw = importsSection.allow ?? [];
423
+ if (!Array.isArray(importAllowRaw)) {
424
+ throw new SettingsError("settings.imports.allow: must be an array");
425
+ }
426
+ const importAllow = importAllowRaw.map((item, index) => {
427
+ const at = `settings.imports.allow[${index}]`;
428
+ if (typeof item === "string" && item !== "") {
429
+ return { module: item };
430
+ }
431
+ if (isRecord2(item) && typeof item.pattern === "string" && item.pattern !== "") {
432
+ rejectUnknownKeys(item, ["pattern", "flags"], at);
433
+ if (item.flags !== void 0 && typeof item.flags !== "string") {
434
+ throw new SettingsError(`${at}.flags: must be a string`);
435
+ }
436
+ return { regex: compile(item.pattern, item.flags, at) };
437
+ }
438
+ if (isRecord2(item) && typeof item.module === "string" && item.module !== "") {
439
+ rejectUnknownKeys(item, ["module", "names"], at);
440
+ const names = stringArray(item.names, `${at}.names`);
441
+ if (names.length === 0) {
442
+ throw new SettingsError(`${at}.names: must list at least one name`);
443
+ }
444
+ return { module: item.module, names };
445
+ }
446
+ throw new SettingsError(
447
+ `${at}: must be a package name, { pattern, flags? } or { module, names }`
448
+ );
449
+ });
450
+ const mutation = section(settings.mutation, ["maxTimeouts"], "settings.mutation");
451
+ if (mutation.maxTimeouts !== void 0 && !(Number.isInteger(mutation.maxTimeouts) && mutation.maxTimeouts >= 0)) {
452
+ throw new SettingsError("settings.mutation.maxTimeouts: must be an integer >= 0");
453
+ }
413
454
  const stryker = section(settings.stryker, ["configFile", "reportFile"], "settings.stryker");
414
455
  const lcov = section(settings.lcov, ["file", "summaryExclude"], "settings.lcov");
415
456
  return {
@@ -431,6 +472,9 @@ function resolveSettings(raw) {
431
472
  selfcheckFailurePattern,
432
473
  strykerConfigFile: optionalString(stryker.configFile, "settings.stryker.configFile"),
433
474
  strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
475
+ importMode: importsSection.mode ?? "blocklist",
476
+ importAllow,
477
+ maxTimeouts: mutation.maxTimeouts ?? null,
434
478
  lcovFile: optionalString(lcov.file, "settings.lcov.file"),
435
479
  lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
436
480
  (pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
@@ -561,7 +605,7 @@ function resolveRootDir(rootDir) {
561
605
 
562
606
  // src/vitest.ts
563
607
  function createVitestGatesConfig(options = {}) {
564
- const { rootDir: rootDirOption, test = {}, ...overrides } = options;
608
+ const { rootDir: rootDirOption, coverageProvider, test = {}, ...overrides } = options;
565
609
  const rootDir = resolveRootDir(rootDirOption);
566
610
  const manifest = loadGates(rootDir);
567
611
  const { coverage = {}, ...testOverrides } = test;
@@ -581,6 +625,7 @@ function createVitestGatesConfig(options = {}) {
581
625
  reporter: ["text"],
582
626
  reportsDirectory: "coverage/gates",
583
627
  ...coverage,
628
+ ...coverageProvider === void 0 ? {} : { provider: coverageProvider },
584
629
  enabled: true,
585
630
  include: manifest.gates.map((gate) => gate.path),
586
631
  thresholds: { perFile: true, ...FULL_COVERAGE }