@t09tanaka/test-gates 0.4.1 → 0.4.2

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/cli.js CHANGED
@@ -319,14 +319,17 @@ function parseImports(source) {
319
319
  const clause = match[3];
320
320
  const module = match[5];
321
321
  const braces = /\{([^}]*)\}/.exec(clause);
322
- const names = braces ? braces[1].split(",").map((part) => part.trim()).filter((part) => part !== "" && !/^type\s/.test(part)).map((part) => part.split(/\s+as\s+/)[0].trim()) : [];
322
+ const entries = braces ? braces[1].split(",").map((part) => part.trim()).filter((part) => part !== "") : [];
323
+ const names = entries.filter((part) => !/^type\s/.test(part)).map((part) => part.split(/\s+as\s+/)[0].trim());
323
324
  const outsideBraces = clause.replace(/\{[^}]*\}/, "").replace(/,/g, "").trim();
324
325
  found.push({
325
326
  module,
326
327
  line: lineAt(source, match.index + match[0].lastIndexOf(module)),
327
328
  typeOnly: match[2] !== void 0,
328
329
  names,
329
- whole: outsideBraces !== ""
330
+ whole: outsideBraces !== "",
331
+ ...match[2] === void 0 && outsideBraces === "" && entries.length > 0 && // `type as kind` imports the value named `type`.
332
+ entries.every((part) => /^type\s+(?!as\s)[\w$]/.test(part)) ? { inlineTypes: true } : {}
330
333
  });
331
334
  }
332
335
  for (const match of source.matchAll(SIDE_EFFECT_IMPORT)) {
@@ -341,6 +344,9 @@ function parseImports(source) {
341
344
  }
342
345
  for (const match of source.matchAll(CALL_IMPORT)) {
343
346
  const module = match[2];
347
+ if (match[1] === "`" && module.includes("${")) {
348
+ continue;
349
+ }
344
350
  found.push({
345
351
  module,
346
352
  line: lineAt(source, match.index + match[0].lastIndexOf(module)),
@@ -351,7 +357,7 @@ function parseImports(source) {
351
357
  }
352
358
  return found.sort((a, b) => a.line - b.line);
353
359
  }
354
- var COMPUTED_CALL_IMPORT = /(?<![.\w$])(?:require|import)\s*\(\s*(?!['"`\s)])/g;
360
+ var COMPUTED_CALL_IMPORT = /(?<![.\w$])(?:require|import)\s*\((?!\s*(?:\)|(?:'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"|`(?:[^`\\$]|\\.|\$(?!\{))*`)\s*\)))/g;
355
361
  function findUncheckableImports(source) {
356
362
  return [...source.matchAll(COMPUTED_CALL_IMPORT)].map((match) => lineAt(source, match.index));
357
363
  }
@@ -378,7 +384,11 @@ function resolveSpecifier(specifier, fromFile, aliases) {
378
384
  const segments = [];
379
385
  for (const segment of joined.split("/")) {
380
386
  if (segment === "..") {
381
- segments.pop();
387
+ if (segments.length === 0 || segments[segments.length - 1] === "..") {
388
+ segments.push(segment);
389
+ } else {
390
+ segments.pop();
391
+ }
382
392
  } else if (segment !== "." && segment !== "") {
383
393
  segments.push(segment);
384
394
  }
@@ -423,8 +433,8 @@ function findImporters(gatePath, others, aliases) {
423
433
  if (imported.typeOnly) {
424
434
  return false;
425
435
  }
426
- const resolved = resolveSpecifier(imported.module, other.path, aliases);
427
- return resolved === target || resolved === asDirectory;
436
+ const resolved2 = resolveSpecifier(imported.module, other.path, aliases);
437
+ return resolved2 === target || resolved2 === asDirectory;
428
438
  })
429
439
  ).map((other) => other.path);
430
440
  }
@@ -555,8 +565,8 @@ function scanSpecSource(gatePath, specPath, source, settings) {
555
565
  const gateDirectory = gateModule.replace(/\/index$/, "");
556
566
  for (const match of source.matchAll(MODULE_MOCK)) {
557
567
  const specifier = match[2];
558
- const resolved = resolveSpecifier(specifier, specPath, settings.importAliases);
559
- if (resolved === gateModule || resolved === gateDirectory) {
568
+ const resolved2 = resolveSpecifier(specifier, specPath, settings.importAliases);
569
+ if (resolved2 === gateModule || resolved2 === gateDirectory) {
560
570
  let line = 1;
561
571
  for (let i = 0; i < match.index; i += 1) {
562
572
  if (source[i] === "\n") {
@@ -1183,8 +1193,8 @@ function planControls(input) {
1183
1193
  if (silent !== void 0) {
1184
1194
  return everyoneAlone(`the gate run left no note for the spec of ${silent.path}`);
1185
1195
  }
1186
- const partsOf = (gate) => notes.flatMap((note) => gate.file in note.files ? [note.files[gate.file]] : []);
1187
- const short = all.find((gate) => accountedFor(totals[gate.file], partsOf(gate)) === "no");
1196
+ const partsOf2 = (gate) => notes.flatMap((note) => gate.file in note.files ? [note.files[gate.file]] : []);
1197
+ const short = all.find((gate) => accountedFor(totals[gate.file], partsOf2(gate)) === "no");
1188
1198
  if (short !== void 0) {
1189
1199
  return everyoneAlone(
1190
1200
  `the notes of the specs do not add up to what the runner counted for ${short.path}`
@@ -1198,7 +1208,7 @@ function planControls(input) {
1198
1208
  if (all.some((other) => other.path !== gate.path && other.path.includes(gate.path))) {
1199
1209
  return false;
1200
1210
  }
1201
- if (accountedFor(totals[gate.file], partsOf(gate)) !== "yes") {
1211
+ if (accountedFor(totals[gate.file], partsOf2(gate)) !== "yes") {
1202
1212
  return false;
1203
1213
  }
1204
1214
  const touching = notes.filter((note) => {
@@ -1413,6 +1423,279 @@ import { spawnSync as spawnSync3 } from "child_process";
1413
1423
  import fs9 from "fs";
1414
1424
  import path9 from "path";
1415
1425
 
1426
+ // src/core/mutation-result.ts
1427
+ var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
1428
+ var SURVIVED = /* @__PURE__ */ new Set(["Survived", "NoCoverage"]);
1429
+ var NOT_EVALUABLE = /* @__PURE__ */ new Set(["RuntimeError", "CompileError"]);
1430
+ function normalizeCode(text) {
1431
+ return text.replace(/\s+/g, " ").trim();
1432
+ }
1433
+ function normalizePath(file) {
1434
+ return file.replace(/\\/g, "/").replace(/^\.\//, "");
1435
+ }
1436
+ function normalizeNewlines(text) {
1437
+ return text.replace(/\r\n/g, "\n");
1438
+ }
1439
+ function sliceByLocation(source, location) {
1440
+ const lines = source.split("\n");
1441
+ const { start, end } = location;
1442
+ const first = lines[start.line - 1];
1443
+ const last = lines[end.line - 1];
1444
+ if (first === void 0 || last === void 0 || start.column < 1 || end.column < 1 || end.line < start.line || end.line === start.line && end.column < start.column) {
1445
+ return null;
1446
+ }
1447
+ if (start.line === end.line) {
1448
+ return first.slice(start.column - 1, end.column - 1);
1449
+ }
1450
+ return [
1451
+ first.slice(start.column - 1),
1452
+ ...lines.slice(start.line, end.line - 1),
1453
+ last.slice(0, end.column - 1)
1454
+ ].join("\n");
1455
+ }
1456
+ function keyOf(mutator, original, replacement) {
1457
+ return JSON.stringify([mutator, normalizeCode(original), normalizeCode(replacement)]);
1458
+ }
1459
+ function locateMutants(source, mutants) {
1460
+ const located = [];
1461
+ const byKey = /* @__PURE__ */ new Map();
1462
+ const outside = [];
1463
+ const sorted2 = [...mutants].sort(
1464
+ (a, b) => a.location.start.line - b.location.start.line || a.location.start.column - b.location.start.column
1465
+ );
1466
+ for (const mutant of sorted2) {
1467
+ const original = sliceByLocation(normalizeNewlines(source), mutant.location);
1468
+ if (original === null) {
1469
+ outside.push(mutant);
1470
+ continue;
1471
+ }
1472
+ const replacement = mutant.replacement ?? "";
1473
+ const key = keyOf(mutant.mutatorName, original, replacement);
1474
+ const group = byKey.get(key) ?? [];
1475
+ const entry = { mutant, original, replacement, occurrence: group.length + 1, sameKeyCount: 0 };
1476
+ group.push(entry);
1477
+ byKey.set(key, group);
1478
+ located.push(entry);
1479
+ }
1480
+ for (const group of byKey.values()) {
1481
+ for (const entry of group) {
1482
+ entry.sameKeyCount = group.length;
1483
+ }
1484
+ }
1485
+ return { located, byKey, outside };
1486
+ }
1487
+ function resolveEntry(entry, byKey) {
1488
+ const group = byKey.get(keyOf(entry.mutator, entry.original, entry.replacement)) ?? [];
1489
+ if (entry.occurrence === void 0 && group.length > 1) {
1490
+ return { ambiguous: group.length };
1491
+ }
1492
+ return { target: group[(entry.occurrence ?? 1) - 1] };
1493
+ }
1494
+ function expectedTimeoutMutants(gate, source, mutants) {
1495
+ const pointed = /* @__PURE__ */ new Set();
1496
+ if (source === void 0 || !Array.isArray(gate.expectedTimeouts)) {
1497
+ return pointed;
1498
+ }
1499
+ const { byKey, outside } = locateMutants(source, mutants);
1500
+ if (outside.length > 0) {
1501
+ return pointed;
1502
+ }
1503
+ for (const entry of gate.expectedTimeouts) {
1504
+ if (!isUsableEquivalentMutant(entry)) {
1505
+ continue;
1506
+ }
1507
+ const found = resolveEntry(entry, byKey);
1508
+ if ("target" in found && found.target !== void 0) {
1509
+ pointed.add(found.target.mutant);
1510
+ }
1511
+ }
1512
+ return pointed;
1513
+ }
1514
+ function describeMutant(file, entry) {
1515
+ const { mutant } = entry;
1516
+ return {
1517
+ file,
1518
+ line: mutant.location.start.line,
1519
+ column: mutant.location.start.column,
1520
+ status: mutant.status,
1521
+ mutator: mutant.mutatorName,
1522
+ original: normalizeCode(entry.original),
1523
+ replacement: normalizeCode(entry.replacement),
1524
+ occurrence: entry.occurrence,
1525
+ sameKeyCount: entry.sameKeyCount,
1526
+ allowance: {
1527
+ mutator: mutant.mutatorName,
1528
+ original: normalizeCode(entry.original),
1529
+ replacement: normalizeCode(entry.replacement),
1530
+ ...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
1531
+ reason: ""
1532
+ }
1533
+ };
1534
+ }
1535
+ function describeAllowance(entry) {
1536
+ const occurrence = entry.occurrence === void 0 ? "" : ` / occurrence ${entry.occurrence}`;
1537
+ return `${entry.mutator} / ${normalizeCode(entry.original)} \u2192 ${normalizeCode(entry.replacement)}${occurrence}`;
1538
+ }
1539
+ function judgeMutationReport(input) {
1540
+ const violations = [];
1541
+ const survivors = [];
1542
+ const unexpectedTimeouts = [];
1543
+ const summary = {
1544
+ total: 0,
1545
+ detected: 0,
1546
+ timeout: 0,
1547
+ allowed: 0,
1548
+ notEvaluable: 0,
1549
+ unallowed: 0,
1550
+ expectedTimeout: 0
1551
+ };
1552
+ const files = /* @__PURE__ */ new Map();
1553
+ for (const [file, result] of Object.entries(input.report.files ?? {})) {
1554
+ files.set(normalizePath(file), result);
1555
+ }
1556
+ for (const gate of input.gates) {
1557
+ const gatePath = normalizePath(gate.path);
1558
+ const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
1559
+ violations.push(...shapeViolations);
1560
+ violations.push(
1561
+ ...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
1562
+ );
1563
+ const result = files.get(gatePath);
1564
+ if (!result || !Array.isArray(result.mutants) || result.mutants.length === 0) {
1565
+ violations.push({
1566
+ file: gate.path,
1567
+ message: "no mutants in the report (the gate was not mutation tested)"
1568
+ });
1569
+ continue;
1570
+ }
1571
+ const diskSource = input.readSource(gate.path);
1572
+ const source = result.source ?? diskSource;
1573
+ if (source === void 0) {
1574
+ violations.push({
1575
+ file: gate.path,
1576
+ message: "source is neither in the report nor readable on disk"
1577
+ });
1578
+ continue;
1579
+ }
1580
+ if (result.source !== void 0 && diskSource !== void 0 && normalizeNewlines(result.source) !== normalizeNewlines(diskSource)) {
1581
+ violations.push({
1582
+ file: gate.path,
1583
+ message: "the report was made from a different version of this file. Run Stryker again"
1584
+ });
1585
+ continue;
1586
+ }
1587
+ const { located, byKey, outside } = locateMutants(source, result.mutants);
1588
+ for (const mutant of outside) {
1589
+ violations.push({
1590
+ file: gate.path,
1591
+ line: mutant.location.start.line,
1592
+ message: `the report location of a ${mutant.mutatorName} mutant is outside the source`
1593
+ });
1594
+ }
1595
+ if (outside.length > 0) {
1596
+ continue;
1597
+ }
1598
+ const claim = (field, entries, noun, accept) => {
1599
+ const claimed = /* @__PURE__ */ new Set();
1600
+ if (!Array.isArray(entries)) {
1601
+ return claimed;
1602
+ }
1603
+ entries.forEach((entry, index) => {
1604
+ if (!isUsableEquivalentMutant(entry)) {
1605
+ return;
1606
+ }
1607
+ const at = `${gate.path}: ${field}[${index}] (${describeAllowance(entry)})`;
1608
+ const found = resolveEntry(entry, byKey);
1609
+ if ("ambiguous" in found) {
1610
+ violations.push({
1611
+ file: MANIFEST_FILE,
1612
+ message: `${at}: matches ${found.ambiguous} mutants in the file. Add "occurrence" (1-based) to pick one`
1613
+ });
1614
+ return;
1615
+ }
1616
+ const target = found.target;
1617
+ const refusal = target ? accept(target.mutant.status) : `stale ${noun} (no such mutant in this run). Remove it`;
1618
+ if (!target || refusal !== null) {
1619
+ violations.push({ file: MANIFEST_FILE, message: `${at}: ${refusal}` });
1620
+ return;
1621
+ }
1622
+ if (claimed.has(target)) {
1623
+ violations.push({
1624
+ file: MANIFEST_FILE,
1625
+ message: `${at}: another ${noun} already covers this mutant`
1626
+ });
1627
+ return;
1628
+ }
1629
+ claimed.add(target);
1630
+ });
1631
+ return claimed;
1632
+ };
1633
+ const allowed = claim(
1634
+ "equivalentMutants",
1635
+ gate.equivalentMutants,
1636
+ "allowance",
1637
+ (status) => SURVIVED.has(status) ? null : `stale allowance (the mutant is now ${status}). Remove it`
1638
+ );
1639
+ const expected = claim(
1640
+ "expectedTimeouts",
1641
+ gate.expectedTimeouts,
1642
+ "entry",
1643
+ (status) => SURVIVED.has(status) ? `the mutant ${status === "NoCoverage" ? "has no coverage" : "survived"} instead of timing out. Detect it with a test` : null
1644
+ );
1645
+ for (const entry of located) {
1646
+ const { mutant } = entry;
1647
+ summary.total += 1;
1648
+ if (DETECTED.has(mutant.status)) {
1649
+ summary.detected += 1;
1650
+ if (mutant.status === "Timeout") {
1651
+ summary.timeout += 1;
1652
+ if (expected.has(entry)) {
1653
+ summary.expectedTimeout += 1;
1654
+ } else {
1655
+ unexpectedTimeouts.push(describeMutant(gate.path, entry));
1656
+ }
1657
+ }
1658
+ } else if (NOT_EVALUABLE.has(mutant.status)) {
1659
+ summary.notEvaluable += 1;
1660
+ } else if (SURVIVED.has(mutant.status)) {
1661
+ if (allowed.has(entry)) {
1662
+ summary.allowed += 1;
1663
+ continue;
1664
+ }
1665
+ summary.unallowed += 1;
1666
+ survivors.push(describeMutant(gate.path, entry));
1667
+ } else {
1668
+ violations.push({
1669
+ file: gate.path,
1670
+ line: mutant.location.start.line,
1671
+ message: mutant.status === "Ignored" ? `a ${mutant.mutatorName} mutant was ignored. Taking mutants out with Stryker comments or mutator exclusions is not allowed` : `a ${mutant.mutatorName} mutant was not evaluated (status: ${mutant.status})`
1672
+ });
1673
+ }
1674
+ }
1675
+ }
1676
+ if (summary.total === 0) {
1677
+ violations.push({
1678
+ file: MANIFEST_FILE,
1679
+ message: "no mutants were evaluated (the mutation test ran on nothing)"
1680
+ });
1681
+ }
1682
+ const maxTimeouts = input.maxTimeouts ?? 0;
1683
+ if (unexpectedTimeouts.length > maxTimeouts) {
1684
+ violations.push({
1685
+ file: MANIFEST_FILE,
1686
+ message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
1687
+ });
1688
+ }
1689
+ return { violations, survivors, unexpectedTimeouts, summary };
1690
+ }
1691
+ function formatSurvivor(survivor) {
1692
+ const occurrence = survivor.sameKeyCount > 1 ? ` / occurrence ${survivor.occurrence} of ${survivor.sameKeyCount}` : "";
1693
+ return `${survivor.file}:${survivor.line}:${survivor.column} / ${survivor.mutator} / ${survivor.original} \u2192 ${survivor.replacement}${occurrence} (${survivor.status})`;
1694
+ }
1695
+ function formatSummary(summary) {
1696
+ return `mutants ${summary.total} / detected ${summary.detected} (timeout ${summary.timeout}) / allowed equivalent ${summary.allowed} / not evaluable ${summary.notEvaluable} / unallowed survivors ${summary.unallowed}`;
1697
+ }
1698
+
1416
1699
  // src/core/reuse-guard.ts
1417
1700
  var RECORD_FORMAT = 2;
1418
1701
  function isStringRecord(value) {
@@ -1531,6 +1814,53 @@ function incrementalOptions(config, args) {
1531
1814
  function comparableArgs(args) {
1532
1815
  return args.filter((arg) => arg !== "--force");
1533
1816
  }
1817
+ function objectOf(value) {
1818
+ return typeof value === "object" && value !== null ? value : {};
1819
+ }
1820
+ function reportFiles(config) {
1821
+ const options = objectOf(config);
1822
+ const fileName = (reporter, fallback) => {
1823
+ const named = objectOf(options[reporter]).fileName;
1824
+ return typeof named === "string" ? named : fallback;
1825
+ };
1826
+ return [
1827
+ fileName("htmlReporter", "reports/mutation/mutation.html"),
1828
+ fileName("jsonReporter", "reports/mutation/mutation.json")
1829
+ ];
1830
+ }
1831
+ var WILDCARDS = /* @__PURE__ */ new Map([
1832
+ ["**/", "(?:.*/)?"],
1833
+ ["*", "[^/]*"],
1834
+ ["?", "[^/]"]
1835
+ ]);
1836
+ function globSource(glob) {
1837
+ return glob.split(/(\*\*\/|\*|\?)/).map((part) => WILDCARDS.get(part) ?? part.replace(/\W/g, "\\$&")).join("");
1838
+ }
1839
+ function strykerIgnores(config) {
1840
+ const patterns = [objectOf(config).ignorePatterns].flat().filter((pattern) => typeof pattern === "string");
1841
+ if (patterns.some((pattern) => pattern.startsWith("!"))) {
1842
+ return () => false;
1843
+ }
1844
+ const rules = patterns.map((pattern) => {
1845
+ const directoryOnly = pattern.endsWith("/");
1846
+ const body = pattern.replace(/\/+$/, "");
1847
+ const anchored = body.includes("/");
1848
+ const expression = new RegExp(`^${globSource(body.replace(/^\//, ""))}$`, "i");
1849
+ return { directoryOnly, anchored, expression };
1850
+ });
1851
+ return (relative) => {
1852
+ const parts = relative.split("/");
1853
+ return rules.some(
1854
+ (rule) => parts.some((part, index) => {
1855
+ if (rule.directoryOnly && index === parts.length - 1) {
1856
+ return false;
1857
+ }
1858
+ const target = rule.anchored ? parts.slice(0, index + 1).join("/") : part;
1859
+ return rule.expression.test(target);
1860
+ })
1861
+ );
1862
+ };
1863
+ }
1534
1864
 
1535
1865
  // src/core/affected.ts
1536
1866
  var CONFIG_NAMES = [
@@ -1545,25 +1875,33 @@ var CONFIG_NAMES = [
1545
1875
  // Declarations need no import to take effect.
1546
1876
  /\.d\.[cm]?ts$/
1547
1877
  ];
1548
- var CONFIG_DIRECTORIES = ["__mocks__", "node_modules"];
1878
+ var ROOT_MOCKS = "__mocks__";
1549
1879
  var DOCUMENT = /\.(?:md|markdown|txt|rst|adoc)$/i;
1550
1880
  var PASSIVE = /\.(?:[cm]?[jt]sx?|json|snap|css|scss|sass|less|svg|png|jpe?g|gif|webp|avif|ico)$/i;
1551
1881
  function kindOf(key, subproject) {
1552
- const inside = subproject === "" || key.startsWith(`${subproject}/`);
1553
- const local = inside && subproject !== "" ? key.slice(subproject.length + 1) : key;
1554
- const segments = local.split("/");
1882
+ const segments = localSegments(key, subproject);
1555
1883
  const name = segments[segments.length - 1];
1556
- if (segments.some((segment) => CONFIG_DIRECTORIES.includes(segment)) || CONFIG_NAMES.some((pattern) => pattern.test(name))) {
1884
+ if (segments[0] === ROOT_MOCKS || segments.includes("node_modules") || CONFIG_NAMES.some((pattern) => pattern.test(name))) {
1557
1885
  return "config";
1558
1886
  }
1887
+ if (isNestedMock(key, subproject)) {
1888
+ return "other";
1889
+ }
1559
1890
  if (DOCUMENT.test(name)) {
1560
1891
  return "document";
1561
1892
  }
1893
+ const inside = subproject === "" || key.startsWith(`${subproject}/`);
1562
1894
  if (!inside || segments.length === 1 || !PASSIVE.test(name)) {
1563
1895
  return "other";
1564
1896
  }
1565
1897
  return "passive";
1566
1898
  }
1899
+ function localSegments(key, subproject) {
1900
+ return (key.startsWith(`${subproject}/`) ? key.slice(subproject.length + 1) : key).split("/");
1901
+ }
1902
+ function isNestedMock(key, subproject) {
1903
+ return localSegments(key, subproject).slice(1, -1).includes(ROOT_MOCKS);
1904
+ }
1567
1905
  function gateDependencies(input) {
1568
1906
  const closureOf = (key) => Object.hasOwn(input.closures, key) ? input.closures[key] : [key];
1569
1907
  const result = /* @__PURE__ */ Object.create(null);
@@ -1587,7 +1925,7 @@ function planAffected(input) {
1587
1925
  const recorded = input.before.affected;
1588
1926
  if (recorded === void 0) {
1589
1927
  return {
1590
- full: "the last run did not write down what each gate depends on (it ran without --affected)"
1928
+ full: "the last run did not write down what each gate depends on (it ran in CI, with an older version, or could not tell)"
1591
1929
  };
1592
1930
  }
1593
1931
  const changed = differingKeys(input.before.files, input.now);
@@ -1596,7 +1934,8 @@ function planAffected(input) {
1596
1934
  const config = new Set(input.config);
1597
1935
  const everywhere = changed.filter((key) => {
1598
1936
  const kind = kindOf(key, input.subproject);
1599
- return config.has(key) || kind === "config" || kind === "other" && !reached.has(key);
1937
+ return config.has(key) || kind === "config" || kind === "other" && !reached.has(key) || // A mock that is gone may have been one of a package, which is used by name for every spec.
1938
+ isNestedMock(key, input.subproject) && !Object.hasOwn(input.now, key);
1600
1939
  });
1601
1940
  if (everywhere.length > 0) {
1602
1941
  return { full: `changed since the last run, and may affect every gate: ${listed(everywhere)}` };
@@ -1624,23 +1963,33 @@ function withoutDots(file) {
1624
1963
  }
1625
1964
  function timeoutRanges(last, gates) {
1626
1965
  const ranges = [];
1966
+ const rerun = /* @__PURE__ */ new Set();
1967
+ const taken = /* @__PURE__ */ new Set();
1627
1968
  const files = last.files ?? {};
1628
1969
  for (const named of gates) {
1629
- const gate = withoutDots(named);
1630
- const mutants = Object.hasOwn(files, gate) ? files[gate]?.mutants : void 0;
1631
- if (mutants === void 0) {
1970
+ const gate = withoutDots(named.path);
1971
+ const result = Object.hasOwn(files, gate) ? files[gate] : void 0;
1972
+ if (result?.mutants === void 0) {
1632
1973
  continue;
1633
1974
  }
1975
+ const { mutants } = result;
1976
+ const expected = expectedTimeoutMutants(named, result.source, mutants);
1634
1977
  for (const mutant of mutants) {
1635
- if (mutant.status === "Timeout") {
1636
- const { start, end } = mutant.location;
1637
- ranges.push(`${gate}:${start.line}:${start.column - 1}-${end.line}:${end.column}`);
1978
+ if (mutant.status !== "Timeout") {
1979
+ continue;
1980
+ }
1981
+ if (expected.has(mutant)) {
1982
+ taken.add(mutant);
1983
+ continue;
1638
1984
  }
1985
+ rerun.add(mutant);
1986
+ const { start, end } = mutant.location;
1987
+ ranges.push(`${gate}:${start.line}:${start.column - 1}-${end.line}:${end.column}`);
1639
1988
  }
1640
1989
  }
1641
- return [...new Set(ranges)];
1990
+ return { ranges: [...new Set(ranges)], rerun: rerun.size, expected: taken.size };
1642
1991
  }
1643
- function describePlan(plan, shown = 5, timeouts = 0) {
1992
+ function describePlan(plan, shown = 5, timeouts = 0, expected = 0) {
1644
1993
  if ("full" in plan) {
1645
1994
  return [
1646
1995
  `test-gates mutation --affected: running every mutant again (--force), the results of the last run are not reused: ${plan.full}`
@@ -1661,8 +2010,14 @@ function describePlan(plan, shown = 5, timeouts = 0) {
1661
2010
  if (timeouts > 0) {
1662
2011
  lines.push(` and ${timeouts} mutant(s) of reused gates that timed out in the last run`);
1663
2012
  }
2013
+ if (expected > 0) {
2014
+ lines.push(` ${describeExpected(expected)}`);
2015
+ }
1664
2016
  return lines;
1665
2017
  }
2018
+ function describeExpected(expected) {
2019
+ return `${expected} mutant(s) that timed out in the last run are taken over: expectedTimeouts lists them`;
2020
+ }
1666
2021
 
1667
2022
  // src/core/doctor.ts
1668
2023
  var NOT_THE_HELPER = "the runner config is not made with";
@@ -1781,25 +2136,52 @@ var SCRIPT = /\.[cm]?[jt]sx?$/;
1781
2136
  var COMPONENT = /\.(?:vue|svelte|astro|mdx)$/;
1782
2137
  var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
1783
2138
  var QUOTED = /(['"`])([^'"`\n]+)\1/g;
1784
- var RUNNER_LOAD = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule)\(\s*(['"`])([^'"`\n]+)\1/g;
2139
+ var RUNNER_LOAD = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|createMockFromModule|genMockFromModule)\s*(?:<[^()\n]*>)?\s*\(\s*(['"`])([^'"`\n]+)\1/g;
1785
2140
  function isFile(file) {
1786
2141
  try {
1787
- return fs4.statSync(file).isFile();
2142
+ return fs4.statSync(file, { throwIfNoEntry: false })?.isFile() === true;
1788
2143
  } catch {
1789
2144
  return false;
1790
2145
  }
1791
2146
  }
1792
- function resolveFile(base, directory = true) {
1793
- const candidates = [
1794
- base,
1795
- ...EXTENSIONS.map((extension) => base + extension),
1796
- // `./fee.js` written in a TypeScript file is `./fee.ts`
1797
- ...EXTENSIONS.map((extension) => base.replace(/\.[cm]?jsx?$/, extension)),
1798
- ...directory ? EXTENSIONS.map((extension) => path4.join(base, `index${extension}`)) : []
1799
- ];
1800
- return candidates.find(isFile) ?? null;
1801
- }
1802
- function installed(specifier, from) {
2147
+ var resolved = null;
2148
+ function resolving(read2) {
2149
+ if (resolved !== null) {
2150
+ return read2();
2151
+ }
2152
+ resolved = /* @__PURE__ */ new Map();
2153
+ try {
2154
+ return read2();
2155
+ } finally {
2156
+ resolved = null;
2157
+ }
2158
+ }
2159
+ var DECLARATIONS = [".d.ts", ".d.mts", ".d.cts"];
2160
+ function resolveFiles(base, directory = true, declarations = true) {
2161
+ const key = `${Number(directory)}${Number(declarations)}${base}`;
2162
+ const known = resolved?.get(key);
2163
+ if (known !== void 0) {
2164
+ return [...known];
2165
+ }
2166
+ const found = filesOf(base, directory, declarations);
2167
+ resolved?.set(key, found);
2168
+ return [...found];
2169
+ }
2170
+ function filesOf(base, directory, declarations) {
2171
+ const candidates = (extensions) => [
2172
+ ...extensions.map((extension) => base + extension),
2173
+ // `./fee.js` written in a TypeScript file is `./fee.ts`
2174
+ ...extensions.map((extension) => base.replace(/\.[cm]?jsx?$/, extension)),
2175
+ ...directory ? extensions.map((extension) => path4.join(base, `index${extension}`)) : []
2176
+ ];
2177
+ const data = [`${base}.json`, ...directory ? [path4.join(base, "index.json")] : []];
2178
+ const scripts = [.../* @__PURE__ */ new Set([base, ...candidates(EXTENSIONS), ...data])].filter(isFile);
2179
+ return scripts.length > 0 || !declarations ? scripts : [...new Set(candidates(DECLARATIONS))].filter(isFile);
2180
+ }
2181
+ function resolveFile(base, directory = true, declarations = true) {
2182
+ return resolveFiles(base, directory, declarations)[0] ?? null;
2183
+ }
2184
+ function installed(specifier, from) {
1803
2185
  const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
1804
2186
  for (let current = path4.dirname(from); ; current = path4.dirname(current)) {
1805
2187
  if (fs4.existsSync(path4.join(current, "node_modules", name))) {
@@ -1811,14 +2193,17 @@ function installed(specifier, from) {
1811
2193
  }
1812
2194
  }
1813
2195
  function loadedFiles(manifest, configFile) {
2196
+ return resolving(() => loadedFilesNow(manifest, configFile));
2197
+ }
2198
+ function loadedFilesNow(manifest, configFile) {
1814
2199
  const aliases = manifest.settings.importAliases;
1815
2200
  const relative = (file) => path4.relative(manifest.dir, file).split(path4.sep).join("/");
1816
2201
  const target = (specifier, from) => {
1817
2202
  if (specifier.startsWith(".")) {
1818
- return resolveFile(path4.resolve(path4.dirname(from), specifier));
2203
+ return resolveFiles(path4.resolve(path4.dirname(from), specifier));
1819
2204
  }
1820
2205
  const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
1821
- return alias === void 0 ? null : resolveFile(
2206
+ return alias === void 0 ? [] : resolveFiles(
1822
2207
  path4.resolve(manifest.dir, alias.target + specifier.slice(alias.prefix.length))
1823
2208
  );
1824
2209
  };
@@ -1843,16 +2228,18 @@ function loadedFiles(manifest, configFile) {
1843
2228
  const paths = strings.filter((text) => file === entry || /^(?:\.|<rootDir>\/)/.test(text));
1844
2229
  for (const text of paths) {
1845
2230
  const stripped = text.replace(/^<rootDir>\//, "");
1846
- const found = resolveFile(path4.resolve(path4.dirname(file), stripped), false) ?? resolveFile(path4.resolve(manifest.dir, stripped), false);
1847
- if (found !== null && SCRIPT.test(found)) {
1848
- named.add(found);
2231
+ const here = resolveFiles(path4.resolve(path4.dirname(file), stripped), false);
2232
+ const found = here.length > 0 ? here : resolveFiles(path4.resolve(manifest.dir, stripped), false);
2233
+ for (const one of found.filter((candidate) => SCRIPT.test(candidate))) {
2234
+ named.add(one);
1849
2235
  }
1850
2236
  }
1851
2237
  for (const specifier of specifiers.filter((one) => isProjectImport(one, aliases))) {
1852
- const found = target(specifier, file);
1853
- if (found !== null && SCRIPT.test(found) && !seenConfigs.has(found)) {
1854
- seenConfigs.add(found);
1855
- configs.push(found);
2238
+ for (const found of target(specifier, file)) {
2239
+ if (SCRIPT.test(found) && !seenConfigs.has(found)) {
2240
+ seenConfigs.add(found);
2241
+ configs.push(found);
2242
+ }
1856
2243
  }
1857
2244
  }
1858
2245
  }
@@ -1869,10 +2256,10 @@ function loadedFiles(manifest, configFile) {
1869
2256
  for (const specifier of specifiers) {
1870
2257
  if (isProjectImport(specifier, aliases)) {
1871
2258
  const found = target(specifier, file);
1872
- if (found === null) {
2259
+ if (found.length === 0) {
1873
2260
  lost ??= `${specifier} (imported by ${relative(file)})`;
1874
2261
  } else {
1875
- next.push(found);
2262
+ next.push(...found);
1876
2263
  }
1877
2264
  } else if (specifier.startsWith("node:") || builtinModules.includes(specifier)) {
1878
2265
  imports.push({ specifier, builtin: true });
@@ -1883,10 +2270,7 @@ function loadedFiles(manifest, configFile) {
1883
2270
  }
1884
2271
  }
1885
2272
  for (const text of strings.filter((one) => one.startsWith("."))) {
1886
- const found = resolveFile(path4.resolve(path4.dirname(file), text));
1887
- if (found !== null) {
1888
- next.push(found);
1889
- }
2273
+ next.push(...resolveFiles(path4.resolve(path4.dirname(file), text)));
1890
2274
  }
1891
2275
  const visited = {
1892
2276
  file: { path: relative(file), source, imports },
@@ -1957,10 +2341,11 @@ var NEVER = ["node_modules", ".git"];
1957
2341
  var WIDENED_SHOWN = 5;
1958
2342
  var SCAN_LIMIT = 8 * 1024 * 1024;
1959
2343
  var UPWARD = /(['"`])((?:[^'"`\n]*\/)?\.\.(?:\/[^'"`\n]*)?)\1/g;
1960
- var DOCUMENTS = [".md", ".markdown", ".txt", ".rst", ".adoc"];
2344
+ var DOCUMENTS = [".md", ".markdown", ".txt", ".rst", ".adoc", ".html", ".htm"];
1961
2345
  var HERE = /^\$\{(?:__dirname|import\.meta\.dirname)\}(?=\/)/;
1962
2346
  var FROM_HERE = /\b(?:__dirname|import\.meta\.dirname)((?:\s*,\s*(['"])[^'"`\n]*\2)+)(\s*\))?/g;
1963
2347
  var LITERAL = /(['"])([^'"`\n]*)\1/g;
2348
+ var DOWNWARD = /(['"`])(\.\/[^'"`\n]*)\1/g;
1964
2349
  var isUpward = (segment) => segment.split("/").includes("..");
1965
2350
  function hashOf(content) {
1966
2351
  return createHash("sha256").update(content).digest("hex");
@@ -1996,9 +2381,12 @@ function git(cwd, args) {
1996
2381
  });
1997
2382
  return result.error || result.status !== 0 ? null : result.stdout;
1998
2383
  }
2384
+ function globLiteral(relative) {
2385
+ return relative.replace(/[*?[\]\\]/g, "\\$&");
2386
+ }
1999
2387
  function gitLister(root) {
2000
2388
  return (directory, recursive) => {
2001
- const relative = toPosix(path5.relative(root, directory));
2389
+ const relative = globLiteral(toPosix(path5.relative(root, directory)));
2002
2390
  const pattern = recursive ? `${relative === "" ? "" : `${relative}/`}**` : `${relative === "" ? "" : `${relative}/`}*`;
2003
2391
  const listed2 = git(root, [
2004
2392
  "ls-files",
@@ -2122,11 +2510,24 @@ function installedVersion(start, name) {
2122
2510
  }
2123
2511
  }
2124
2512
  }
2513
+ function nodeOnPath(cwd) {
2514
+ const result = spawnSync("node", ["--version"], {
2515
+ cwd,
2516
+ encoding: "utf8",
2517
+ shell: process.platform === "win32",
2518
+ timeout: 3e4
2519
+ });
2520
+ const version = result.status === 0 ? result.stdout.trim() : "";
2521
+ return version === "" ? {} : { "Node.js on PATH": version };
2522
+ }
2125
2523
  function ownVersion() {
2126
2524
  const { version } = createRequire(import.meta.url)("../package.json");
2127
2525
  return version;
2128
2526
  }
2129
2527
  function snapshotOf(input) {
2528
+ return resolving(() => snapshotNow(input));
2529
+ }
2530
+ function snapshotNow(input) {
2130
2531
  const dir = fs5.realpathSync(input.dir);
2131
2532
  const top = git(dir, ["rev-parse", "--show-toplevel"]);
2132
2533
  let root = top === null ? dir : fs5.realpathSync(top.trim());
@@ -2180,19 +2581,74 @@ function snapshotOf(input) {
2180
2581
  }
2181
2582
  };
2182
2583
  const follow = (fixed, open, anchored, file, reference) => {
2183
- const target = path5.resolve(path5.dirname(file), fixed);
2184
- let isDirectory = false;
2584
+ const from = (start) => {
2585
+ const target = path5.resolve(start, fixed);
2586
+ let isDirectory2 = false;
2587
+ try {
2588
+ isDirectory2 = fs5.statSync(target).isDirectory();
2589
+ } catch {
2590
+ }
2591
+ return {
2592
+ target,
2593
+ isDirectory: isDirectory2,
2594
+ found: isDirectory2 || resolveFile(target, true, false) !== null
2595
+ };
2596
+ };
2597
+ const fromFile = from(path5.dirname(file));
2598
+ const targets = [fromFile];
2599
+ if (!anchored && path5.dirname(file) !== dir) {
2600
+ const fromRun = from(dir);
2601
+ if (fromRun.found && !fromFile.found) {
2602
+ targets[0] = fromRun;
2603
+ } else if (fromRun.found && isInside(fromRun.target, listRoot)) {
2604
+ enqueue(listRoot, file, reference);
2605
+ } else if (fromRun.found) {
2606
+ targets.push(fromRun);
2607
+ }
2608
+ }
2609
+ for (const { target, isDirectory: isDirectory2 } of targets) {
2610
+ const beside = (isDirectory2 || open ? [] : resolveFiles(target, false, false)).filter(
2611
+ (one) => besideAncestors.has(one)
2612
+ );
2613
+ if (beside.length > 0) {
2614
+ beside.forEach(reach);
2615
+ continue;
2616
+ }
2617
+ if (!isDirectory2 && !isInside(listRoot, target) && resolveFile(target, true, false) === null) {
2618
+ files[toPosix(path5.relative(listRoot, target))] = "missing";
2619
+ continue;
2620
+ }
2621
+ const directory = isDirectory2 ? target : path5.dirname(target);
2622
+ if (fs5.existsSync(directory)) {
2623
+ enqueue(directory, file, reference, anchored && !open);
2624
+ }
2625
+ }
2626
+ };
2627
+ const reached = /* @__PURE__ */ new Set();
2628
+ const reach = (file) => {
2629
+ if (reached.has(file) || DOCUMENTS.includes(path5.extname(file).toLowerCase())) {
2630
+ return;
2631
+ }
2632
+ reached.add(file);
2633
+ let text;
2185
2634
  try {
2186
- isDirectory = fs5.statSync(target).isDirectory();
2635
+ const content = fs5.readFileSync(file);
2636
+ if (content.length > SCAN_LIMIT || content.subarray(0, 8e3).includes(0)) {
2637
+ return;
2638
+ }
2639
+ text = content.toString("utf8").replace(/\\[\\/]/g, "/");
2187
2640
  } catch {
2188
- }
2189
- if (!isDirectory && !isInside(listRoot, target) && resolveFile(target) === null) {
2190
- files[toPosix(path5.relative(listRoot, target))] = "missing";
2191
2641
  return;
2192
2642
  }
2193
- const directory = isDirectory ? target : path5.dirname(target);
2194
- if (fs5.existsSync(directory)) {
2195
- enqueue(directory, file, reference, anchored && !open);
2643
+ for (const match of text.matchAll(DOWNWARD)) {
2644
+ const literal = match[2];
2645
+ const open = literal.includes("${");
2646
+ const fixed = open ? literal.slice(0, literal.lastIndexOf("/", literal.indexOf("${")) + 1) : literal;
2647
+ if (open && isUpward(literal.slice(literal.indexOf("${")))) {
2648
+ enqueue(listRoot, file, literal);
2649
+ } else {
2650
+ follow(fixed, open, false, file, literal);
2651
+ }
2196
2652
  }
2197
2653
  };
2198
2654
  const scan = (text, file) => {
@@ -2264,8 +2720,17 @@ function snapshotOf(input) {
2264
2720
  }
2265
2721
  };
2266
2722
  const ancestors = upTo(dir, listRoot).slice(1);
2267
- for (const directory of ancestors) {
2268
- list(directory, false).forEach(add);
2723
+ const besideAncestors = /* @__PURE__ */ new Set();
2724
+ const ancestorFiles = ancestors.flatMap((directory) => list(directory, false));
2725
+ for (const relative of ancestorFiles) {
2726
+ besideAncestors.add(path5.join(listRoot, relative));
2727
+ }
2728
+ ancestorFiles.forEach(add);
2729
+ for (const directory of [dir, ...ancestors]) {
2730
+ const patches = path5.join(directory, "patches");
2731
+ if (fs5.existsSync(patches) && fs5.statSync(patches).isDirectory()) {
2732
+ enqueue(patches, directory);
2733
+ }
2269
2734
  }
2270
2735
  for (const directory of [dir, ...ancestors]) {
2271
2736
  for (const linked of linkedPackages(path5.join(directory, "node_modules"), listRoot)) {
@@ -2287,11 +2752,23 @@ function snapshotOf(input) {
2287
2752
  }
2288
2753
  }
2289
2754
  }
2290
- for (let position = 0; position < regions.length; position += 1) {
2291
- list(regions[position], true).forEach(add);
2755
+ let position = 0;
2756
+ const drain = () => {
2757
+ for (; position < regions.length; position += 1) {
2758
+ list(regions[position], true).forEach(add);
2759
+ }
2760
+ };
2761
+ drain();
2762
+ for (const file of followed ?? []) {
2763
+ const parts = path5.relative(listRoot, file).split(path5.sep);
2764
+ if (isInside(listRoot, file) && !parts.some((part) => NEVER.includes(part)) && fs5.statSync(file, { throwIfNoEntry: false })?.isFile() === true) {
2765
+ add(toPosix(path5.relative(listRoot, file)));
2766
+ }
2292
2767
  }
2768
+ drain();
2293
2769
  const environment = {
2294
2770
  "Node.js": process.version,
2771
+ ...nodeOnPath(dir),
2295
2772
  platform: `${process.platform} ${process.arch}`,
2296
2773
  "test-gates": ownVersion()
2297
2774
  };
@@ -2310,7 +2787,17 @@ import { builtinModules as builtinModules2 } from "module";
2310
2787
  import path6 from "path";
2311
2788
  var AffectedError = class extends Error {
2312
2789
  };
2313
- var RUNTIME_PATHS = /\b(?:__dirname|__filename|require\.(?:context|resolve)|process\.cwd|createRequire|toMatchFileSnapshot|readFileSync|readdirSync)\b|\bimport\.meta\.(?:url|dirname|filename|glob|resolve)\b/;
2790
+ var RUNTIME_PATHS = /\b(?:require\.(?:context|resolve)|process\.cwd|createRequire|toMatchFileSnapshot|readFileSync|readdirSync)\b|\bimport\.meta\.(?:glob|resolve)\b/;
2791
+ var LOCATION = /\b(?:__dirname|__filename)\b|\bimport\.meta\.(?:url|dirname|filename)\b/;
2792
+ var LOCATION_READS = /\b(?:readFile|readdir|readlink|existsSync|stat|statSync|lstat|lstatSync|access|accessSync|realpath|realpathSync|open|openSync|opendir|opendirSync|createReadStream|watch|watchFile|glob|globSync|promises)\b|\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|setMock|createMockFromModule)\(\s*(?![\s'"`)])|\bWorker\(/;
2793
+ var LOCATION_SAFE = /* @__PURE__ */ new Set([
2794
+ "path",
2795
+ "path/posix",
2796
+ "path/win32",
2797
+ "url",
2798
+ "vitest",
2799
+ "@jest/globals"
2800
+ ]);
2314
2801
  var FILE_MODULES = /* @__PURE__ */ new Set([
2315
2802
  "fs",
2316
2803
  "fs/promises",
@@ -2366,10 +2853,98 @@ function runnerConfigs(dir, stryker, options) {
2366
2853
  }
2367
2854
  }
2368
2855
  }
2369
- return { files: files.filter((file) => fs6.existsSync(file)).map(realpath) };
2856
+ return {
2857
+ files: files.filter((file) => fs6.existsSync(file)).map(realpath),
2858
+ runner: options.testRunner,
2859
+ checkers: Array.isArray(options.checkers) && options.checkers.length > 0
2860
+ };
2861
+ }
2862
+ var CHECKS_TYPES = /\bcheckers\b|typescript-checker|\btypecheck\b/;
2863
+ var TS_JEST_STRIPS = /["']?(?:isolatedModules["']?\s*:\s*true|diagnostics["']?\s*:\s*false)/;
2864
+ var TS_JEST_UNSURE = /\b(?:projects|compilerOptions)\b|\btsconfig["']?\s*:\s*\{/;
2865
+ var STRIPPING_TRANSFORMS = /@swc\/jest|babel-jest|esbuild-jest|@sucrase\/jest/;
2866
+ var KEEPS_IMPORTS = /\b(?:verbatimModuleSyntax|preserveValueImports|importsNotUsedAsValues|onlyRemoveTypeImports)\b/;
2867
+ var COMPILER_CONFIG = /^(?:[jt]sconfig(?:\..+)?\.json|\.swcrc|\.babelrc(?:\..+)?|babel\.config\..+)$/;
2868
+ function typesErased(configs, texts, compiler) {
2869
+ const all = texts.map(withoutComments).join("\n");
2870
+ let erased = false;
2871
+ if (configs.checkers !== true && !CHECKS_TYPES.test(all)) {
2872
+ if (configs.runner === "vitest") {
2873
+ erased = true;
2874
+ } else if (configs.runner === "jest") {
2875
+ erased = /\bts-jest\b/.test(all) ? TS_JEST_STRIPS.test(all) && !TS_JEST_UNSURE.test(all) : STRIPPING_TRANSFORMS.test(all) && !/\bpreset\b/.test(all);
2876
+ }
2877
+ }
2878
+ const inline = erased && !KEEPS_IMPORTS.test(all) && compiler.some(({ file }) => /^tsconfig/.test(path6.basename(file))) && compiler.every(({ text }) => text !== null && !KEEPS_IMPORTS.test(text));
2879
+ return { erased, inline };
2880
+ }
2881
+ function compilerConfigs(dir, root, keys) {
2882
+ const above = /* @__PURE__ */ new Set();
2883
+ for (let current = path6.dirname(dir); ; current = path6.dirname(current)) {
2884
+ const relative = path6.relative(root, current);
2885
+ if (relative.startsWith("..") || path6.isAbsolute(relative)) {
2886
+ break;
2887
+ }
2888
+ above.add(current);
2889
+ if (current === path6.dirname(current)) {
2890
+ break;
2891
+ }
2892
+ }
2893
+ const compared = new Set(keys.map((key) => path6.join(root, key)));
2894
+ const starts = [...compared].filter(
2895
+ (file) => (COMPILER_CONFIG.test(path6.basename(file)) || path6.basename(file) === "package.json") && (file.startsWith(`${dir}${path6.sep}`) || above.has(path6.dirname(file)))
2896
+ );
2897
+ const found = [];
2898
+ const seen = /* @__PURE__ */ new Set();
2899
+ const queue = [...starts];
2900
+ for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
2901
+ if (seen.has(file)) {
2902
+ continue;
2903
+ }
2904
+ seen.add(file);
2905
+ let text = null;
2906
+ if (compared.has(file) || file.split(path6.sep).includes("node_modules")) {
2907
+ try {
2908
+ text = fs6.readFileSync(file, "utf8");
2909
+ } catch {
2910
+ text = null;
2911
+ }
2912
+ }
2913
+ found.push({ file, text });
2914
+ for (const base of text === null ? [] : valuesOf(text, "extends")) {
2915
+ queue.push(extended(base, file) ?? `${file} extends ${base}`);
2916
+ }
2917
+ }
2918
+ return found;
2919
+ }
2920
+ function extended(base, from) {
2921
+ const candidates = (start) => [
2922
+ start,
2923
+ `${start}.json`,
2924
+ path6.join(start, "tsconfig.json")
2925
+ ];
2926
+ if (base.startsWith(".") || path6.isAbsolute(base)) {
2927
+ return candidates(path6.resolve(path6.dirname(from), base)).find(isFile2) ?? null;
2928
+ }
2929
+ for (let current = path6.dirname(from); ; current = path6.dirname(current)) {
2930
+ const file = candidates(path6.join(current, "node_modules", base)).find(isFile2);
2931
+ if (file !== void 0) {
2932
+ return file;
2933
+ }
2934
+ if (current === path6.dirname(current)) {
2935
+ return null;
2936
+ }
2937
+ }
2938
+ }
2939
+ function isFile2(file) {
2940
+ try {
2941
+ return fs6.statSync(file).isFile();
2942
+ } catch {
2943
+ return false;
2944
+ }
2370
2945
  }
2371
2946
  function fileEdges(file, context) {
2372
- const { dir, aliases, cache } = context;
2947
+ const { dir, aliases, cache, types } = context;
2373
2948
  const known = cache.get(file);
2374
2949
  if (known !== void 0) {
2375
2950
  return known;
@@ -2377,10 +2952,10 @@ function fileEdges(file, context) {
2377
2952
  const shown = (one) => toPosix2(path6.relative(dir, one));
2378
2953
  const target = (specifier, from) => {
2379
2954
  if (specifier.startsWith(".")) {
2380
- return resolveFile(path6.resolve(path6.dirname(from), specifier));
2955
+ return resolveFiles(path6.resolve(path6.dirname(from), specifier));
2381
2956
  }
2382
2957
  const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
2383
- return alias === void 0 ? null : resolveFile(path6.resolve(dir, alias.target + specifier.slice(alias.prefix.length)));
2958
+ return alias === void 0 ? [] : resolveFiles(path6.resolve(dir, alias.target + specifier.slice(alias.prefix.length)));
2384
2959
  };
2385
2960
  const found = { loads: [], unknown: null, computed: false, unnamed: [] };
2386
2961
  cache.set(file, found);
@@ -2392,41 +2967,61 @@ function fileEdges(file, context) {
2392
2967
  if (fs6.existsSync(snapshotFile)) {
2393
2968
  found.loads.push(realpath(snapshotFile));
2394
2969
  }
2970
+ const mocks = path6.join(path6.dirname(file), "__mocks__");
2971
+ const name = path6.basename(file);
2972
+ const candidates = isDirectory(mocks) ? [
2973
+ path6.join(mocks, name),
2974
+ ...resolveFiles(path6.join(mocks, name.replace(/\.[^.]+$/, "")), false, false)
2975
+ ] : [];
2976
+ for (const mock of new Set(candidates)) {
2977
+ if (isFile2(mock)) {
2978
+ found.loads.push(realpath(mock));
2979
+ }
2980
+ }
2395
2981
  if (!SCRIPT.test(file) && !COMPONENT.test(file)) {
2396
2982
  return found;
2397
2983
  }
2398
2984
  const source = fs6.readFileSync(file, "utf8");
2985
+ const code = withoutComments(source);
2399
2986
  if (COMPONENT.test(file)) {
2400
2987
  found.unknown = `${shown(file)} is a component, which can use others that it does not import`;
2401
2988
  }
2402
- const uncheckable = findUncheckableImports(source)[0];
2403
- const runtime = RUNTIME_PATHS.exec(source);
2989
+ const uncheckable = findUncheckableImports(code)[0];
2990
+ const runtime = RUNTIME_PATHS.exec(code);
2404
2991
  if (uncheckable !== void 0) {
2405
2992
  found.unknown = `${shown(file)}:${uncheckable} imports a module whose path is not written out`;
2406
2993
  found.computed = true;
2407
2994
  } else if (runtime !== null) {
2408
2995
  found.unknown = `${shown(file)} uses ${runtime[0]}, which can reach files that no import names`;
2409
2996
  }
2997
+ const location = LOCATION.exec(code);
2998
+ const refs = parseImports(code);
2999
+ const dropped = (ref) => types !== void 0 && types.erased && (ref.typeOnly || types.inline && ref.inlineTypes === true);
2410
3000
  const specifiers = /* @__PURE__ */ new Set([
2411
- ...parseImports(source).map((ref) => ref.module),
2412
- ...[...source.matchAll(RUNNER_LOAD)].map((match) => match[2])
3001
+ ...refs.filter((ref) => !dropped(ref)).map((ref) => ref.module),
3002
+ ...[...code.matchAll(RUNNER_LOAD)].map((match) => match[2])
2413
3003
  ]);
3004
+ const typeLines = /* @__PURE__ */ new Map();
3005
+ for (const ref of refs.filter(dropped)) {
3006
+ const key = `${ref.line}:${ref.module}`;
3007
+ typeLines.set(key, (typeLines.get(key) ?? 0) + 1);
3008
+ }
2414
3009
  for (const specifier of specifiers) {
2415
3010
  if (path6.isAbsolute(specifier) || specifier.startsWith("file:")) {
2416
- const resolved = resolveFile(
3011
+ const resolved2 = resolveFiles(
2417
3012
  specifier.startsWith("file:") ? new URL(specifier).pathname : specifier
2418
3013
  );
2419
- if (resolved === null) {
3014
+ if (resolved2.length === 0) {
2420
3015
  found.unknown ??= `${shown(file)} imports ${specifier}, which leads to no file`;
2421
3016
  } else {
2422
- found.loads.push(realpath(resolved));
3017
+ found.loads.push(...resolved2.map(realpath));
2423
3018
  }
2424
3019
  } else if (isProjectImport(specifier, aliases)) {
2425
- const resolved = target(specifier, file);
2426
- if (resolved === null) {
3020
+ const resolved2 = target(specifier, file);
3021
+ if (resolved2.length === 0) {
2427
3022
  found.unknown ??= `${shown(file)} imports ${specifier}, which leads to no file`;
2428
3023
  } else {
2429
- found.loads.push(realpath(resolved));
3024
+ found.loads.push(...resolved2.map(realpath));
2430
3025
  }
2431
3026
  } else if (FILE_MODULES.has(bare(specifier))) {
2432
3027
  found.unknown ??= `${shown(file)} imports ${specifier}, which can reach files that no import names`;
@@ -2440,18 +3035,40 @@ function fileEdges(file, context) {
2440
3035
  }
2441
3036
  }
2442
3037
  }
3038
+ if (location !== null) {
3039
+ const reads = LOCATION_READS.exec(code);
3040
+ const reader = [...specifiers].find(
3041
+ (specifier) => !specifier.startsWith(".") && !path6.isAbsolute(specifier) && !specifier.startsWith("file:") && !isProjectImport(specifier, aliases) && !LOCATION_SAFE.has(bare(specifier))
3042
+ );
3043
+ if (reads !== null) {
3044
+ found.unknown ??= `${shown(file)} uses ${location[0]} and ${reads[0].replace(/\(\s*$/, "")}, which can reach files that no import names`;
3045
+ } else if (reader !== void 0) {
3046
+ found.unknown ??= `${shown(file)} uses ${location[0]} and imports ${reader}, which can read files by it that no import names`;
3047
+ }
3048
+ }
3049
+ let line = 1;
3050
+ let counted = 0;
2443
3051
  for (const match of source.matchAll(QUOTED)) {
2444
- const text = match[2];
2445
- if (text.startsWith(".") && !specifiers.has(text)) {
2446
- const resolved = resolveFile(path6.resolve(path6.dirname(file), text));
2447
- if (resolved !== null) {
2448
- found.loads.push(realpath(resolved));
3052
+ for (; counted < match.index; counted += 1) {
3053
+ if (source[counted] === "\n") {
3054
+ line += 1;
2449
3055
  }
2450
3056
  }
3057
+ const text = match[2];
3058
+ const key = `${line}:${text}`;
3059
+ const left = typeLines.get(key) ?? 0;
3060
+ if (left > 0) {
3061
+ typeLines.set(key, left - 1);
3062
+ } else if (text.startsWith(".") && !specifiers.has(text)) {
3063
+ found.loads.push(...resolveFiles(path6.resolve(path6.dirname(file), text)).map(realpath));
3064
+ }
2451
3065
  }
2452
3066
  return found;
2453
3067
  }
2454
3068
  function affectedInputs(manifest, input) {
3069
+ return resolving(() => inputsOf(manifest, input));
3070
+ }
3071
+ function inputsOf(manifest, input) {
2455
3072
  const dir = realpath(manifest.dir);
2456
3073
  const aliases = manifest.settings.importAliases;
2457
3074
  const placed = [...input.tests, ...Object.values(input.covering).flat()];
@@ -2467,8 +3084,43 @@ function affectedInputs(manifest, input) {
2467
3084
  return toPosix2(relative);
2468
3085
  };
2469
3086
  const shown = (file) => toPosix2(path6.relative(dir, file));
3087
+ const plain2 = /* @__PURE__ */ new Map();
3088
+ const configTexts = [];
3089
+ const runnerSources = [];
3090
+ const reached = new Set(input.configs.files);
3091
+ const pending = [...input.configs.files];
3092
+ for (let file = pending.shift(); file !== void 0; file = pending.shift()) {
3093
+ const text = fs6.readFileSync(file, "utf8");
3094
+ configTexts.push(text);
3095
+ runnerSources.push({ file, text });
3096
+ for (const next of fileEdges(file, { dir, aliases, cache: plain2 }).loads) {
3097
+ if (!reached.has(next) && SCRIPT.test(next)) {
3098
+ reached.add(next);
3099
+ pending.push(next);
3100
+ }
3101
+ }
3102
+ }
3103
+ const compiler = compilerConfigs(dir, realpath(input.root), Object.keys(input.files));
3104
+ const types = typesErased(input.configs, configTexts, compiler);
3105
+ const maps = unexplainedMaps({
3106
+ dir,
3107
+ aliases,
3108
+ // Not what lies outside of the compared files: this package, imported by path, is installed.
3109
+ runner: [
3110
+ ...runnerSources.filter(({ file }) => {
3111
+ const relative = path6.relative(input.root, realpath(file));
3112
+ return !relative.startsWith("..") && toPosix2(relative) in input.files;
3113
+ }),
3114
+ ...jestInPackage(dir, input.configs)
3115
+ ],
3116
+ compiler,
3117
+ ...input.configs.runner === void 0 ? {} : { testRunner: input.configs.runner }
3118
+ });
3119
+ if (maps !== null) {
3120
+ throw new AffectedError(maps);
3121
+ }
2470
3122
  const edges = /* @__PURE__ */ new Map();
2471
- const edgesOf = (file) => fileEdges(file, { dir, aliases, cache: edges });
3123
+ const edgesOf = (file) => fileEdges(file, { dir, aliases, cache: edges, types });
2472
3124
  const closure = (start, strict) => {
2473
3125
  const seen = /* @__PURE__ */ new Set([start]);
2474
3126
  const queue = [start];
@@ -2509,9 +3161,17 @@ function affectedInputs(manifest, input) {
2509
3161
  }
2510
3162
  for (const match of fs6.readFileSync(loaded, "utf8").matchAll(QUOTED)) {
2511
3163
  const text = match[2].replace(/^<rootDir>\//, "");
2512
- const found = resolveFile(path6.resolve(path6.dirname(loaded), text), false) ?? resolveFile(path6.resolve(dir, text), false);
2513
- if (found !== null && !text.includes("*") && isCompared(found)) {
2514
- named.push(realpath(found));
3164
+ const here = resolveFiles(path6.resolve(path6.dirname(loaded), text), false, false);
3165
+ const candidates = here.length > 0 ? here : resolveFiles(path6.resolve(dir, text), false, false);
3166
+ for (const found of candidates) {
3167
+ if (text.includes("*") || !isCompared(found)) {
3168
+ continue;
3169
+ }
3170
+ if (SCRIPT.test(found)) {
3171
+ named.push(realpath(found));
3172
+ } else {
3173
+ config.add(realpath(found));
3174
+ }
2515
3175
  }
2516
3176
  }
2517
3177
  }
@@ -2579,7 +3239,22 @@ function affectedInputs(manifest, input) {
2579
3239
  files: found.files
2580
3240
  };
2581
3241
  }
2582
- return { subproject, config: keysOf([...config]).sort(), gates };
3242
+ const packageMocks = Object.keys(input.files).filter(
3243
+ (key) => isNestedMock(key, subproject) && mockNames(key).some(
3244
+ (name) => builtinModules2.includes(name) || installed(name, path6.join(input.root, key))
3245
+ )
3246
+ );
3247
+ return {
3248
+ subproject,
3249
+ config: [.../* @__PURE__ */ new Set([...keysOf([...config]), ...packageMocks])].sort(),
3250
+ gates
3251
+ };
3252
+ }
3253
+ function mockNames(key) {
3254
+ const segments = key.replace(/\.[^./]+$/, "").split("/");
3255
+ return segments.flatMap(
3256
+ (segment, index) => segment === "__mocks__" && index < segments.length - 1 ? [segments.slice(index + 1).join("/")] : []
3257
+ );
2583
3258
  }
2584
3259
  var NOT_THE_HELPER2 = "the runner config is not made with createJestGatesConfig or createVitestGatesConfig";
2585
3260
  var OWN = /@t09tanaka\/test-gates\/(?:jest|vitest)\b/;
@@ -2615,6 +3290,9 @@ var READ_BY_NAME = [
2615
3290
  /^\.swcrc$/
2616
3291
  ];
2617
3292
  function loadedScope(manifest, configs, listed2) {
3293
+ return resolving(() => scopeOf(manifest, configs, listed2));
3294
+ }
3295
+ function scopeOf(manifest, configs, listed2) {
2618
3296
  const dir = realpath(manifest.dir);
2619
3297
  const cache = /* @__PURE__ */ new Map();
2620
3298
  const context = { dir, aliases: manifest.settings.importAliases, cache };
@@ -2671,19 +3349,16 @@ function loadedScope(manifest, configs, listed2) {
2671
3349
  for (const specifier of edges.unnamed) {
2672
3350
  endingAs(specifier, listed2).forEach(add);
2673
3351
  }
2674
- const mock = path6.join(path6.dirname(file), "__mocks__", path6.basename(file));
2675
- if (fs6.existsSync(mock)) {
2676
- add(mock);
2677
- }
2678
3352
  if (naming.has(file) || SCRIPT.test(file) && path6.dirname(file) === dir) {
2679
3353
  for (const named2 of namedIn(file)) {
2680
3354
  const text = named2.replace(/^<rootDir>\//, "");
2681
- const found = resolveFile(path6.resolve(path6.dirname(file), text), false) ?? resolveFile(path6.resolve(dir, text), false);
2682
- if (found !== null && !text.includes("*")) {
2683
- if (!SCRIPT.test(found)) {
2684
- naming.add(realpath(found));
3355
+ const here = resolveFiles(path6.resolve(path6.dirname(file), text), false);
3356
+ const found = here.length > 0 ? here : resolveFiles(path6.resolve(dir, text), false);
3357
+ for (const one of text.includes("*") ? [] : found) {
3358
+ if (!SCRIPT.test(one)) {
3359
+ naming.add(realpath(one));
2685
3360
  }
2686
- add(found);
3361
+ add(one);
2687
3362
  }
2688
3363
  }
2689
3364
  }
@@ -2740,286 +3415,420 @@ function valuesOf(text, key) {
2740
3415
  return found;
2741
3416
  }
2742
3417
  function withoutComments(text) {
2743
- let out = "";
3418
+ const out = [];
3419
+ const before = { last: "", word: "" };
3420
+ let space = false;
3421
+ const keep = (part) => {
3422
+ out.push(part);
3423
+ for (const one of part) {
3424
+ if (/\s/.test(one)) {
3425
+ space = true;
3426
+ continue;
3427
+ }
3428
+ const word = /[\w$]/.test(one);
3429
+ before.word = !word ? "" : space || !/[\w$]/.test(before.last) ? one : before.word + one;
3430
+ before.last = one;
3431
+ space = false;
3432
+ }
3433
+ };
2744
3434
  for (let at = 0; at < text.length; at += 1) {
2745
3435
  const char = text[at];
3436
+ const next = text[at + 1];
2746
3437
  if (char === "'" || char === '"' || char === "`") {
2747
- let end = at + 1;
2748
- while (end < text.length && text[end] !== char && text[end] !== "\n") {
2749
- end += text[end] === "\\" ? 2 : 1;
2750
- }
2751
- out += text.slice(at, end + 1);
3438
+ const end = stringEnd(text, at);
3439
+ keep(text.slice(at, end + 1));
2752
3440
  at = end;
2753
- } else if (char === "/" && text[at + 1] === "/") {
3441
+ } else if (char === "/" && next === "/" && !SCHEME.test(text.slice(Math.max(0, at - 40), at))) {
2754
3442
  const end = text.indexOf("\n", at);
2755
3443
  at = end === -1 ? text.length : end - 1;
2756
- } else if (char === "/" && text[at + 1] === "*") {
3444
+ } else if (char === "/" && next === "*") {
2757
3445
  const end = text.indexOf("*/", at + 2);
2758
- at = end === -1 ? text.length : end + 1;
3446
+ const stop = end === -1 ? text.length : end + 2;
3447
+ keep(` ${text.slice(at, stop).replace(/[^\n]/g, "")}`);
3448
+ at = stop - 1;
3449
+ } else if (char === "/" && next !== "/") {
3450
+ const end = patternEnd(text, at, mayStartPattern(before));
3451
+ keep(text.slice(at, end + 1));
3452
+ at = end;
2759
3453
  } else {
2760
- out += char;
3454
+ keep(char);
3455
+ }
3456
+ }
3457
+ return out.join("");
3458
+ }
3459
+ var SCHEME = /(?:^|[^\w$.+-])[A-Za-z][\w+.-]*:$/;
3460
+ function patternEnd(text, at, pattern) {
3461
+ let inClass = false;
3462
+ let end = at + 1;
3463
+ for (; end < text.length && text[end] !== "\n"; end += 1) {
3464
+ const char = text[end];
3465
+ if (char === "\\") {
3466
+ end += 1;
3467
+ } else if (char === "[") {
3468
+ inClass = true;
3469
+ } else if (char === "]") {
3470
+ inClass = false;
3471
+ } else if (char === "/" && !inClass) {
3472
+ const comment = text[end + 1] === "/" || text[end + 1] === "*";
3473
+ return pattern || !comment ? end : at;
3474
+ } else if (!pattern && (char === "'" || char === '"' || char === "`")) {
3475
+ return at;
3476
+ }
3477
+ }
3478
+ return pattern ? end : at;
3479
+ }
3480
+ function stringEnd(text, at) {
3481
+ const quote = text[at];
3482
+ let end = at + 1;
3483
+ while (end < text.length && text[end] !== quote && (quote === "`" || text[end] !== "\n")) {
3484
+ if (text[end] === "\\") {
3485
+ end += 2;
3486
+ } else if (quote === "`" && text.startsWith("${", end)) {
3487
+ end = placeholderEnd(text, end + 2) + 1;
3488
+ } else {
3489
+ end += 1;
2761
3490
  }
2762
3491
  }
2763
- return out;
3492
+ return end;
2764
3493
  }
2765
-
2766
- // src/commands/mutation.ts
2767
- import { spawnSync as spawnSync2 } from "child_process";
2768
- import fs8 from "fs";
2769
- import path8 from "path";
2770
- import { pathToFileURL } from "url";
2771
-
2772
- // src/commands/mutation-result.ts
2773
- import fs7 from "fs";
2774
- import path7 from "path";
2775
-
2776
- // src/core/mutation-result.ts
2777
- var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
2778
- var SURVIVED = /* @__PURE__ */ new Set(["Survived", "NoCoverage"]);
2779
- var NOT_EVALUABLE = /* @__PURE__ */ new Set(["RuntimeError", "CompileError"]);
2780
- function normalizeCode(text) {
2781
- return text.replace(/\s+/g, " ").trim();
2782
- }
2783
- function normalizePath(file) {
2784
- return file.replace(/\\/g, "/").replace(/^\.\//, "");
2785
- }
2786
- function normalizeNewlines(text) {
2787
- return text.replace(/\r\n/g, "\n");
3494
+ function placeholderEnd(text, at) {
3495
+ let depth = 0;
3496
+ for (let index = at; index < text.length; index += 1) {
3497
+ const char = text[index];
3498
+ if (char === "'" || char === '"' || char === "`") {
3499
+ index = stringEnd(text, index);
3500
+ } else if (char === "{") {
3501
+ depth += 1;
3502
+ } else if (char === "}") {
3503
+ if (depth === 0) {
3504
+ return index;
3505
+ }
3506
+ depth -= 1;
3507
+ }
3508
+ }
3509
+ return text.length;
3510
+ }
3511
+ var BEFORE_PATTERN = /* @__PURE__ */ new Set([
3512
+ "return",
3513
+ "typeof",
3514
+ "instanceof",
3515
+ "in",
3516
+ "of",
3517
+ "new",
3518
+ "delete",
3519
+ "void",
3520
+ "throw",
3521
+ "case",
3522
+ "do",
3523
+ "else",
3524
+ "yield",
3525
+ "await"
3526
+ ]);
3527
+ function mayStartPattern(before) {
3528
+ if (before.word !== "") {
3529
+ return BEFORE_PATTERN.has(before.word);
3530
+ }
3531
+ return before.last === "" || !")]".includes(before.last);
3532
+ }
3533
+ function jestInPackage(dir, configs) {
3534
+ const file = path6.join(dir, "package.json");
3535
+ return configs.runner === "jest" && configs.files.length === 1 && fs6.existsSync(file) ? [{ file, text: fs6.readFileSync(file, "utf8"), mapperOnly: true }] : [];
3536
+ }
3537
+ var STRING_LITERAL = String.raw`'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"`;
3538
+ function stringValue(text) {
3539
+ const match = new RegExp(`^\\s*(${STRING_LITERAL})\\s*$`).exec(text);
3540
+ return match === null ? null : match[1].slice(1, -1).replace(/\\(.)/g, "$1");
3541
+ }
3542
+ function partsOf(text, at) {
3543
+ const parts = [];
3544
+ let depth = 0;
3545
+ let start = at + 1;
3546
+ for (let index = at; index < text.length; index += 1) {
3547
+ const char = text[index];
3548
+ if (char === "'" || char === '"' || char === "`") {
3549
+ let end = index + 1;
3550
+ while (end < text.length && text[end] !== char) {
3551
+ end += text[end] === "\\" ? 2 : 1;
3552
+ }
3553
+ index = end;
3554
+ } else if ("{[(".includes(char)) {
3555
+ depth += 1;
3556
+ } else if ("}])".includes(char)) {
3557
+ depth -= 1;
3558
+ if (depth === 0) {
3559
+ parts.push(text.slice(start, index));
3560
+ return { parts: parts.filter((part) => part.trim() !== ""), end: index };
3561
+ }
3562
+ } else if (char === "," && depth === 1) {
3563
+ parts.push(text.slice(start, index));
3564
+ start = index + 1;
3565
+ }
3566
+ }
3567
+ return null;
2788
3568
  }
2789
- function sliceByLocation(source, location) {
2790
- const lines = source.split("\n");
2791
- const { start, end } = location;
2792
- const first = lines[start.line - 1];
2793
- const last = lines[end.line - 1];
2794
- if (first === void 0 || last === void 0 || start.column < 1 || end.column < 1 || end.line < start.line || end.line === start.line && end.column < start.column) {
3569
+ function property(part) {
3570
+ const match = new RegExp(`^\\s*(${STRING_LITERAL}|[\\w$]+)\\s*:([\\s\\S]*)$`).exec(part);
3571
+ if (match === null) {
2795
3572
  return null;
2796
3573
  }
2797
- if (start.line === end.line) {
2798
- return first.slice(start.column - 1, end.column - 1);
3574
+ const key = match[1];
3575
+ return { key: stringValue(key) ?? key, value: match[2].trim() };
3576
+ }
3577
+ function aliasPath(expression, file, text, depth = 0) {
3578
+ const value = expression.trim();
3579
+ const here = path6.dirname(file);
3580
+ if (value === "__dirname" || value === "import.meta.dirname" || /^(?:path\.)?dirname\(\s*(?:url\.)?fileURLToPath\(\s*import\.meta\.url\s*\)\s*\)$/.test(value)) {
3581
+ return here;
3582
+ }
3583
+ const url = new RegExp(
3584
+ `^(?:(?:url\\.)?fileURLToPath\\(\\s*new URL\\(\\s*(${STRING_LITERAL})\\s*,\\s*import\\.meta\\.url\\s*\\)\\s*\\)|new URL\\(\\s*(${STRING_LITERAL})\\s*,\\s*import\\.meta\\.url\\s*\\)\\.pathname)$`
3585
+ ).exec(value);
3586
+ if (url !== null) {
3587
+ return path6.resolve(here, stringValue(url[1] ?? url[2]));
3588
+ }
3589
+ const call = /^(?:path\.)?(?:resolve|join)\s*\(/.exec(value);
3590
+ if (call !== null) {
3591
+ const found = partsOf(value, call[0].length - 1);
3592
+ if (found === null || value.slice(found.end + 1).trim() !== "" || found.parts.length === 0) {
3593
+ return null;
3594
+ }
3595
+ const [first, ...rest] = found.parts;
3596
+ const base = aliasPath(first, file, text, depth + 1);
3597
+ const parts = rest.map(stringValue);
3598
+ return base === null || parts.some((part) => part === null) ? null : path6.resolve(base, ...parts);
2799
3599
  }
2800
- return [
2801
- first.slice(start.column - 1),
2802
- ...lines.slice(start.line, end.line - 1),
2803
- last.slice(0, end.column - 1)
2804
- ].join("\n");
3600
+ if (/^[A-Za-z_$][\w$]*$/.test(value) && depth < 4) {
3601
+ const declarations = text.match(
3602
+ new RegExp(`\\b(?:const|let|var|function|class)\\s+${value.replace(/\$/g, "\\$")}\\b`, "g")
3603
+ );
3604
+ const bound = new RegExp(`\\bconst\\s+${value.replace(/\$/g, "\\$")}\\s*=\\s*([^;\\n]+)`).exec(
3605
+ text
3606
+ );
3607
+ return declarations?.length === 1 && bound !== null ? aliasPath(bound[1], file, text, depth + 1) : null;
3608
+ }
3609
+ return null;
2805
3610
  }
2806
- function keyOf(mutator, original, replacement) {
2807
- return JSON.stringify([mutator, normalizeCode(original), normalizeCode(replacement)]);
3611
+ var PACKAGE_NAME = /^(?:@[\w-][\w.-]*\/)?[\w-][\w.-]*$/;
3612
+ function isDirectory(file) {
3613
+ try {
3614
+ return fs6.statSync(file).isDirectory();
3615
+ } catch {
3616
+ return false;
3617
+ }
2808
3618
  }
2809
- function describeMutant(file, entry) {
2810
- const { mutant } = entry;
2811
- return {
2812
- file,
2813
- line: mutant.location.start.line,
2814
- column: mutant.location.start.column,
2815
- status: mutant.status,
2816
- mutator: mutant.mutatorName,
2817
- original: normalizeCode(entry.original),
2818
- replacement: normalizeCode(entry.replacement),
2819
- occurrence: entry.occurrence,
2820
- sameKeyCount: entry.sameKeyCount,
2821
- allowance: {
2822
- mutator: mutant.mutatorName,
2823
- original: normalizeCode(entry.original),
2824
- replacement: normalizeCode(entry.replacement),
2825
- ...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
2826
- reason: ""
2827
- }
2828
- };
3619
+ function regexPrefix(literal) {
3620
+ const match = /^\/\^((?:[\w@~#%-]|\\[./@~#-])*)\\\/\/$/.exec(literal.trim());
3621
+ return match === null ? null : `${match[1].replace(/\\(.)/g, "$1")}/`;
2829
3622
  }
2830
- function describeAllowance(entry) {
2831
- const occurrence = entry.occurrence === void 0 ? "" : ` / occurrence ${entry.occurrence}`;
2832
- return `${entry.mutator} / ${normalizeCode(entry.original)} \u2192 ${normalizeCode(entry.replacement)}${occurrence}`;
3623
+ function mapperPrefix(pattern) {
3624
+ const match = /^\^((?:[\w@~#%/-]|\\[./@~#-])*)\(\.\*\)\$?$/.exec(pattern);
3625
+ return match === null ? null : match[1].replace(/\\(.)/g, "$1");
2833
3626
  }
2834
- function judgeMutationReport(input) {
2835
- const violations = [];
2836
- const survivors = [];
2837
- const unexpectedTimeouts = [];
2838
- const summary = {
2839
- total: 0,
2840
- detected: 0,
2841
- timeout: 0,
2842
- allowed: 0,
2843
- notEvaluable: 0,
2844
- unallowed: 0,
2845
- expectedTimeout: 0
3627
+ var ROOT_DIR_HERE = /* @__PURE__ */ new Set([
3628
+ "__dirname",
3629
+ "import.meta.dirname",
3630
+ "import.meta.url",
3631
+ "'.'",
3632
+ '"."',
3633
+ "'./'",
3634
+ '"./"'
3635
+ ]);
3636
+ function unexplainedMaps(input) {
3637
+ const { dir, aliases } = input;
3638
+ const reasons = [];
3639
+ const mapped = [];
3640
+ const shown = (file) => toPosix2(path6.relative(dir, file));
3641
+ const under = (base, rest) => rest === "" ? `${base}/` : path6.join(base, rest);
3642
+ const named = (prefix, target) => {
3643
+ const alias = aliases.find((candidate) => prefix.startsWith(candidate.prefix));
3644
+ if (alias === void 0) {
3645
+ return false;
3646
+ }
3647
+ const full = alias.target + prefix.slice(alias.prefix.length);
3648
+ const slash = full === "" || full.endsWith("/");
3649
+ return path6.resolve(dir, full) === path6.resolve(target) && slash === target.endsWith("/");
2846
3650
  };
2847
- const files = /* @__PURE__ */ new Map();
2848
- for (const [file, result] of Object.entries(input.report.files ?? {})) {
2849
- files.set(normalizePath(file), result);
3651
+ const advice = (prefix, target) => {
3652
+ const relative = toPosix2(path6.relative(dir, target));
3653
+ const shownTarget = relative === "" ? "./" : `${relative}/`;
3654
+ const shorter = aliases.find((alias) => prefix.startsWith(alias.prefix));
3655
+ const before = shorter === void 0 ? "" : shorter.prefix === prefix ? `, in place of the alias of ${prefix}` : `, before the alias of ${shorter.prefix}`;
3656
+ return ` (add { "prefix": "${prefix}", "target": "${shownTarget}" } to settings.importAliases${before})`;
3657
+ };
3658
+ const missing = (file, key, word, prefix, target) => `${shown(file)} maps ${key} with ${word} in a way settings.importAliases does not name` + (prefix !== void 0 && target !== void 0 && isDirectory(target) ? advice(prefix, target) : "");
3659
+ for (const { file, text: raw, mapperOnly } of input.runner) {
3660
+ let viteAlias2 = function(part, list) {
3661
+ let key = null;
3662
+ let prefix = null;
3663
+ let to = null;
3664
+ if (list) {
3665
+ const object = /^\s*\{/.test(part) ? partsOf(part, part.indexOf("{")) : null;
3666
+ const fields = Object.fromEntries(
3667
+ (object?.parts ?? []).map(property).map((one) => [one?.key ?? "", one?.value ?? ""])
3668
+ );
3669
+ if (object === null || Object.keys(fields).sort().join() !== "find,replacement" || part.slice(object.end + 1).trim() !== "") {
3670
+ return unreadableAlias2();
3671
+ }
3672
+ const find = fields.find;
3673
+ const replacement = fields.replacement;
3674
+ key = stringValue(find);
3675
+ if (key !== null) {
3676
+ prefix = key.endsWith("/") ? null : `${key}/`;
3677
+ const target = aliasPath(replacement, file, text);
3678
+ to = target === null ? null : `${target}/`;
3679
+ } else {
3680
+ key = find.trim();
3681
+ prefix = regexPrefix(find);
3682
+ const slashed = new RegExp(`^([\\s\\S]+?)\\s*\\+\\s*(?:'/'|"/")$`).exec(replacement);
3683
+ const target = slashed === null ? null : aliasPath(slashed[1], file, text);
3684
+ to = target === null ? null : `${target}/`;
3685
+ if (prefix === null && to === null) {
3686
+ return unreadableAlias2();
3687
+ }
3688
+ }
3689
+ } else {
3690
+ const entry = property(part);
3691
+ if (entry === null) {
3692
+ return unreadableAlias2();
3693
+ }
3694
+ key = entry.key;
3695
+ prefix = key.endsWith("/") ? null : `${key}/`;
3696
+ const target = aliasPath(entry.value, file, text);
3697
+ to = target === null ? null : `${target}/`;
3698
+ }
3699
+ if (prefix === null || to === null || PACKAGE_NAME.test(key) && installed(key, file)) {
3700
+ return missing(file, key, "alias");
3701
+ }
3702
+ if (!named(prefix, to)) {
3703
+ return missing(file, key, "alias", prefix, to);
3704
+ }
3705
+ mapped.push(prefix);
3706
+ return null;
3707
+ }, unreadableAlias2 = function() {
3708
+ return `${shown(file)} names alias in a way that cannot be read as a list of aliases`;
3709
+ };
3710
+ var viteAlias = viteAlias2, unreadableAlias = unreadableAlias2;
3711
+ const text = withoutComments(raw);
3712
+ const words = mapperOnly === true ? /\bmoduleNameMapper\b/g : /\b(?:alias|moduleNameMapper)\b/g;
3713
+ for (const match of text.matchAll(words)) {
3714
+ const word = match[0];
3715
+ const opening = /^['"]?\s*:\s*/.exec(text.slice(match.index + word.length));
3716
+ const at = opening === null ? -1 : match.index + word.length + opening[0].length;
3717
+ const found = at === -1 ? null : partsOf(text, at);
3718
+ const unreadable = `${shown(file)} names ${word} in a way that cannot be read as a list of aliases`;
3719
+ if (found === null || text[at] !== "{" && !(word === "alias" && text[at] === "[")) {
3720
+ reasons.push(unreadable);
3721
+ continue;
3722
+ }
3723
+ for (const part of found.parts) {
3724
+ if (word === "alias") {
3725
+ const reason = viteAlias2(part, text[at] === "[");
3726
+ if (reason !== null) {
3727
+ reasons.push(reason);
3728
+ }
3729
+ continue;
3730
+ }
3731
+ const entry = property(part);
3732
+ if (entry === null) {
3733
+ reasons.push(unreadable);
3734
+ continue;
3735
+ }
3736
+ const value = stringValue(entry.value);
3737
+ const root = [...text.matchAll(/\brootDir\b['"]?\s*:\s*([^,}\n]+)/g)].map((one) => one[1].trim()).find((set) => !ROOT_DIR_HERE.has(set));
3738
+ if (root !== void 0 && (value === null || value.includes("<rootDir>"))) {
3739
+ reasons.push(`${shown(file)} sets rootDir to a directory that cannot be told: ${root}`);
3740
+ continue;
3741
+ }
3742
+ if (value !== null && !value.includes("$")) {
3743
+ continue;
3744
+ }
3745
+ const prefix = mapperPrefix(entry.key);
3746
+ const target = value === null ? null : /^<rootDir>\/(.*)\$1$/.exec(value);
3747
+ const to = target === null ? null : under(path6.dirname(file), target[1]);
3748
+ if (prefix === null || to === null) {
3749
+ reasons.push(missing(file, entry.key, word));
3750
+ } else if (!named(prefix, to)) {
3751
+ reasons.push(missing(file, entry.key, word, prefix, to));
3752
+ } else {
3753
+ mapped.push(prefix);
3754
+ }
3755
+ }
3756
+ }
2850
3757
  }
2851
- for (const gate of input.gates) {
2852
- const gatePath = normalizePath(gate.path);
2853
- const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
2854
- violations.push(...shapeViolations);
2855
- violations.push(
2856
- ...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
2857
- );
2858
- const result = files.get(gatePath);
2859
- if (!result || !Array.isArray(result.mutants) || result.mutants.length === 0) {
2860
- violations.push({
2861
- file: gate.path,
2862
- message: "no mutants in the report (the gate was not mutation tested)"
2863
- });
3758
+ const tsconfigs = input.compiler.filter(({ file }) => /^[jt]sconfig/.test(path6.basename(file)));
3759
+ const withBase = tsconfigs.filter(({ text }) => text !== null && /"baseUrl"\s*:/.test(text));
3760
+ for (const { file, text } of input.compiler) {
3761
+ if (text === null) {
2864
3762
  continue;
2865
3763
  }
2866
- const diskSource = input.readSource(gate.path);
2867
- const source = result.source ?? diskSource;
2868
- if (source === void 0) {
2869
- violations.push({
2870
- file: gate.path,
2871
- message: "source is neither in the report nor readable on disk"
2872
- });
3764
+ const plain2 = withoutComments(text);
3765
+ const name = path6.basename(file);
3766
+ const swc = name === ".swcrc";
3767
+ if (!/^[jt]sconfig/.test(name) && !swc) {
3768
+ const babel = /"babel"\s*:\s*/.exec(plain2);
3769
+ const own2 = name !== "package.json" ? plain2 : babel === null ? "" : (() => {
3770
+ const found2 = partsOf(plain2, babel.index + babel[0].length);
3771
+ return found2 === null ? plain2 : found2.parts.join(",");
3772
+ })();
3773
+ const maps = /module-resolver|\balias\b/.exec(own2);
3774
+ if (maps !== null) {
3775
+ reasons.push(`${shown(file)} maps modules (${maps[0]}), which cannot be told`);
3776
+ }
2873
3777
  continue;
2874
3778
  }
2875
- if (result.source !== void 0 && diskSource !== void 0 && normalizeNewlines(result.source) !== normalizeNewlines(diskSource)) {
2876
- violations.push({
2877
- file: gate.path,
2878
- message: "the report was made from a different version of this file. Run Stryker again"
2879
- });
3779
+ const paths = /"paths"\s*:\s*/.exec(plain2);
3780
+ if (paths === null || !swc && input.testRunner === "jest") {
2880
3781
  continue;
2881
3782
  }
2882
- const located = [];
2883
- const byKey = /* @__PURE__ */ new Map();
2884
- const sorted2 = [...result.mutants].sort(
2885
- (a, b) => a.location.start.line - b.location.start.line || a.location.start.column - b.location.start.column
2886
- );
2887
- let locationsValid = true;
2888
- for (const mutant of sorted2) {
2889
- const original = sliceByLocation(normalizeNewlines(source), mutant.location);
2890
- if (original === null) {
2891
- violations.push({
2892
- file: gate.path,
2893
- line: mutant.location.start.line,
2894
- message: `the report location of a ${mutant.mutatorName} mutant is outside the source`
2895
- });
2896
- locationsValid = false;
2897
- continue;
2898
- }
2899
- const replacement = mutant.replacement ?? "";
2900
- const key = keyOf(mutant.mutatorName, original, replacement);
2901
- const group = byKey.get(key) ?? [];
2902
- const entry = {
2903
- mutant,
2904
- original,
2905
- replacement,
2906
- occurrence: group.length + 1,
2907
- sameKeyCount: 0
2908
- };
2909
- group.push(entry);
2910
- byKey.set(key, group);
2911
- located.push(entry);
2912
- }
2913
- for (const group of byKey.values()) {
2914
- for (const entry of group) {
2915
- entry.sameKeyCount = group.length;
2916
- }
3783
+ const at = paths.index + paths[0].length;
3784
+ const found = plain2[at] === "{" ? partsOf(plain2, at) : null;
3785
+ if (found === null) {
3786
+ reasons.push(`${shown(file)} names paths in a way that cannot be read as a list of aliases`);
3787
+ continue;
2917
3788
  }
2918
- if (!locationsValid) {
3789
+ const own = /"baseUrl"\s*:\s*"([^"]*)"/.exec(plain2);
3790
+ if (own === null && !swc && withBase.length > 0) {
3791
+ reasons.push(`${shown(file)} names paths, and baseUrl is set in another file`);
2919
3792
  continue;
2920
3793
  }
2921
- const claim = (field, entries, noun, accept) => {
2922
- const claimed = /* @__PURE__ */ new Set();
2923
- if (!Array.isArray(entries)) {
2924
- return claimed;
3794
+ const base = path6.resolve(path6.dirname(file), own?.[1] ?? ".");
3795
+ for (const part of found.parts) {
3796
+ const entry = property(part);
3797
+ if (entry === null) {
3798
+ reasons.push(
3799
+ `${shown(file)} names paths in a way that cannot be read as a list of aliases`
3800
+ );
3801
+ continue;
2925
3802
  }
2926
- entries.forEach((entry, index) => {
2927
- if (!isUsableEquivalentMutant(entry)) {
2928
- return;
2929
- }
2930
- const at = `${gate.path}: ${field}[${index}] (${describeAllowance(entry)})`;
2931
- const group = byKey.get(keyOf(entry.mutator, entry.original, entry.replacement)) ?? [];
2932
- if (entry.occurrence === void 0 && group.length > 1) {
2933
- violations.push({
2934
- file: MANIFEST_FILE,
2935
- message: `${at}: matches ${group.length} mutants in the file. Add "occurrence" (1-based) to pick one`
2936
- });
2937
- return;
2938
- }
2939
- const target = group[(entry.occurrence ?? 1) - 1];
2940
- const refusal = target ? accept(target.mutant.status) : `stale ${noun} (no such mutant in this run). Remove it`;
2941
- if (!target || refusal !== null) {
2942
- violations.push({ file: MANIFEST_FILE, message: `${at}: ${refusal}` });
2943
- return;
2944
- }
2945
- if (claimed.has(target)) {
2946
- violations.push({
2947
- file: MANIFEST_FILE,
2948
- message: `${at}: another ${noun} already covers this mutant`
2949
- });
2950
- return;
2951
- }
2952
- claimed.add(target);
2953
- });
2954
- return claimed;
2955
- };
2956
- const allowed = claim(
2957
- "equivalentMutants",
2958
- gate.equivalentMutants,
2959
- "allowance",
2960
- (status) => SURVIVED.has(status) ? null : `stale allowance (the mutant is now ${status}). Remove it`
2961
- );
2962
- const expected = claim(
2963
- "expectedTimeouts",
2964
- gate.expectedTimeouts,
2965
- "entry",
2966
- (status) => SURVIVED.has(status) ? `the mutant ${status === "NoCoverage" ? "has no coverage" : "survived"} instead of timing out. Detect it with a test` : null
2967
- );
2968
- for (const entry of located) {
2969
- const { mutant } = entry;
2970
- summary.total += 1;
2971
- if (DETECTED.has(mutant.status)) {
2972
- summary.detected += 1;
2973
- if (mutant.status === "Timeout") {
2974
- summary.timeout += 1;
2975
- if (expected.has(entry)) {
2976
- summary.expectedTimeout += 1;
2977
- } else {
2978
- unexpectedTimeouts.push(describeMutant(gate.path, entry));
2979
- }
2980
- }
2981
- } else if (NOT_EVALUABLE.has(mutant.status)) {
2982
- summary.notEvaluable += 1;
2983
- } else if (SURVIVED.has(mutant.status)) {
2984
- if (allowed.has(entry)) {
2985
- summary.allowed += 1;
2986
- continue;
2987
- }
2988
- summary.unallowed += 1;
2989
- survivors.push(describeMutant(gate.path, entry));
2990
- } else {
2991
- violations.push({
2992
- file: gate.path,
2993
- line: mutant.location.start.line,
2994
- message: mutant.status === "Ignored" ? `a ${mutant.mutatorName} mutant was ignored. Taking mutants out with Stryker comments or mutator exclusions is not allowed` : `a ${mutant.mutatorName} mutant was not evaluated (status: ${mutant.status})`
2995
- });
3803
+ const list = entry.value.startsWith("[") ? partsOf(entry.value, 0) : null;
3804
+ const target = list?.parts.length === 1 ? stringValue(list.parts[0]) : null;
3805
+ const prefix = entry.key.endsWith("*") ? entry.key.slice(0, -1) : null;
3806
+ if (prefix === null && target !== null && !target.includes("*") && path6.resolve(base, target).split(path6.sep).includes("node_modules")) {
3807
+ continue;
3808
+ }
3809
+ const directory = target?.endsWith("*") ? target.slice(0, -1) : null;
3810
+ if (!swc && prefix !== null && mapped.some((one) => prefix.startsWith(one))) {
3811
+ continue;
3812
+ }
3813
+ if (prefix === null || directory === null) {
3814
+ reasons.push(missing(file, entry.key, "paths"));
3815
+ } else if (!named(prefix, under(base, directory))) {
3816
+ reasons.push(missing(file, entry.key, "paths", prefix, under(base, directory)));
2996
3817
  }
2997
3818
  }
2998
3819
  }
2999
- if (summary.total === 0) {
3000
- violations.push({
3001
- file: MANIFEST_FILE,
3002
- message: "no mutants were evaluated (the mutation test ran on nothing)"
3003
- });
3004
- }
3005
- const maxTimeouts = input.maxTimeouts ?? 0;
3006
- if (unexpectedTimeouts.length > maxTimeouts) {
3007
- violations.push({
3008
- file: MANIFEST_FILE,
3009
- message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
3010
- });
3011
- }
3012
- return { violations, survivors, unexpectedTimeouts, summary };
3013
- }
3014
- function formatSurvivor(survivor) {
3015
- const occurrence = survivor.sameKeyCount > 1 ? ` / occurrence ${survivor.occurrence} of ${survivor.sameKeyCount}` : "";
3016
- return `${survivor.file}:${survivor.line}:${survivor.column} / ${survivor.mutator} / ${survivor.original} \u2192 ${survivor.replacement}${occurrence} (${survivor.status})`;
3017
- }
3018
- function formatSummary(summary) {
3019
- return `mutants ${summary.total} / detected ${summary.detected} (timeout ${summary.timeout}) / allowed equivalent ${summary.allowed} / not evaluable ${summary.notEvaluable} / unallowed survivors ${summary.unallowed}`;
3820
+ return reasons.length === 0 ? null : [...new Set(reasons)].join("; ");
3020
3821
  }
3021
3822
 
3823
+ // src/commands/mutation.ts
3824
+ import { spawnSync as spawnSync2 } from "child_process";
3825
+ import fs8 from "fs";
3826
+ import path8 from "path";
3827
+ import { pathToFileURL } from "url";
3828
+
3022
3829
  // src/commands/mutation-result.ts
3830
+ import fs7 from "fs";
3831
+ import path7 from "path";
3023
3832
  function reportPathOf(manifest, override) {
3024
3833
  return path7.resolve(manifest.dir, override ?? manifest.settings.strykerReportFile);
3025
3834
  }
@@ -3124,7 +3933,7 @@ function loadStrykerConfig(manifest, config) {
3124
3933
  return void 0;
3125
3934
  }
3126
3935
  }
3127
- const script = `const loaded = await import(${JSON.stringify(pathToFileURL(file).href)});const config = loaded.default?.default ?? loaded.default ?? loaded;process.stdout.write(${JSON.stringify(CONFIG_MARK)} + JSON.stringify({incremental: config.incremental, force: config.force,incrementalFile: config.incrementalFile, tempDirName: config.tempDirName,testRunner: config.testRunner, vitestConfig: config.vitest?.configFile,jestConfig: config.jest?.configFile,}) + ${JSON.stringify(CONFIG_MARK)});`;
3936
+ const script = `const loaded = await import(${JSON.stringify(pathToFileURL(file).href)});const config = loaded.default?.default ?? loaded.default ?? loaded;process.stdout.write(${JSON.stringify(CONFIG_MARK)} + JSON.stringify({incremental: config.incremental, force: config.force,incrementalFile: config.incrementalFile, tempDirName: config.tempDirName,testRunner: config.testRunner, vitestConfig: config.vitest?.configFile,jestConfig: config.jest?.configFile, checkers: config.checkers,htmlReporter: config.htmlReporter, jsonReporter: config.jsonReporter,ignorePatterns: config.ignorePatterns,}) + ${JSON.stringify(CONFIG_MARK)});`;
3128
3937
  const result = spawnSync2(process.execPath, ["--input-type=module", "--eval", script], {
3129
3938
  cwd: manifest.dir,
3130
3939
  encoding: "utf8",
@@ -3177,7 +3986,13 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3177
3986
  const loaded = loadStrykerConfig(manifest, config);
3178
3987
  const options = incrementalOptions(loaded, strykerArgs);
3179
3988
  const base = fs8.realpathSync(manifest.dir);
3180
- const configs = () => runnerConfigs(base, config, loaded ?? {});
3989
+ const checkersArg = strykerArgs.some(
3990
+ (arg) => arg === "--checkers" || arg.startsWith("--checkers=")
3991
+ );
3992
+ const configs = () => runnerConfigs(base, config, {
3993
+ ...loaded ?? {},
3994
+ ...checkersArg ? { checkers: ["--checkers"] } : {}
3995
+ });
3181
3996
  const decision = {
3182
3997
  incremental: false,
3183
3998
  incrementalFile: null,
@@ -3207,12 +4022,24 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3207
4022
  let snapshot = null;
3208
4023
  let root = base;
3209
4024
  let failure = "the Stryker config could not be loaded to see where the incremental file is";
4025
+ const reports = reportFiles(loaded).map((file) => path8.resolve(base, file));
4026
+ const ignores = strykerIgnores(loaded);
4027
+ const read2 = (file) => {
4028
+ const relative = path8.relative(base, file);
4029
+ const inside = !relative.startsWith("..") && !path8.isAbsolute(relative);
4030
+ return !reports.includes(file) && !(inside && ignores(relative.split(path8.sep).join("/")));
4031
+ };
3210
4032
  if (loaded !== void 0) {
3211
4033
  const scope = (listed2) => {
3212
4034
  try {
3213
4035
  const found = loadedScope(manifest, configs(), listed2);
3214
- decision.scope = "files" in found ? { files: found.files.size } : found;
3215
- return "files" in found ? found.files : null;
4036
+ if (!("files" in found)) {
4037
+ decision.scope = found;
4038
+ return null;
4039
+ }
4040
+ const files = new Set([...found.files].filter(read2));
4041
+ decision.scope = { files: files.size };
4042
+ return files;
3216
4043
  } catch (error) {
3217
4044
  decision.scope = { every: error.message };
3218
4045
  return null;
@@ -3227,7 +4054,8 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3227
4054
  incrementalFile,
3228
4055
  recordFile,
3229
4056
  path8.resolve(base, options.tempDirName),
3230
- path8.resolve(base, manifest.settings.strykerReportFile)
4057
+ path8.resolve(base, manifest.settings.strykerReportFile),
4058
+ ...reports
3231
4059
  ],
3232
4060
  args: comparableArgs(strykerArgs)
3233
4061
  }));
@@ -3260,14 +4088,18 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3260
4088
  force = failure;
3261
4089
  } else if (!affected) {
3262
4090
  force = refuseReuse({ record: stored, incremental, snapshot });
4091
+ if (force === null) {
4092
+ const read3 = readRecord({ record: stored, incremental, snapshot });
4093
+ last = "record" in read3 ? read3.record.affected : void 0;
4094
+ }
3263
4095
  } else if (ci !== null) {
3264
4096
  force = ci;
3265
4097
  } else {
3266
- const read2 = readRecord({ record: stored, incremental, snapshot });
3267
- if ("refused" in read2) {
3268
- force = read2.refused;
4098
+ const read3 = readRecord({ record: stored, incremental, snapshot });
4099
+ if ("refused" in read3) {
4100
+ force = read3.refused;
3269
4101
  } else {
3270
- last = read2.record.affected;
4102
+ last = read3.record.affected;
3271
4103
  try {
3272
4104
  const inputs = affectedInputs(manifest, {
3273
4105
  root,
@@ -3279,7 +4111,7 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3279
4111
  )
3280
4112
  });
3281
4113
  plan = planAffected({
3282
- before: { files: read2.record.snapshot.files, affected: last },
4114
+ before: { files: read3.record.snapshot.files, affected: last },
3283
4115
  now: snapshot.files,
3284
4116
  subproject: inputs.subproject,
3285
4117
  config: inputs.config,
@@ -3323,7 +4155,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
3323
4155
  ]
3324
4156
  } : nothing;
3325
4157
  }
3326
- const { snapshot, root, plan, last, ci, configs, force } = decision;
4158
+ const { snapshot, root, plan, last, configs, force } = decision;
3327
4159
  const incrementalFile = decision.incrementalFile;
3328
4160
  const recordFile = decision.recordFile;
3329
4161
  const incremental = decision.incrementalHash;
@@ -3337,34 +4169,46 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
3337
4169
  const rerun = plan !== null && "rerun" in plan ? plan.rerun.map((entry) => entry.gate) : null;
3338
4170
  const everyGate = force ?? (rerun !== null && rerun.length > 0 && rerun.length === manifest.gates.length ? "every gate is affected" : null);
3339
4171
  let ranges = [];
4172
+ let timedOut = 0;
4173
+ let expected = 0;
3340
4174
  if (everyGate === null && incremental !== null && !options.force) {
3341
- const kept = rerun === null ? manifest.gates.map((gate) => gate.path) : manifest.gates.map((gate) => gate.path).filter((gate) => !rerun.includes(gate));
4175
+ const kept = rerun === null ? manifest.gates : manifest.gates.filter((gate) => !rerun.includes(gate.path));
3342
4176
  try {
3343
- ranges = timeoutRanges(
4177
+ ({
4178
+ ranges,
4179
+ rerun: timedOut,
4180
+ expected
4181
+ } = timeoutRanges(
3344
4182
  JSON.parse(fs8.readFileSync(incrementalFile, "utf8")),
3345
4183
  kept
3346
- );
4184
+ ));
3347
4185
  } catch {
3348
- ranges = [];
3349
4186
  }
3350
4187
  }
3351
4188
  const subset = rerun !== null && rerun.length > 0 && everyGate === null ? rerun : [];
4189
+ const ran = affected ? rerun : everyGate !== null || options.force || incremental === null ? null : [];
3352
4190
  let said = [];
3353
4191
  if (affected && !options.force) {
3354
4192
  if (force !== null) {
3355
4193
  said = describePlan({ full: force });
3356
4194
  } else if (plan !== null) {
3357
- said = describePlan(plan, 5, ranges.length);
4195
+ said = describePlan(plan, 5, timedOut, expected);
3358
4196
  } else if (decision.unchanged !== null) {
3359
4197
  said = [
3360
4198
  `test-gates mutation --affected: nothing changed since the last run, every result is reused (gate by gate, ${decision.unchanged.replace(/^what a gate/, "what each gate")})`,
3361
- ...ranges.length > 0 ? [` and ${ranges.length} mutant(s) that timed out in the last run are run again`] : []
4199
+ ...ranges.length > 0 ? [` and ${timedOut} mutant(s) that timed out in the last run are run again`] : [],
4200
+ ...expected > 0 ? [` ${describeExpected(expected)}`] : []
3362
4201
  ];
3363
4202
  }
3364
- } else if (ranges.length > 0) {
3365
- said = [
3366
- `test-gates mutation: ${ranges.length} mutant(s) that timed out in the last run are run again, the other results are reused`
3367
- ];
4203
+ } else {
4204
+ if (ranges.length > 0) {
4205
+ said.push(
4206
+ `test-gates mutation: ${timedOut} mutant(s) that timed out in the last run are run again, the other results are reused`
4207
+ );
4208
+ }
4209
+ if (expected > 0) {
4210
+ said.push(`test-gates mutation: ${describeExpected(expected)}`);
4211
+ }
3368
4212
  }
3369
4213
  return {
3370
4214
  // Every gate is run again: the same as a run of every mutant.
@@ -3377,8 +4221,8 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
3377
4221
  return;
3378
4222
  }
3379
4223
  const record = { format: RECORD_FORMAT, incremental: written, snapshot };
3380
- if (affected && ci === null && judged !== null) {
3381
- record.affected = affectedRecord(judged, rerun, last);
4224
+ if (affectedRefused(process.env) === null && judged !== null) {
4225
+ record.affected = affectedRecord(judged, ran, last);
3382
4226
  }
3383
4227
  try {
3384
4228
  fs8.writeFileSync(recordFile, `${JSON.stringify(record)}
@@ -3387,7 +4231,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
3387
4231
  }
3388
4232
  }
3389
4233
  };
3390
- function affectedRecord(judged, ran, previous) {
4234
+ function affectedRecord(judged, ran2, previous) {
3391
4235
  const gates = manifest.gates.map((gate) => gate.path);
3392
4236
  const dirty = new Set(
3393
4237
  [...judged.verdict.survivors, ...judged.verdict.unexpectedTimeouts].map((one) => one.file)
@@ -3395,7 +4239,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
3395
4239
  const fresh = coveringSpecs(judged.report, gates);
3396
4240
  const covering = /* @__PURE__ */ Object.create(null);
3397
4241
  for (const gate of gates) {
3398
- const specs = ran === null || ran.includes(gate) ? fresh[gate] : previous?.gates[gate]?.specs ?? null;
4242
+ const specs = ran2 === null || ran2.includes(gate) ? fresh[gate] : previous?.gates[gate]?.specs ?? null;
3399
4243
  if (specs !== null && specs !== void 0 && !dirty.has(gate)) {
3400
4244
  covering[gate] = specs;
3401
4245
  }
@@ -3417,7 +4261,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
3417
4261
  }
3418
4262
  return { tests: Object.keys(judged.report.testFiles ?? {}).sort(), gates: recorded };
3419
4263
  } catch (error) {
3420
- if (error instanceof AffectedError) {
4264
+ if (error instanceof AffectedError || !affected) {
3421
4265
  return void 0;
3422
4266
  }
3423
4267
  throw error;