@t09tanaka/test-gates 0.1.0 → 0.3.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-F7WBEFIL.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];
@@ -239,6 +301,21 @@ function scanStrykerConfig(configPath, source) {
239
301
  return violations;
240
302
  }
241
303
 
304
+ // src/core/vitest-coverage.ts
305
+ function vitestCoverageViolation(input) {
306
+ if (input.vitestVersion === null || input.vitestVersion === "") {
307
+ return null;
308
+ }
309
+ const major = Number(input.vitestVersion.split(".")[0]);
310
+ if (!Number.isInteger(major) || major >= 4) {
311
+ return null;
312
+ }
313
+ if (/\bistanbul\b/.test(input.configSource) || /\bexperimentalAstAwareRemapping\s*:\s*true\b/.test(input.configSource)) {
314
+ return null;
315
+ }
316
+ return `Vitest ${input.vitestVersion} with the v8 coverage provider does not count the skipped side of an "if" without "else", so a gate can show 100% branches with that side untested. Use coverageProvider: 'istanbul' (needs @vitest/coverage-istanbul), set coverage.experimentalAstAwareRemapping: true, or move to Vitest 4`;
317
+ }
318
+
242
319
  // src/commands/check.ts
243
320
  var STRYKER_CONFIG_NAMES = [
244
321
  "stryker.gates.config.mjs",
@@ -329,8 +406,40 @@ function checkGates(manifest) {
329
406
  violations.push(...scanStrykerConfig(strykerConfig, readText(dir, strykerConfig)));
330
407
  }
331
408
  }
409
+ violations.push(...checkVitestCoverage(manifest));
332
410
  return violations;
333
411
  }
412
+ var VITEST_GATE_CONFIGS = ["ts", "mts", "cts", "js", "mjs", "cjs"].map(
413
+ (extension) => `vitest.gates.config.${extension}`
414
+ );
415
+ function installedVitestVersion(startDir) {
416
+ let current = startDir;
417
+ for (; ; ) {
418
+ try {
419
+ const pkg = JSON.parse(
420
+ fs.readFileSync(path.join(current, "node_modules", "vitest", "package.json"), "utf8")
421
+ );
422
+ return typeof pkg.version === "string" ? pkg.version : null;
423
+ } catch {
424
+ const parent = path.dirname(current);
425
+ if (parent === current) {
426
+ return null;
427
+ }
428
+ current = parent;
429
+ }
430
+ }
431
+ }
432
+ function checkVitestCoverage(manifest) {
433
+ const config = VITEST_GATE_CONFIGS.find((name) => fs.existsSync(path.join(manifest.dir, name)));
434
+ if (!config) {
435
+ return [];
436
+ }
437
+ const message = vitestCoverageViolation({
438
+ vitestVersion: installedVitestVersion(manifest.dir),
439
+ configSource: readText(manifest.dir, config)
440
+ });
441
+ return message === null ? [] : [{ file: config, message }];
442
+ }
334
443
 
335
444
  // src/core/mutation-result.ts
336
445
  var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
@@ -365,6 +474,27 @@ function sliceByLocation(source, location) {
365
474
  function keyOf(mutator, original, replacement) {
366
475
  return JSON.stringify([mutator, normalizeCode(original), normalizeCode(replacement)]);
367
476
  }
477
+ function describeMutant(file, entry) {
478
+ const { mutant } = entry;
479
+ return {
480
+ file,
481
+ line: mutant.location.start.line,
482
+ column: mutant.location.start.column,
483
+ status: mutant.status,
484
+ mutator: mutant.mutatorName,
485
+ original: normalizeCode(entry.original),
486
+ replacement: normalizeCode(entry.replacement),
487
+ occurrence: entry.occurrence,
488
+ sameKeyCount: entry.sameKeyCount,
489
+ allowance: {
490
+ mutator: mutant.mutatorName,
491
+ original: normalizeCode(entry.original),
492
+ replacement: normalizeCode(entry.replacement),
493
+ ...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
494
+ reason: ""
495
+ }
496
+ };
497
+ }
368
498
  function describeAllowance(entry) {
369
499
  const occurrence = entry.occurrence === void 0 ? "" : ` / occurrence ${entry.occurrence}`;
370
500
  return `${entry.mutator} / ${normalizeCode(entry.original)} \u2192 ${normalizeCode(entry.replacement)}${occurrence}`;
@@ -372,13 +502,15 @@ function describeAllowance(entry) {
372
502
  function judgeMutationReport(input) {
373
503
  const violations = [];
374
504
  const survivors = [];
505
+ const unexpectedTimeouts = [];
375
506
  const summary = {
376
507
  total: 0,
377
508
  detected: 0,
378
509
  timeout: 0,
379
510
  allowed: 0,
380
511
  notEvaluable: 0,
381
- unallowed: 0
512
+ unallowed: 0,
513
+ expectedTimeout: 0
382
514
  };
383
515
  const files = /* @__PURE__ */ new Map();
384
516
  for (const [file, result] of Object.entries(input.report.files ?? {})) {
@@ -388,7 +520,9 @@ function judgeMutationReport(input) {
388
520
  const gatePath = normalizePath(gate.path);
389
521
  const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
390
522
  violations.push(...shapeViolations);
391
- const allowances = Array.isArray(gate.equivalentMutants) ? gate.equivalentMutants : [];
523
+ violations.push(
524
+ ...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
525
+ );
392
526
  const result = files.get(gatePath);
393
527
  if (!result || !Array.isArray(result.mutants) || result.mutants.length === 0) {
394
528
  violations.push({
@@ -452,38 +586,53 @@ function judgeMutationReport(input) {
452
586
  if (!locationsValid) {
453
587
  continue;
454
588
  }
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;
589
+ const claim = (field, entries, noun, accept) => {
590
+ const claimed = /* @__PURE__ */ new Set();
591
+ if (!Array.isArray(entries)) {
592
+ return claimed;
468
593
  }
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;
477
- }
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
- });
594
+ entries.forEach((entry, index) => {
595
+ if (!isUsableEquivalentMutant(entry)) {
596
+ return;
597
+ }
598
+ const at = `${gate.path}: ${field}[${index}] (${describeAllowance(entry)})`;
599
+ const group = byKey.get(keyOf(entry.mutator, entry.original, entry.replacement)) ?? [];
600
+ if (entry.occurrence === void 0 && group.length > 1) {
601
+ violations.push({
602
+ file: MANIFEST_FILE,
603
+ message: `${at}: matches ${group.length} mutants in the file. Add "occurrence" (1-based) to pick one`
604
+ });
605
+ return;
606
+ }
607
+ const target = group[(entry.occurrence ?? 1) - 1];
608
+ const refusal = target ? accept(target.mutant.status) : `stale ${noun} (no such mutant in this run). Remove it`;
609
+ if (!target || refusal !== null) {
610
+ violations.push({ file: MANIFEST_FILE, message: `${at}: ${refusal}` });
611
+ return;
612
+ }
613
+ if (claimed.has(target)) {
614
+ violations.push({
615
+ file: MANIFEST_FILE,
616
+ message: `${at}: another ${noun} already covers this mutant`
617
+ });
618
+ return;
619
+ }
620
+ claimed.add(target);
621
+ });
622
+ return claimed;
623
+ };
624
+ const allowed = claim(
625
+ "equivalentMutants",
626
+ gate.equivalentMutants,
627
+ "allowance",
628
+ (status) => SURVIVED.has(status) ? null : `stale allowance (the mutant is now ${status}). Remove it`
629
+ );
630
+ const expected = claim(
631
+ "expectedTimeouts",
632
+ gate.expectedTimeouts,
633
+ "entry",
634
+ (status) => SURVIVED.has(status) ? `the mutant ${status === "NoCoverage" ? "has no coverage" : "survived"} instead of timing out. Detect it with a test` : null
635
+ );
487
636
  for (const entry of located) {
488
637
  const { mutant } = entry;
489
638
  summary.total += 1;
@@ -491,6 +640,11 @@ function judgeMutationReport(input) {
491
640
  summary.detected += 1;
492
641
  if (mutant.status === "Timeout") {
493
642
  summary.timeout += 1;
643
+ if (expected.has(entry)) {
644
+ summary.expectedTimeout += 1;
645
+ } else {
646
+ unexpectedTimeouts.push(describeMutant(gate.path, entry));
647
+ }
494
648
  }
495
649
  } else if (NOT_EVALUABLE.has(mutant.status)) {
496
650
  summary.notEvaluable += 1;
@@ -500,24 +654,7 @@ function judgeMutationReport(input) {
500
654
  continue;
501
655
  }
502
656
  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
- });
657
+ survivors.push(describeMutant(gate.path, entry));
521
658
  } else {
522
659
  violations.push({
523
660
  file: gate.path,
@@ -533,7 +670,14 @@ function judgeMutationReport(input) {
533
670
  message: "no mutants were evaluated (the mutation test ran on nothing)"
534
671
  });
535
672
  }
536
- return { violations, survivors, summary };
673
+ const maxTimeouts = input.maxTimeouts ?? 0;
674
+ if (unexpectedTimeouts.length > maxTimeouts) {
675
+ violations.push({
676
+ file: MANIFEST_FILE,
677
+ message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
678
+ });
679
+ }
680
+ return { violations, survivors, unexpectedTimeouts, summary };
537
681
  }
538
682
  function formatSurvivor(survivor) {
539
683
  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
  }
@@ -209,8 +212,13 @@ var DEFAULT_FORBIDDEN_SOURCE = [
209
212
  {
210
213
  pattern: `^\\s*(['"])use (client|server)\\1`,
211
214
  reason: "'use client' / 'use server' directive (not a pure module)"
215
+ },
216
+ {
217
+ pattern: "\\bprocess\\??\\.env\\b",
218
+ reason: "reads process.env (not a pure module). Take the value as an argument"
212
219
  }
213
220
  ];
221
+ var DEFAULT_MAX_TIMEOUTS = 0;
214
222
  var SettingsError = class extends Error {
215
223
  };
216
224
  function isRecord2(value) {
@@ -310,7 +318,9 @@ function resolveSettings(raw) {
310
318
  "gateCommand",
311
319
  "selfcheck",
312
320
  "stryker",
313
- "lcov"
321
+ "lcov",
322
+ "imports",
323
+ "mutation"
314
324
  ],
315
325
  "settings"
316
326
  );
@@ -411,6 +421,42 @@ function resolveSettings(raw) {
411
421
  "settings.selfcheck.failurePattern"
412
422
  );
413
423
  }
424
+ const importsSection = section(settings.imports, ["mode", "allow"], "settings.imports");
425
+ if (importsSection.mode !== void 0 && importsSection.mode !== "blocklist" && importsSection.mode !== "allowlist") {
426
+ throw new SettingsError('settings.imports.mode: must be "blocklist" or "allowlist"');
427
+ }
428
+ const importAllowRaw = importsSection.allow ?? [];
429
+ if (!Array.isArray(importAllowRaw)) {
430
+ throw new SettingsError("settings.imports.allow: must be an array");
431
+ }
432
+ const importAllow = importAllowRaw.map((item, index) => {
433
+ const at = `settings.imports.allow[${index}]`;
434
+ if (typeof item === "string" && item !== "") {
435
+ return { module: item };
436
+ }
437
+ if (isRecord2(item) && typeof item.pattern === "string" && item.pattern !== "") {
438
+ rejectUnknownKeys(item, ["pattern", "flags"], at);
439
+ if (item.flags !== void 0 && typeof item.flags !== "string") {
440
+ throw new SettingsError(`${at}.flags: must be a string`);
441
+ }
442
+ return { regex: compile(item.pattern, item.flags, at) };
443
+ }
444
+ if (isRecord2(item) && typeof item.module === "string" && item.module !== "") {
445
+ rejectUnknownKeys(item, ["module", "names"], at);
446
+ const names = stringArray(item.names, `${at}.names`);
447
+ if (names.length === 0) {
448
+ throw new SettingsError(`${at}.names: must list at least one name`);
449
+ }
450
+ return { module: item.module, names };
451
+ }
452
+ throw new SettingsError(
453
+ `${at}: must be a package name, { pattern, flags? } or { module, names }`
454
+ );
455
+ });
456
+ const mutation = section(settings.mutation, ["maxTimeouts"], "settings.mutation");
457
+ if (mutation.maxTimeouts !== void 0 && !(Number.isInteger(mutation.maxTimeouts) && mutation.maxTimeouts >= 0)) {
458
+ throw new SettingsError("settings.mutation.maxTimeouts: must be an integer >= 0");
459
+ }
414
460
  const stryker = section(settings.stryker, ["configFile", "reportFile"], "settings.stryker");
415
461
  const lcov = section(settings.lcov, ["file", "summaryExclude"], "settings.lcov");
416
462
  return {
@@ -432,6 +478,9 @@ function resolveSettings(raw) {
432
478
  selfcheckFailurePattern,
433
479
  strykerConfigFile: optionalString(stryker.configFile, "settings.stryker.configFile"),
434
480
  strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
481
+ importMode: importsSection.mode ?? "blocklist",
482
+ importAllow,
483
+ maxTimeouts: mutation.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS,
435
484
  lcovFile: optionalString(lcov.file, "settings.lcov.file"),
436
485
  lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
437
486
  (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-F7WBEFIL.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
  }
@@ -208,8 +211,13 @@ var DEFAULT_FORBIDDEN_SOURCE = [
208
211
  {
209
212
  pattern: `^\\s*(['"])use (client|server)\\1`,
210
213
  reason: "'use client' / 'use server' directive (not a pure module)"
214
+ },
215
+ {
216
+ pattern: "\\bprocess\\??\\.env\\b",
217
+ reason: "reads process.env (not a pure module). Take the value as an argument"
211
218
  }
212
219
  ];
220
+ var DEFAULT_MAX_TIMEOUTS = 0;
213
221
  var SettingsError = class extends Error {
214
222
  };
215
223
  function isRecord2(value) {
@@ -309,7 +317,9 @@ function resolveSettings(raw) {
309
317
  "gateCommand",
310
318
  "selfcheck",
311
319
  "stryker",
312
- "lcov"
320
+ "lcov",
321
+ "imports",
322
+ "mutation"
313
323
  ],
314
324
  "settings"
315
325
  );
@@ -410,6 +420,42 @@ function resolveSettings(raw) {
410
420
  "settings.selfcheck.failurePattern"
411
421
  );
412
422
  }
423
+ const importsSection = section(settings.imports, ["mode", "allow"], "settings.imports");
424
+ if (importsSection.mode !== void 0 && importsSection.mode !== "blocklist" && importsSection.mode !== "allowlist") {
425
+ throw new SettingsError('settings.imports.mode: must be "blocklist" or "allowlist"');
426
+ }
427
+ const importAllowRaw = importsSection.allow ?? [];
428
+ if (!Array.isArray(importAllowRaw)) {
429
+ throw new SettingsError("settings.imports.allow: must be an array");
430
+ }
431
+ const importAllow = importAllowRaw.map((item, index) => {
432
+ const at = `settings.imports.allow[${index}]`;
433
+ if (typeof item === "string" && item !== "") {
434
+ return { module: item };
435
+ }
436
+ if (isRecord2(item) && typeof item.pattern === "string" && item.pattern !== "") {
437
+ rejectUnknownKeys(item, ["pattern", "flags"], at);
438
+ if (item.flags !== void 0 && typeof item.flags !== "string") {
439
+ throw new SettingsError(`${at}.flags: must be a string`);
440
+ }
441
+ return { regex: compile(item.pattern, item.flags, at) };
442
+ }
443
+ if (isRecord2(item) && typeof item.module === "string" && item.module !== "") {
444
+ rejectUnknownKeys(item, ["module", "names"], at);
445
+ const names = stringArray(item.names, `${at}.names`);
446
+ if (names.length === 0) {
447
+ throw new SettingsError(`${at}.names: must list at least one name`);
448
+ }
449
+ return { module: item.module, names };
450
+ }
451
+ throw new SettingsError(
452
+ `${at}: must be a package name, { pattern, flags? } or { module, names }`
453
+ );
454
+ });
455
+ const mutation = section(settings.mutation, ["maxTimeouts"], "settings.mutation");
456
+ if (mutation.maxTimeouts !== void 0 && !(Number.isInteger(mutation.maxTimeouts) && mutation.maxTimeouts >= 0)) {
457
+ throw new SettingsError("settings.mutation.maxTimeouts: must be an integer >= 0");
458
+ }
413
459
  const stryker = section(settings.stryker, ["configFile", "reportFile"], "settings.stryker");
414
460
  const lcov = section(settings.lcov, ["file", "summaryExclude"], "settings.lcov");
415
461
  return {
@@ -431,6 +477,9 @@ function resolveSettings(raw) {
431
477
  selfcheckFailurePattern,
432
478
  strykerConfigFile: optionalString(stryker.configFile, "settings.stryker.configFile"),
433
479
  strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
480
+ importMode: importsSection.mode ?? "blocklist",
481
+ importAllow,
482
+ maxTimeouts: mutation.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS,
434
483
  lcovFile: optionalString(lcov.file, "settings.lcov.file"),
435
484
  lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
436
485
  (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-F7WBEFIL.js";
7
7
 
8
8
  // src/stryker.ts
9
9
  var WEAKENING_OPTIONS = ["ignoreStatic", "ignorers"];