@t09tanaka/test-gates 0.4.0 → 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";
@@ -1697,6 +2052,13 @@ function diagnose(facts) {
1697
2052
  fix: "write such a path from the directory of its file (path.resolve(__dirname, \u2026)) so that it can be followed, or keep it out of the files the run loads"
1698
2053
  });
1699
2054
  }
2055
+ if (facts.blind !== null) {
2056
+ findings.push({
2057
+ level: facts.hooks.some((hook) => hook.affected) ? "problem" : "note",
2058
+ text: `with --affected, a change of any file runs every mutant: what a gate depends on cannot be told (${facts.blind})`,
2059
+ fix: "keep what reads files or loads modules by a computed name out of what the gates and their specs import, or run without --affected"
2060
+ });
2061
+ }
1700
2062
  if (facts.scope !== null && "every" in facts.scope) {
1701
2063
  findings.push({
1702
2064
  level: "note",
@@ -1711,7 +2073,7 @@ function diagnose(facts) {
1711
2073
  fix: null
1712
2074
  });
1713
2075
  }
1714
- for (const hook of facts.hooks.filter((one) => !one.affected)) {
2076
+ for (const hook of facts.hooks.filter((one) => !one.affected && facts.blind === null)) {
1715
2077
  findings.push({
1716
2078
  level: "problem",
1717
2079
  text: `${hook.file} runs test-gates mutation without --affected: a change of any file of the subproject runs every mutant`,
@@ -1774,23 +2136,50 @@ var SCRIPT = /\.[cm]?[jt]sx?$/;
1774
2136
  var COMPONENT = /\.(?:vue|svelte|astro|mdx)$/;
1775
2137
  var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
1776
2138
  var QUOTED = /(['"`])([^'"`\n]+)\1/g;
1777
- 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;
1778
2140
  function isFile(file) {
1779
2141
  try {
1780
- return fs4.statSync(file).isFile();
2142
+ return fs4.statSync(file, { throwIfNoEntry: false })?.isFile() === true;
1781
2143
  } catch {
1782
2144
  return false;
1783
2145
  }
1784
2146
  }
1785
- function resolveFile(base, directory = true) {
1786
- const candidates = [
1787
- base,
1788
- ...EXTENSIONS.map((extension) => base + extension),
1789
- // `./fee.js` written in a TypeScript file is `./fee.ts`
1790
- ...EXTENSIONS.map((extension) => base.replace(/\.[cm]?jsx?$/, extension)),
1791
- ...directory ? EXTENSIONS.map((extension) => path4.join(base, `index${extension}`)) : []
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}`)) : []
1792
2176
  ];
1793
- return candidates.find(isFile) ?? null;
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;
1794
2183
  }
1795
2184
  function installed(specifier, from) {
1796
2185
  const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
@@ -1804,14 +2193,17 @@ function installed(specifier, from) {
1804
2193
  }
1805
2194
  }
1806
2195
  function loadedFiles(manifest, configFile) {
2196
+ return resolving(() => loadedFilesNow(manifest, configFile));
2197
+ }
2198
+ function loadedFilesNow(manifest, configFile) {
1807
2199
  const aliases = manifest.settings.importAliases;
1808
2200
  const relative = (file) => path4.relative(manifest.dir, file).split(path4.sep).join("/");
1809
2201
  const target = (specifier, from) => {
1810
2202
  if (specifier.startsWith(".")) {
1811
- return resolveFile(path4.resolve(path4.dirname(from), specifier));
2203
+ return resolveFiles(path4.resolve(path4.dirname(from), specifier));
1812
2204
  }
1813
2205
  const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
1814
- return alias === void 0 ? null : resolveFile(
2206
+ return alias === void 0 ? [] : resolveFiles(
1815
2207
  path4.resolve(manifest.dir, alias.target + specifier.slice(alias.prefix.length))
1816
2208
  );
1817
2209
  };
@@ -1836,16 +2228,18 @@ function loadedFiles(manifest, configFile) {
1836
2228
  const paths = strings.filter((text) => file === entry || /^(?:\.|<rootDir>\/)/.test(text));
1837
2229
  for (const text of paths) {
1838
2230
  const stripped = text.replace(/^<rootDir>\//, "");
1839
- const found = resolveFile(path4.resolve(path4.dirname(file), stripped), false) ?? resolveFile(path4.resolve(manifest.dir, stripped), false);
1840
- if (found !== null && SCRIPT.test(found)) {
1841
- 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);
1842
2235
  }
1843
2236
  }
1844
2237
  for (const specifier of specifiers.filter((one) => isProjectImport(one, aliases))) {
1845
- const found = target(specifier, file);
1846
- if (found !== null && SCRIPT.test(found) && !seenConfigs.has(found)) {
1847
- seenConfigs.add(found);
1848
- 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
+ }
1849
2243
  }
1850
2244
  }
1851
2245
  }
@@ -1862,10 +2256,10 @@ function loadedFiles(manifest, configFile) {
1862
2256
  for (const specifier of specifiers) {
1863
2257
  if (isProjectImport(specifier, aliases)) {
1864
2258
  const found = target(specifier, file);
1865
- if (found === null) {
2259
+ if (found.length === 0) {
1866
2260
  lost ??= `${specifier} (imported by ${relative(file)})`;
1867
2261
  } else {
1868
- next.push(found);
2262
+ next.push(...found);
1869
2263
  }
1870
2264
  } else if (specifier.startsWith("node:") || builtinModules.includes(specifier)) {
1871
2265
  imports.push({ specifier, builtin: true });
@@ -1876,10 +2270,7 @@ function loadedFiles(manifest, configFile) {
1876
2270
  }
1877
2271
  }
1878
2272
  for (const text of strings.filter((one) => one.startsWith("."))) {
1879
- const found = resolveFile(path4.resolve(path4.dirname(file), text));
1880
- if (found !== null) {
1881
- next.push(found);
1882
- }
2273
+ next.push(...resolveFiles(path4.resolve(path4.dirname(file), text)));
1883
2274
  }
1884
2275
  const visited = {
1885
2276
  file: { path: relative(file), source, imports },
@@ -1950,10 +2341,11 @@ var NEVER = ["node_modules", ".git"];
1950
2341
  var WIDENED_SHOWN = 5;
1951
2342
  var SCAN_LIMIT = 8 * 1024 * 1024;
1952
2343
  var UPWARD = /(['"`])((?:[^'"`\n]*\/)?\.\.(?:\/[^'"`\n]*)?)\1/g;
1953
- var DOCUMENTS = [".md", ".markdown", ".txt", ".rst", ".adoc"];
2344
+ var DOCUMENTS = [".md", ".markdown", ".txt", ".rst", ".adoc", ".html", ".htm"];
1954
2345
  var HERE = /^\$\{(?:__dirname|import\.meta\.dirname)\}(?=\/)/;
1955
2346
  var FROM_HERE = /\b(?:__dirname|import\.meta\.dirname)((?:\s*,\s*(['"])[^'"`\n]*\2)+)(\s*\))?/g;
1956
2347
  var LITERAL = /(['"])([^'"`\n]*)\1/g;
2348
+ var DOWNWARD = /(['"`])(\.\/[^'"`\n]*)\1/g;
1957
2349
  var isUpward = (segment) => segment.split("/").includes("..");
1958
2350
  function hashOf(content) {
1959
2351
  return createHash("sha256").update(content).digest("hex");
@@ -1989,9 +2381,12 @@ function git(cwd, args) {
1989
2381
  });
1990
2382
  return result.error || result.status !== 0 ? null : result.stdout;
1991
2383
  }
2384
+ function globLiteral(relative) {
2385
+ return relative.replace(/[*?[\]\\]/g, "\\$&");
2386
+ }
1992
2387
  function gitLister(root) {
1993
2388
  return (directory, recursive) => {
1994
- const relative = toPosix(path5.relative(root, directory));
2389
+ const relative = globLiteral(toPosix(path5.relative(root, directory)));
1995
2390
  const pattern = recursive ? `${relative === "" ? "" : `${relative}/`}**` : `${relative === "" ? "" : `${relative}/`}*`;
1996
2391
  const listed2 = git(root, [
1997
2392
  "ls-files",
@@ -2115,11 +2510,24 @@ function installedVersion(start, name) {
2115
2510
  }
2116
2511
  }
2117
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
+ }
2118
2523
  function ownVersion() {
2119
2524
  const { version } = createRequire(import.meta.url)("../package.json");
2120
2525
  return version;
2121
2526
  }
2122
2527
  function snapshotOf(input) {
2528
+ return resolving(() => snapshotNow(input));
2529
+ }
2530
+ function snapshotNow(input) {
2123
2531
  const dir = fs5.realpathSync(input.dir);
2124
2532
  const top = git(dir, ["rev-parse", "--show-toplevel"]);
2125
2533
  let root = top === null ? dir : fs5.realpathSync(top.trim());
@@ -2173,19 +2581,74 @@ function snapshotOf(input) {
2173
2581
  }
2174
2582
  };
2175
2583
  const follow = (fixed, open, anchored, file, reference) => {
2176
- const target = path5.resolve(path5.dirname(file), fixed);
2177
- 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;
2178
2634
  try {
2179
- 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, "/");
2180
2640
  } catch {
2181
- }
2182
- if (!isDirectory && !isInside(listRoot, target) && resolveFile(target) === null) {
2183
- files[toPosix(path5.relative(listRoot, target))] = "missing";
2184
2641
  return;
2185
2642
  }
2186
- const directory = isDirectory ? target : path5.dirname(target);
2187
- if (fs5.existsSync(directory)) {
2188
- 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
+ }
2189
2652
  }
2190
2653
  };
2191
2654
  const scan = (text, file) => {
@@ -2257,8 +2720,17 @@ function snapshotOf(input) {
2257
2720
  }
2258
2721
  };
2259
2722
  const ancestors = upTo(dir, listRoot).slice(1);
2260
- for (const directory of ancestors) {
2261
- 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
+ }
2262
2734
  }
2263
2735
  for (const directory of [dir, ...ancestors]) {
2264
2736
  for (const linked of linkedPackages(path5.join(directory, "node_modules"), listRoot)) {
@@ -2280,11 +2752,23 @@ function snapshotOf(input) {
2280
2752
  }
2281
2753
  }
2282
2754
  }
2283
- for (let position = 0; position < regions.length; position += 1) {
2284
- 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
+ }
2285
2767
  }
2768
+ drain();
2286
2769
  const environment = {
2287
2770
  "Node.js": process.version,
2771
+ ...nodeOnPath(dir),
2288
2772
  platform: `${process.platform} ${process.arch}`,
2289
2773
  "test-gates": ownVersion()
2290
2774
  };
@@ -2297,19 +2781,23 @@ function snapshotOf(input) {
2297
2781
  return { snapshot: { mode, environment, args: input.args, files }, root: listRoot, widened };
2298
2782
  }
2299
2783
 
2300
- // src/commands/mutation.ts
2301
- import { spawnSync as spawnSync2 } from "child_process";
2302
- import fs8 from "fs";
2303
- import path8 from "path";
2304
- import { pathToFileURL } from "url";
2305
-
2306
2784
  // src/affected-files.ts
2307
2785
  import fs6 from "fs";
2308
2786
  import { builtinModules as builtinModules2 } from "module";
2309
2787
  import path6 from "path";
2310
2788
  var AffectedError = class extends Error {
2311
2789
  };
2312
- 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
+ ]);
2313
2801
  var FILE_MODULES = /* @__PURE__ */ new Set([
2314
2802
  "fs",
2315
2803
  "fs/promises",
@@ -2365,10 +2853,98 @@ function runnerConfigs(dir, stryker, options) {
2365
2853
  }
2366
2854
  }
2367
2855
  }
2368
- 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
+ }
2369
2945
  }
2370
2946
  function fileEdges(file, context) {
2371
- const { dir, aliases, cache } = context;
2947
+ const { dir, aliases, cache, types } = context;
2372
2948
  const known = cache.get(file);
2373
2949
  if (known !== void 0) {
2374
2950
  return known;
@@ -2376,10 +2952,10 @@ function fileEdges(file, context) {
2376
2952
  const shown = (one) => toPosix2(path6.relative(dir, one));
2377
2953
  const target = (specifier, from) => {
2378
2954
  if (specifier.startsWith(".")) {
2379
- return resolveFile(path6.resolve(path6.dirname(from), specifier));
2955
+ return resolveFiles(path6.resolve(path6.dirname(from), specifier));
2380
2956
  }
2381
2957
  const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
2382
- 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)));
2383
2959
  };
2384
2960
  const found = { loads: [], unknown: null, computed: false, unnamed: [] };
2385
2961
  cache.set(file, found);
@@ -2391,41 +2967,61 @@ function fileEdges(file, context) {
2391
2967
  if (fs6.existsSync(snapshotFile)) {
2392
2968
  found.loads.push(realpath(snapshotFile));
2393
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
+ }
2394
2981
  if (!SCRIPT.test(file) && !COMPONENT.test(file)) {
2395
2982
  return found;
2396
2983
  }
2397
2984
  const source = fs6.readFileSync(file, "utf8");
2985
+ const code = withoutComments(source);
2398
2986
  if (COMPONENT.test(file)) {
2399
2987
  found.unknown = `${shown(file)} is a component, which can use others that it does not import`;
2400
2988
  }
2401
- const uncheckable = findUncheckableImports(source)[0];
2402
- const runtime = RUNTIME_PATHS.exec(source);
2989
+ const uncheckable = findUncheckableImports(code)[0];
2990
+ const runtime = RUNTIME_PATHS.exec(code);
2403
2991
  if (uncheckable !== void 0) {
2404
2992
  found.unknown = `${shown(file)}:${uncheckable} imports a module whose path is not written out`;
2405
2993
  found.computed = true;
2406
2994
  } else if (runtime !== null) {
2407
2995
  found.unknown = `${shown(file)} uses ${runtime[0]}, which can reach files that no import names`;
2408
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);
2409
3000
  const specifiers = /* @__PURE__ */ new Set([
2410
- ...parseImports(source).map((ref) => ref.module),
2411
- ...[...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])
2412
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
+ }
2413
3009
  for (const specifier of specifiers) {
2414
3010
  if (path6.isAbsolute(specifier) || specifier.startsWith("file:")) {
2415
- const resolved = resolveFile(
3011
+ const resolved2 = resolveFiles(
2416
3012
  specifier.startsWith("file:") ? new URL(specifier).pathname : specifier
2417
3013
  );
2418
- if (resolved === null) {
3014
+ if (resolved2.length === 0) {
2419
3015
  found.unknown ??= `${shown(file)} imports ${specifier}, which leads to no file`;
2420
3016
  } else {
2421
- found.loads.push(realpath(resolved));
3017
+ found.loads.push(...resolved2.map(realpath));
2422
3018
  }
2423
3019
  } else if (isProjectImport(specifier, aliases)) {
2424
- const resolved = target(specifier, file);
2425
- if (resolved === null) {
3020
+ const resolved2 = target(specifier, file);
3021
+ if (resolved2.length === 0) {
2426
3022
  found.unknown ??= `${shown(file)} imports ${specifier}, which leads to no file`;
2427
3023
  } else {
2428
- found.loads.push(realpath(resolved));
3024
+ found.loads.push(...resolved2.map(realpath));
2429
3025
  }
2430
3026
  } else if (FILE_MODULES.has(bare(specifier))) {
2431
3027
  found.unknown ??= `${shown(file)} imports ${specifier}, which can reach files that no import names`;
@@ -2439,18 +3035,40 @@ function fileEdges(file, context) {
2439
3035
  }
2440
3036
  }
2441
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;
2442
3051
  for (const match of source.matchAll(QUOTED)) {
2443
- const text = match[2];
2444
- if (text.startsWith(".") && !specifiers.has(text)) {
2445
- const resolved = resolveFile(path6.resolve(path6.dirname(file), text));
2446
- if (resolved !== null) {
2447
- found.loads.push(realpath(resolved));
3052
+ for (; counted < match.index; counted += 1) {
3053
+ if (source[counted] === "\n") {
3054
+ line += 1;
2448
3055
  }
2449
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
+ }
2450
3065
  }
2451
3066
  return found;
2452
3067
  }
2453
3068
  function affectedInputs(manifest, input) {
3069
+ return resolving(() => inputsOf(manifest, input));
3070
+ }
3071
+ function inputsOf(manifest, input) {
2454
3072
  const dir = realpath(manifest.dir);
2455
3073
  const aliases = manifest.settings.importAliases;
2456
3074
  const placed = [...input.tests, ...Object.values(input.covering).flat()];
@@ -2466,8 +3084,43 @@ function affectedInputs(manifest, input) {
2466
3084
  return toPosix2(relative);
2467
3085
  };
2468
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
+ }
2469
3122
  const edges = /* @__PURE__ */ new Map();
2470
- const edgesOf = (file) => fileEdges(file, { dir, aliases, cache: edges });
3123
+ const edgesOf = (file) => fileEdges(file, { dir, aliases, cache: edges, types });
2471
3124
  const closure = (start, strict) => {
2472
3125
  const seen = /* @__PURE__ */ new Set([start]);
2473
3126
  const queue = [start];
@@ -2508,9 +3161,17 @@ function affectedInputs(manifest, input) {
2508
3161
  }
2509
3162
  for (const match of fs6.readFileSync(loaded, "utf8").matchAll(QUOTED)) {
2510
3163
  const text = match[2].replace(/^<rootDir>\//, "");
2511
- const found = resolveFile(path6.resolve(path6.dirname(loaded), text), false) ?? resolveFile(path6.resolve(dir, text), false);
2512
- if (found !== null && !text.includes("*") && isCompared(found)) {
2513
- 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
+ }
2514
3175
  }
2515
3176
  }
2516
3177
  }
@@ -2578,7 +3239,22 @@ function affectedInputs(manifest, input) {
2578
3239
  files: found.files
2579
3240
  };
2580
3241
  }
2581
- 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
+ );
2582
3258
  }
2583
3259
  var NOT_THE_HELPER2 = "the runner config is not made with createJestGatesConfig or createVitestGatesConfig";
2584
3260
  var OWN = /@t09tanaka\/test-gates\/(?:jest|vitest)\b/;
@@ -2614,6 +3290,9 @@ var READ_BY_NAME = [
2614
3290
  /^\.swcrc$/
2615
3291
  ];
2616
3292
  function loadedScope(manifest, configs, listed2) {
3293
+ return resolving(() => scopeOf(manifest, configs, listed2));
3294
+ }
3295
+ function scopeOf(manifest, configs, listed2) {
2617
3296
  const dir = realpath(manifest.dir);
2618
3297
  const cache = /* @__PURE__ */ new Map();
2619
3298
  const context = { dir, aliases: manifest.settings.importAliases, cache };
@@ -2670,19 +3349,16 @@ function loadedScope(manifest, configs, listed2) {
2670
3349
  for (const specifier of edges.unnamed) {
2671
3350
  endingAs(specifier, listed2).forEach(add);
2672
3351
  }
2673
- const mock = path6.join(path6.dirname(file), "__mocks__", path6.basename(file));
2674
- if (fs6.existsSync(mock)) {
2675
- add(mock);
2676
- }
2677
3352
  if (naming.has(file) || SCRIPT.test(file) && path6.dirname(file) === dir) {
2678
3353
  for (const named2 of namedIn(file)) {
2679
3354
  const text = named2.replace(/^<rootDir>\//, "");
2680
- const found = resolveFile(path6.resolve(path6.dirname(file), text), false) ?? resolveFile(path6.resolve(dir, text), false);
2681
- if (found !== null && !text.includes("*")) {
2682
- if (!SCRIPT.test(found)) {
2683
- 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));
2684
3360
  }
2685
- add(found);
3361
+ add(one);
2686
3362
  }
2687
3363
  }
2688
3364
  }
@@ -2739,280 +3415,420 @@ function valuesOf(text, key) {
2739
3415
  return found;
2740
3416
  }
2741
3417
  function withoutComments(text) {
2742
- 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
+ };
2743
3434
  for (let at = 0; at < text.length; at += 1) {
2744
3435
  const char = text[at];
3436
+ const next = text[at + 1];
2745
3437
  if (char === "'" || char === '"' || char === "`") {
2746
- let end = at + 1;
2747
- while (end < text.length && text[end] !== char && text[end] !== "\n") {
2748
- end += text[end] === "\\" ? 2 : 1;
2749
- }
2750
- out += text.slice(at, end + 1);
3438
+ const end = stringEnd(text, at);
3439
+ keep(text.slice(at, end + 1));
2751
3440
  at = end;
2752
- } else if (char === "/" && text[at + 1] === "/") {
3441
+ } else if (char === "/" && next === "/" && !SCHEME.test(text.slice(Math.max(0, at - 40), at))) {
2753
3442
  const end = text.indexOf("\n", at);
2754
3443
  at = end === -1 ? text.length : end - 1;
2755
- } else if (char === "/" && text[at + 1] === "*") {
3444
+ } else if (char === "/" && next === "*") {
2756
3445
  const end = text.indexOf("*/", at + 2);
2757
- 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;
3453
+ } else {
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;
2758
3488
  } else {
2759
- out += char;
3489
+ end += 1;
2760
3490
  }
2761
3491
  }
2762
- return out;
3492
+ return end;
2763
3493
  }
2764
-
2765
- // src/commands/mutation-result.ts
2766
- import fs7 from "fs";
2767
- import path7 from "path";
2768
-
2769
- // src/core/mutation-result.ts
2770
- var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
2771
- var SURVIVED = /* @__PURE__ */ new Set(["Survived", "NoCoverage"]);
2772
- var NOT_EVALUABLE = /* @__PURE__ */ new Set(["RuntimeError", "CompileError"]);
2773
- function normalizeCode(text) {
2774
- return text.replace(/\s+/g, " ").trim();
2775
- }
2776
- function normalizePath(file) {
2777
- return file.replace(/\\/g, "/").replace(/^\.\//, "");
2778
- }
2779
- function normalizeNewlines(text) {
2780
- 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;
2781
3568
  }
2782
- function sliceByLocation(source, location) {
2783
- const lines = source.split("\n");
2784
- const { start, end } = location;
2785
- const first = lines[start.line - 1];
2786
- const last = lines[end.line - 1];
2787
- 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) {
2788
3572
  return null;
2789
3573
  }
2790
- if (start.line === end.line) {
2791
- 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);
2792
3599
  }
2793
- return [
2794
- first.slice(start.column - 1),
2795
- ...lines.slice(start.line, end.line - 1),
2796
- last.slice(0, end.column - 1)
2797
- ].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;
2798
3610
  }
2799
- function keyOf(mutator, original, replacement) {
2800
- 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
+ }
2801
3618
  }
2802
- function describeMutant(file, entry) {
2803
- const { mutant } = entry;
2804
- return {
2805
- file,
2806
- line: mutant.location.start.line,
2807
- column: mutant.location.start.column,
2808
- status: mutant.status,
2809
- mutator: mutant.mutatorName,
2810
- original: normalizeCode(entry.original),
2811
- replacement: normalizeCode(entry.replacement),
2812
- occurrence: entry.occurrence,
2813
- sameKeyCount: entry.sameKeyCount,
2814
- allowance: {
2815
- mutator: mutant.mutatorName,
2816
- original: normalizeCode(entry.original),
2817
- replacement: normalizeCode(entry.replacement),
2818
- ...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
2819
- reason: ""
2820
- }
2821
- };
3619
+ function regexPrefix(literal) {
3620
+ const match = /^\/\^((?:[\w@~#%-]|\\[./@~#-])*)\\\/\/$/.exec(literal.trim());
3621
+ return match === null ? null : `${match[1].replace(/\\(.)/g, "$1")}/`;
2822
3622
  }
2823
- function describeAllowance(entry) {
2824
- const occurrence = entry.occurrence === void 0 ? "" : ` / occurrence ${entry.occurrence}`;
2825
- 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");
2826
3626
  }
2827
- function judgeMutationReport(input) {
2828
- const violations = [];
2829
- const survivors = [];
2830
- const unexpectedTimeouts = [];
2831
- const summary = {
2832
- total: 0,
2833
- detected: 0,
2834
- timeout: 0,
2835
- allowed: 0,
2836
- notEvaluable: 0,
2837
- unallowed: 0,
2838
- 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("/");
2839
3650
  };
2840
- const files = /* @__PURE__ */ new Map();
2841
- for (const [file, result] of Object.entries(input.report.files ?? {})) {
2842
- 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
+ }
2843
3757
  }
2844
- for (const gate of input.gates) {
2845
- const gatePath = normalizePath(gate.path);
2846
- const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
2847
- violations.push(...shapeViolations);
2848
- violations.push(
2849
- ...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
2850
- );
2851
- const result = files.get(gatePath);
2852
- if (!result || !Array.isArray(result.mutants) || result.mutants.length === 0) {
2853
- violations.push({
2854
- file: gate.path,
2855
- message: "no mutants in the report (the gate was not mutation tested)"
2856
- });
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) {
2857
3762
  continue;
2858
3763
  }
2859
- const diskSource = input.readSource(gate.path);
2860
- const source = result.source ?? diskSource;
2861
- if (source === void 0) {
2862
- violations.push({
2863
- file: gate.path,
2864
- message: "source is neither in the report nor readable on disk"
2865
- });
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
+ }
2866
3777
  continue;
2867
3778
  }
2868
- if (result.source !== void 0 && diskSource !== void 0 && normalizeNewlines(result.source) !== normalizeNewlines(diskSource)) {
2869
- violations.push({
2870
- file: gate.path,
2871
- message: "the report was made from a different version of this file. Run Stryker again"
2872
- });
3779
+ const paths = /"paths"\s*:\s*/.exec(plain2);
3780
+ if (paths === null || !swc && input.testRunner === "jest") {
2873
3781
  continue;
2874
3782
  }
2875
- const located = [];
2876
- const byKey = /* @__PURE__ */ new Map();
2877
- const sorted2 = [...result.mutants].sort(
2878
- (a, b) => a.location.start.line - b.location.start.line || a.location.start.column - b.location.start.column
2879
- );
2880
- let locationsValid = true;
2881
- for (const mutant of sorted2) {
2882
- const original = sliceByLocation(normalizeNewlines(source), mutant.location);
2883
- if (original === null) {
2884
- violations.push({
2885
- file: gate.path,
2886
- line: mutant.location.start.line,
2887
- message: `the report location of a ${mutant.mutatorName} mutant is outside the source`
2888
- });
2889
- locationsValid = false;
2890
- continue;
2891
- }
2892
- const replacement = mutant.replacement ?? "";
2893
- const key = keyOf(mutant.mutatorName, original, replacement);
2894
- const group = byKey.get(key) ?? [];
2895
- const entry = {
2896
- mutant,
2897
- original,
2898
- replacement,
2899
- occurrence: group.length + 1,
2900
- sameKeyCount: 0
2901
- };
2902
- group.push(entry);
2903
- byKey.set(key, group);
2904
- located.push(entry);
2905
- }
2906
- for (const group of byKey.values()) {
2907
- for (const entry of group) {
2908
- entry.sameKeyCount = group.length;
2909
- }
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;
2910
3788
  }
2911
- 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`);
2912
3792
  continue;
2913
3793
  }
2914
- const claim = (field, entries, noun, accept) => {
2915
- const claimed = /* @__PURE__ */ new Set();
2916
- if (!Array.isArray(entries)) {
2917
- 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;
2918
3802
  }
2919
- entries.forEach((entry, index) => {
2920
- if (!isUsableEquivalentMutant(entry)) {
2921
- return;
2922
- }
2923
- const at = `${gate.path}: ${field}[${index}] (${describeAllowance(entry)})`;
2924
- const group = byKey.get(keyOf(entry.mutator, entry.original, entry.replacement)) ?? [];
2925
- if (entry.occurrence === void 0 && group.length > 1) {
2926
- violations.push({
2927
- file: MANIFEST_FILE,
2928
- message: `${at}: matches ${group.length} mutants in the file. Add "occurrence" (1-based) to pick one`
2929
- });
2930
- return;
2931
- }
2932
- const target = group[(entry.occurrence ?? 1) - 1];
2933
- const refusal = target ? accept(target.mutant.status) : `stale ${noun} (no such mutant in this run). Remove it`;
2934
- if (!target || refusal !== null) {
2935
- violations.push({ file: MANIFEST_FILE, message: `${at}: ${refusal}` });
2936
- return;
2937
- }
2938
- if (claimed.has(target)) {
2939
- violations.push({
2940
- file: MANIFEST_FILE,
2941
- message: `${at}: another ${noun} already covers this mutant`
2942
- });
2943
- return;
2944
- }
2945
- claimed.add(target);
2946
- });
2947
- return claimed;
2948
- };
2949
- const allowed = claim(
2950
- "equivalentMutants",
2951
- gate.equivalentMutants,
2952
- "allowance",
2953
- (status) => SURVIVED.has(status) ? null : `stale allowance (the mutant is now ${status}). Remove it`
2954
- );
2955
- const expected = claim(
2956
- "expectedTimeouts",
2957
- gate.expectedTimeouts,
2958
- "entry",
2959
- (status) => SURVIVED.has(status) ? `the mutant ${status === "NoCoverage" ? "has no coverage" : "survived"} instead of timing out. Detect it with a test` : null
2960
- );
2961
- for (const entry of located) {
2962
- const { mutant } = entry;
2963
- summary.total += 1;
2964
- if (DETECTED.has(mutant.status)) {
2965
- summary.detected += 1;
2966
- if (mutant.status === "Timeout") {
2967
- summary.timeout += 1;
2968
- if (expected.has(entry)) {
2969
- summary.expectedTimeout += 1;
2970
- } else {
2971
- unexpectedTimeouts.push(describeMutant(gate.path, entry));
2972
- }
2973
- }
2974
- } else if (NOT_EVALUABLE.has(mutant.status)) {
2975
- summary.notEvaluable += 1;
2976
- } else if (SURVIVED.has(mutant.status)) {
2977
- if (allowed.has(entry)) {
2978
- summary.allowed += 1;
2979
- continue;
2980
- }
2981
- summary.unallowed += 1;
2982
- survivors.push(describeMutant(gate.path, entry));
2983
- } else {
2984
- violations.push({
2985
- file: gate.path,
2986
- line: mutant.location.start.line,
2987
- 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})`
2988
- });
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)));
2989
3817
  }
2990
3818
  }
2991
3819
  }
2992
- if (summary.total === 0) {
2993
- violations.push({
2994
- file: MANIFEST_FILE,
2995
- message: "no mutants were evaluated (the mutation test ran on nothing)"
2996
- });
2997
- }
2998
- const maxTimeouts = input.maxTimeouts ?? 0;
2999
- if (unexpectedTimeouts.length > maxTimeouts) {
3000
- violations.push({
3001
- file: MANIFEST_FILE,
3002
- message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
3003
- });
3004
- }
3005
- return { violations, survivors, unexpectedTimeouts, summary };
3006
- }
3007
- function formatSurvivor(survivor) {
3008
- const occurrence = survivor.sameKeyCount > 1 ? ` / occurrence ${survivor.occurrence} of ${survivor.sameKeyCount}` : "";
3009
- return `${survivor.file}:${survivor.line}:${survivor.column} / ${survivor.mutator} / ${survivor.original} \u2192 ${survivor.replacement}${occurrence} (${survivor.status})`;
3010
- }
3011
- function formatSummary(summary) {
3012
- 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("; ");
3013
3821
  }
3014
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
+
3015
3829
  // src/commands/mutation-result.ts
3830
+ import fs7 from "fs";
3831
+ import path7 from "path";
3016
3832
  function reportPathOf(manifest, override) {
3017
3833
  return path7.resolve(manifest.dir, override ?? manifest.settings.strykerReportFile);
3018
3834
  }
@@ -3117,7 +3933,7 @@ function loadStrykerConfig(manifest, config) {
3117
3933
  return void 0;
3118
3934
  }
3119
3935
  }
3120
- 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)});`;
3121
3937
  const result = spawnSync2(process.execPath, ["--input-type=module", "--eval", script], {
3122
3938
  cwd: manifest.dir,
3123
3939
  encoding: "utf8",
@@ -3170,7 +3986,13 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3170
3986
  const loaded = loadStrykerConfig(manifest, config);
3171
3987
  const options = incrementalOptions(loaded, strykerArgs);
3172
3988
  const base = fs8.realpathSync(manifest.dir);
3173
- 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
+ });
3174
3996
  const decision = {
3175
3997
  incremental: false,
3176
3998
  incrementalFile: null,
@@ -3186,6 +4008,7 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3186
4008
  plan: null,
3187
4009
  last: void 0,
3188
4010
  ci: affected ? affectedRefused(process.env) : null,
4011
+ unchanged: null,
3189
4012
  configs
3190
4013
  };
3191
4014
  if (loaded !== void 0 && !options.incremental) {
@@ -3199,12 +4022,24 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3199
4022
  let snapshot = null;
3200
4023
  let root = base;
3201
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
+ };
3202
4032
  if (loaded !== void 0) {
3203
4033
  const scope = (listed2) => {
3204
4034
  try {
3205
4035
  const found = loadedScope(manifest, configs(), listed2);
3206
- decision.scope = "files" in found ? { files: found.files.size } : found;
3207
- 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;
3208
4043
  } catch (error) {
3209
4044
  decision.scope = { every: error.message };
3210
4045
  return null;
@@ -3219,7 +4054,8 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3219
4054
  incrementalFile,
3220
4055
  recordFile,
3221
4056
  path8.resolve(base, options.tempDirName),
3222
- path8.resolve(base, manifest.settings.strykerReportFile)
4057
+ path8.resolve(base, manifest.settings.strykerReportFile),
4058
+ ...reports
3223
4059
  ],
3224
4060
  args: comparableArgs(strykerArgs)
3225
4061
  }));
@@ -3252,14 +4088,18 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3252
4088
  force = failure;
3253
4089
  } else if (!affected) {
3254
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
+ }
3255
4095
  } else if (ci !== null) {
3256
4096
  force = ci;
3257
4097
  } else {
3258
- const read2 = readRecord({ record: stored, incremental, snapshot });
3259
- if ("refused" in read2) {
3260
- force = read2.refused;
4098
+ const read3 = readRecord({ record: stored, incremental, snapshot });
4099
+ if ("refused" in read3) {
4100
+ force = read3.refused;
3261
4101
  } else {
3262
- last = read2.record.affected;
4102
+ last = read3.record.affected;
3263
4103
  try {
3264
4104
  const inputs = affectedInputs(manifest, {
3265
4105
  root,
@@ -3271,7 +4111,7 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3271
4111
  )
3272
4112
  });
3273
4113
  plan = planAffected({
3274
- before: { files: read2.record.snapshot.files, affected: last },
4114
+ before: { files: read3.record.snapshot.files, affected: last },
3275
4115
  now: snapshot.files,
3276
4116
  subproject: inputs.subproject,
3277
4117
  config: inputs.config,
@@ -3287,7 +4127,12 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3287
4127
  plan = { full: `what a gate depends on cannot be told: ${error.message}` };
3288
4128
  }
3289
4129
  if ("full" in plan) {
3290
- force = plan.full;
4130
+ if (refuseReuse({ record: stored, incremental, snapshot }) === null) {
4131
+ decision.unchanged = plan.full;
4132
+ plan = null;
4133
+ } else {
4134
+ force = plan.full;
4135
+ }
3291
4136
  }
3292
4137
  }
3293
4138
  }
@@ -3310,7 +4155,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
3310
4155
  ]
3311
4156
  } : nothing;
3312
4157
  }
3313
- const { snapshot, root, plan, last, ci, configs, force } = decision;
4158
+ const { snapshot, root, plan, last, configs, force } = decision;
3314
4159
  const incrementalFile = decision.incrementalFile;
3315
4160
  const recordFile = decision.recordFile;
3316
4161
  const incremental = decision.incrementalHash;
@@ -3324,29 +4169,46 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
3324
4169
  const rerun = plan !== null && "rerun" in plan ? plan.rerun.map((entry) => entry.gate) : null;
3325
4170
  const everyGate = force ?? (rerun !== null && rerun.length > 0 && rerun.length === manifest.gates.length ? "every gate is affected" : null);
3326
4171
  let ranges = [];
4172
+ let timedOut = 0;
4173
+ let expected = 0;
3327
4174
  if (everyGate === null && incremental !== null && !options.force) {
3328
- 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));
3329
4176
  try {
3330
- ranges = timeoutRanges(
4177
+ ({
4178
+ ranges,
4179
+ rerun: timedOut,
4180
+ expected
4181
+ } = timeoutRanges(
3331
4182
  JSON.parse(fs8.readFileSync(incrementalFile, "utf8")),
3332
4183
  kept
3333
- );
4184
+ ));
3334
4185
  } catch {
3335
- ranges = [];
3336
4186
  }
3337
4187
  }
3338
4188
  const subset = rerun !== null && rerun.length > 0 && everyGate === null ? rerun : [];
4189
+ const ran = affected ? rerun : everyGate !== null || options.force || incremental === null ? null : [];
3339
4190
  let said = [];
3340
4191
  if (affected && !options.force) {
3341
4192
  if (force !== null) {
3342
4193
  said = describePlan({ full: force });
3343
4194
  } else if (plan !== null) {
3344
- said = describePlan(plan, 5, ranges.length);
4195
+ said = describePlan(plan, 5, timedOut, expected);
4196
+ } else if (decision.unchanged !== null) {
4197
+ said = [
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")})`,
4199
+ ...ranges.length > 0 ? [` and ${timedOut} mutant(s) that timed out in the last run are run again`] : [],
4200
+ ...expected > 0 ? [` ${describeExpected(expected)}`] : []
4201
+ ];
4202
+ }
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)}`);
3345
4211
  }
3346
- } else if (ranges.length > 0) {
3347
- said = [
3348
- `test-gates mutation: ${ranges.length} mutant(s) that timed out in the last run are run again, the other results are reused`
3349
- ];
3350
4212
  }
3351
4213
  return {
3352
4214
  // Every gate is run again: the same as a run of every mutant.
@@ -3359,8 +4221,8 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
3359
4221
  return;
3360
4222
  }
3361
4223
  const record = { format: RECORD_FORMAT, incremental: written, snapshot };
3362
- if (affected && ci === null && judged !== null) {
3363
- record.affected = affectedRecord(judged, rerun, last);
4224
+ if (affectedRefused(process.env) === null && judged !== null) {
4225
+ record.affected = affectedRecord(judged, ran, last);
3364
4226
  }
3365
4227
  try {
3366
4228
  fs8.writeFileSync(recordFile, `${JSON.stringify(record)}
@@ -3369,7 +4231,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
3369
4231
  }
3370
4232
  }
3371
4233
  };
3372
- function affectedRecord(judged, ran, previous) {
4234
+ function affectedRecord(judged, ran2, previous) {
3373
4235
  const gates = manifest.gates.map((gate) => gate.path);
3374
4236
  const dirty = new Set(
3375
4237
  [...judged.verdict.survivors, ...judged.verdict.unexpectedTimeouts].map((one) => one.file)
@@ -3377,7 +4239,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
3377
4239
  const fresh = coveringSpecs(judged.report, gates);
3378
4240
  const covering = /* @__PURE__ */ Object.create(null);
3379
4241
  for (const gate of gates) {
3380
- 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;
3381
4243
  if (specs !== null && specs !== void 0 && !dirty.has(gate)) {
3382
4244
  covering[gate] = specs;
3383
4245
  }
@@ -3399,7 +4261,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
3399
4261
  }
3400
4262
  return { tests: Object.keys(judged.report.testFiles ?? {}).sort(), gates: recorded };
3401
4263
  } catch (error) {
3402
- if (error instanceof AffectedError) {
4264
+ if (error instanceof AffectedError || !affected) {
3403
4265
  return void 0;
3404
4266
  }
3405
4267
  throw error;
@@ -3562,11 +4424,30 @@ function runDoctor(dir, io2) {
3562
4424
  ` next run with --affected: ${first.replace(/^test-gates mutation --affected: /, "")}`
3563
4425
  );
3564
4426
  rest.forEach((line) => io2.out(` ${line}`));
4427
+ } else if (affected.unchanged !== null) {
4428
+ io2.out(" next run with --affected: reuses the results of the last run (nothing changed)");
3565
4429
  } else {
3566
4430
  const why = affected.forced ? "the Stryker config sets force" : affected.force;
3567
4431
  io2.out(` next run with --affected: every mutant, ${why ?? "there is nothing to reuse"}`);
3568
4432
  }
3569
4433
  }
4434
+ let blind = null;
4435
+ if (plain2.snapshot !== null) {
4436
+ try {
4437
+ affectedInputs(manifest, {
4438
+ root: plain2.root,
4439
+ files: plain2.snapshot.files,
4440
+ configs: plain2.configs(),
4441
+ tests: [],
4442
+ covering: {}
4443
+ });
4444
+ } catch (error) {
4445
+ if (!(error instanceof AffectedError)) {
4446
+ throw error;
4447
+ }
4448
+ blind = error.message;
4449
+ }
4450
+ }
3570
4451
  const { lines, status } = formatFindings(
3571
4452
  diagnose({
3572
4453
  incremental: plain2.incremental,
@@ -3574,7 +4455,8 @@ function runDoctor(dir, io2) {
3574
4455
  failure: plain2.failure,
3575
4456
  widened: plain2.widened,
3576
4457
  scope: plain2.scope,
3577
- hooks: hooksOf(fs9.realpathSync(manifest.dir))
4458
+ hooks: hooksOf(fs9.realpathSync(manifest.dir)),
4459
+ blind
3578
4460
  })
3579
4461
  );
3580
4462
  lines.forEach((line) => io2.out(line));