@systemfsoftware/stryker-js-typescript-checker 5.0.0 → 5.0.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/index.mjs CHANGED
@@ -5,17 +5,18 @@ import * as FileSystem from "effect/FileSystem";
5
5
  import * as Layer from "effect/Layer";
6
6
  import * as Path from "effect/Path";
7
7
  import * as S from "effect/Schema";
8
- import { Cell, Wire, Workflow } from "@systemfsoftware/effect-cell-types";
9
- import { Mutant, errorToString } from "@systemfsoftware/stryker-js/Mutant";
8
+ import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
9
+ import { Mutant, errorToString, normalizeFileName } from "@systemfsoftware/stryker-js/Mutant";
10
10
  import { Predicate, Result, Schema } from "effect";
11
11
  import * as HashMap from "effect/HashMap";
12
12
  import * as Match from "effect/Match";
13
- import * as MutableHashMap from "effect/MutableHashMap";
14
13
  import * as Option from "effect/Option";
15
14
  import { API, DiagnosticCategory } from "typescript/unstable/sync";
15
+ import * as Arr from "effect/Array";
16
16
  import * as Result$1 from "effect/Result";
17
17
  import { StrykerOptionsSchema } from "@systemfsoftware/stryker-js/Schema";
18
18
  import * as Context from "effect/Context";
19
+ import * as MutableHashMap from "effect/MutableHashMap";
19
20
  import * as MutableHashSet from "effect/MutableHashSet";
20
21
  import * as Ref from "effect/Ref";
21
22
  import { SyntaxKind } from "typescript/unstable/ast";
@@ -39,25 +40,29 @@ var typescript_checker_options_default = {
39
40
  } }
40
41
  };
41
42
  //#endregion
42
- //#region src/check-mutants.workflow.ts
43
- var DiagnosticWithoutFileError = class extends S.TaggedError()("DiagnosticWithoutFileError", { text: Wire.mint(S.String) }) {};
44
- var DiagnosticInUnrelatedFileError = class extends S.TaggedError()("DiagnosticInUnrelatedFileError", {
45
- text: Wire.mint(S.String),
46
- fileName: Wire.mint(S.String)
47
- }) {};
48
- const DiagnosticSchema = Wire.wire({
49
- fileName: Wire.mint(S.optional(Wire.mint(S.String))),
50
- text: Wire.mint(S.String)
43
+ //#region src/CheckMutants.schema.ts
44
+ /** A file name the node map can be keyed by: non-empty, and naming an extension. */
45
+ const SourceFileSchema = S.NonEmptyString.pipe(S.check(S.isPattern(/\.[^./\\]+$/)));
46
+ const DiagnosticSchema = S.Struct({
47
+ fileName: S.optional(SourceFileSchema),
48
+ text: S.String
51
49
  });
52
- const TSFileNodeSchema = Wire.mint(S.suspend(() => Wire.wire({
53
- fileName: Wire.mint(S.String),
54
- parents: Wire.mint(S.Array(TSFileNodeSchema)),
55
- children: Wire.mint(S.Array(TSFileNodeSchema))
56
- })));
50
+ const TSFileNodeSchema = S.suspend(() => S.Struct({
51
+ fileName: SourceFileSchema,
52
+ parents: S.Array(TSFileNodeSchema),
53
+ children: S.Array(TSFileNodeSchema)
54
+ }));
57
55
  var CheckMutantsInput = class extends S.TaggedClass()("CheckMutantsInput", {
58
56
  mutants: S.Array(Mutant),
59
57
  diagnostics: S.Array(DiagnosticSchema),
60
- nodes: Wire.mint(S.Record(Wire.mint(S.String), TSFileNodeSchema))
58
+ nodes: S.Record(SourceFileSchema, TSFileNodeSchema)
59
+ }) {};
60
+ //#endregion
61
+ //#region src/check-mutants.workflow.ts
62
+ var DiagnosticWithoutFileError = class extends S.TaggedError()("DiagnosticWithoutFileError", { text: S.String }) {};
63
+ var DiagnosticInUnrelatedFileError = class extends S.TaggedError()("DiagnosticInUnrelatedFileError", {
64
+ text: S.String,
65
+ fileName: S.String
61
66
  }) {};
62
67
  const CheckMutantsTypeId = Symbol.for("@systemfsoftware/stryker-js-typescript-checker/CheckMutants");
63
68
  const MutantCheckStatusSchema = S.Union([S.Struct({ status: S.Literal("passed") }), S.Struct({
@@ -74,83 +79,90 @@ var RetestRequired = class extends S.TaggedClass()("RetestRequired", {
74
79
  [CheckMutantsTypeId] = CheckMutantsTypeId;
75
80
  };
76
81
  const normalizeFileName$3 = (fileName) => fileName.replace(/\\/g, "/");
77
- const getMutantsWithReferenceToChildrenOrSelf = (node, mutants, nodesChecked = []) => {
78
- if (nodesChecked.includes(node.fileName)) return [];
79
- nodesChecked.push(node.fileName);
80
- const relatedMutants = mutants.filter((m) => normalizeFileName$3(m.fileName) === node.fileName);
81
- const childResult = node.children.flatMap((c) => getMutantsWithReferenceToChildrenOrSelf(c, mutants, nodesChecked));
82
- return [...relatedMutants, ...childResult];
83
- };
84
- const classifyDiagnosticsPure = (diagnostics, mutants, nodes) => {
85
- const definitive = {};
86
- const needsRetest = {};
87
- if (diagnostics.length > 0 && mutants.length === 1) {
88
- const only = mutants[0];
89
- if (only !== void 0) {
90
- definitive[only.id] = [...diagnostics];
91
- return Result$1.succeed({
92
- definitive,
93
- needsRetest: []
94
- });
95
- }
96
- }
97
- for (const diagnostic of diagnostics) {
98
- const fileName = diagnostic.fileName;
99
- if (fileName === void 0 || fileName === "") return Result$1.fail(new DiagnosticWithoutFileError({ text: diagnostic.text }));
100
- const node = nodes[fileName];
101
- if (node === void 0) return Result$1.fail(new DiagnosticInUnrelatedFileError({
82
+ const nodeAt = (fileName, nodes) => Option.filter(Option.fromUndefinedOr(nodes[fileName]), () => Object.hasOwn(nodes, fileName));
83
+ const nodeOf = (diagnostic, nodes) => Option.match(Option.filter(Option.fromUndefinedOr(diagnostic.fileName), (fileName) => fileName !== ""), {
84
+ onNone: () => Result$1.fail(new DiagnosticWithoutFileError({ text: diagnostic.text })),
85
+ onSome: (fileName) => Option.match(nodeAt(fileName, nodes), {
86
+ onNone: () => Result$1.fail(new DiagnosticInUnrelatedFileError({
102
87
  text: diagnostic.text,
103
88
  fileName
104
- }));
105
- const related = getMutantsWithReferenceToChildrenOrSelf(node, [...mutants]);
106
- if (related.length === 0) for (const m of mutants) needsRetest[m.id] = m;
107
- else if (related.length === 1) {
108
- const only = related[0];
109
- if (only !== void 0) {
110
- const existing = definitive[only.id];
111
- if (existing !== void 0) existing.push(diagnostic);
112
- else definitive[only.id] = [diagnostic];
113
- }
114
- } else for (const m of related) needsRetest[m.id] = m;
115
- }
116
- const filteredRetest = Object.values(needsRetest).filter((m) => definitive[m.id] === void 0);
117
- return Result$1.succeed({
118
- definitive,
119
- needsRetest: filteredRetest
120
- });
121
- };
122
- const buildResult = (input) => {
123
- const mutants = input.mutants;
124
- const diagnostics = input.diagnostics;
125
- const nodes = input.nodes;
126
- if (mutants.length === 0) return Result$1.succeed(CheckFinished.make({ results: {} }));
127
- const first = mutants[0];
128
- if (first === void 0 || nodes[normalizeFileName$3(first.fileName)] === void 0) {
129
- const results = {};
130
- for (const m of mutants) results[m.id] = { status: "passed" };
131
- return Result$1.succeed(CheckFinished.make({ results }));
132
- }
133
- const classified = classifyDiagnosticsPure(diagnostics, mutants, nodes);
134
- if (Result$1.isFailure(classified)) return Result$1.fail(classified.failure);
135
- const { definitive, needsRetest } = classified.success;
136
- const retestIds = {};
137
- for (const m of needsRetest) retestIds[m.id] = true;
138
- const results = {};
139
- for (const m of mutants) {
140
- const diags = definitive[m.id];
141
- if (diags !== void 0) results[m.id] = {
89
+ })),
90
+ onSome: (node) => Result$1.succeed(node)
91
+ })
92
+ });
93
+ const walk = (node, mutants, visited) => Option.match(Option.filter(Option.some(node), (current) => !visited.includes(current.fileName)), {
94
+ onNone: () => [],
95
+ onSome: (current) => [...mutants.filter((mutant) => normalizeFileName$3(mutant.fileName) === current.fileName), ...current.children.flatMap((child) => walk(child, mutants, [...visited, current.fileName]))]
96
+ });
97
+ const emptyAccumulator = () => ({
98
+ definitive: HashMap.empty(),
99
+ needsRetest: HashMap.empty()
100
+ });
101
+ const addAll = (into, mutants) => mutants.reduce((accumulated, mutant) => HashMap.set(accumulated, mutant.id, mutant), into);
102
+ const appendDiagnostic = (into, mutantId, diagnostic) => HashMap.set(into, mutantId, [...Option.getOrElse(HashMap.get(into, mutantId), () => []), diagnostic]);
103
+ const classifyOne = (state, diagnostic, mutants, nodes) => Result$1.flatMap(nodeOf(diagnostic, nodes), (node) => {
104
+ const related = walk(node, mutants, []);
105
+ return Result$1.succeed(Option.match(Option.filter(Arr.head(related), () => related.length === 1), {
106
+ onSome: (only) => ({
107
+ definitive: appendDiagnostic(state.definitive, only.id, diagnostic),
108
+ needsRetest: state.needsRetest
109
+ }),
110
+ onNone: () => Option.match(Arr.head(related), {
111
+ onNone: () => ({
112
+ definitive: state.definitive,
113
+ needsRetest: addAll(state.needsRetest, mutants)
114
+ }),
115
+ onSome: () => ({
116
+ definitive: state.definitive,
117
+ needsRetest: addAll(state.needsRetest, related)
118
+ })
119
+ })
120
+ }));
121
+ });
122
+ const classifyDiagnostics = (diagnostics, mutants, nodes) => Option.match(Option.filter(Option.filter(Arr.head(mutants), () => mutants.length === 1), () => diagnostics.length > 0), {
123
+ onSome: (only) => Result$1.succeed({
124
+ definitive: HashMap.set(HashMap.empty(), only.id, [...diagnostics]),
125
+ needsRetest: []
126
+ }),
127
+ onNone: () => Result$1.map(diagnostics.reduce((accumulated, diagnostic) => Result$1.flatMap(accumulated, (state) => classifyOne(state, diagnostic, mutants, nodes)), Result$1.succeed(emptyAccumulator())), (state) => ({
128
+ definitive: state.definitive,
129
+ needsRetest: HashMap.toValues(state.needsRetest).filter((mutant) => !HashMap.has(state.definitive, mutant.id))
130
+ }))
131
+ });
132
+ const passedResults = (mutants) => mutants.map((mutant) => [mutant.id, { status: "passed" }]);
133
+ const checkResults = (mutants, classification) => {
134
+ const retestIds = classification.needsRetest.reduce((accumulated, mutant) => HashMap.set(accumulated, mutant.id, true), HashMap.empty());
135
+ return mutants.flatMap((mutant) => Option.match(HashMap.get(classification.definitive, mutant.id), {
136
+ onSome: (diagnostics) => [[mutant.id, {
142
137
  status: "compileError",
143
- reason: diags.map((d) => d.text).join("\n")
144
- };
145
- else if (retestIds[m.id] !== true) results[m.id] = { status: "passed" };
146
- }
147
- if (needsRetest.length === 0) return Result$1.succeed(CheckFinished.make({ results }));
148
- return Result$1.succeed(RetestRequired.make({
149
- results,
150
- needsRetest: [...needsRetest]
138
+ reason: diagnostics.map((entry) => entry.text).join("\n")
139
+ }]],
140
+ onNone: () => Option.match(Option.filter(Option.some({ status: "passed" }), () => !HashMap.has(retestIds, mutant.id)), {
141
+ onSome: (status) => [[mutant.id, status]],
142
+ onNone: () => []
143
+ })
151
144
  }));
152
145
  };
153
- const checkMutants = Workflow.make(CheckMutantsInput, (input) => buildResult(input));
146
+ const verdictOf = (mutants, classification) => {
147
+ const results = Object.fromEntries(checkResults(mutants, classification));
148
+ return Option.match(Arr.head(classification.needsRetest), {
149
+ onNone: () => CheckFinished.make({ results }),
150
+ onSome: () => RetestRequired.make({
151
+ results,
152
+ needsRetest: [...classification.needsRetest]
153
+ })
154
+ });
155
+ };
156
+ const classify = (input) => Result$1.map(classifyDiagnostics(input.diagnostics, input.mutants, input.nodes), (classification) => verdictOf(input.mutants, classification));
157
+ const withoutDisambiguation = (input) => Option.match(Arr.head(input.mutants), {
158
+ onNone: () => Option.some(CheckFinished.make({ results: {} })),
159
+ onSome: (first) => Option.map(Option.filter(Option.some(first), () => !Object.hasOwn(input.nodes, normalizeFileName$3(first.fileName))), () => CheckFinished.make({ results: Object.fromEntries(passedResults(input.mutants)) }))
160
+ });
161
+ const verdict = (input) => Option.match(withoutDisambiguation(input), {
162
+ onNone: () => classify(input),
163
+ onSome: (decision) => Result$1.succeed(decision)
164
+ });
165
+ const checkMutants = Workflow.make(CheckMutantsInput, verdict);
154
166
  //#endregion
155
167
  //#region src/Checker.schema.ts
156
168
  /**
@@ -159,6 +171,7 @@ const checkMutants = Workflow.make(CheckMutantsInput, (input) => buildResult(inp
159
171
  * Houses the wire types and error variants shared by the capability and its
160
172
  * workflow. Decoded at the checker boundary; no I/O.
161
173
  */
174
+ const TypescriptCheckerOptionsSchema = S.Struct({ typescriptChecker: S.optional(S.Struct({ prioritizePerformanceOverAccuracy: S.optional(S.Boolean) })) });
162
175
  var CheckMutantsCommand = class extends S.TaggedClass()("CheckMutantsCommand", { mutants: S.Array(Mutant) }) {};
163
176
  /**
164
177
  * Every way the TypeScript compiler can fail while serving a check.
@@ -175,12 +188,7 @@ var CompilerFailed = class extends S.TaggedError()("CompilerFailed", {
175
188
  subject: S.optional(S.String)
176
189
  }) {
177
190
  get message() {
178
- switch (this.reason) {
179
- case "not-initialized": return "The TypeScript compiler was used before it was initialized";
180
- case "no-projects": return `No projects were found for ${this.subject ?? "the tsconfig"}`;
181
- case "unknown-file-node": return `The file graph has no node for '${this.subject ?? "a file"}', which should not happen`;
182
- case "file-not-in-project": return `'${this.subject ?? "a file"}' is part of your TypeScript project but could not be found on disk`;
183
- }
191
+ return Match.value(this.reason).pipe(Match.when("not-initialized", () => "The TypeScript compiler was used before it was initialized"), Match.when("no-projects", () => `No projects were found for ${this.subject ?? "the tsconfig"}`), Match.when("unknown-file-node", () => `The file graph has no node for '${this.subject ?? "a file"}', which should not happen`), Match.when("file-not-in-project", () => `'${this.subject ?? "a file"}' is part of your TypeScript project but could not be found on disk`), Match.exhaustive);
184
192
  }
185
193
  };
186
194
  //#endregion
@@ -468,6 +476,10 @@ const LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES = Object.freeze({
468
476
  declaration: true,
469
477
  composite: true
470
478
  });
479
+ const reasonOfThrown = (error) => Match.value(error).pipe(Match.when(Predicate.isError, (thrown) => thrown.message), Match.when(Predicate.isString, (thrown) => thrown), Match.orElse((thrown) => Option.match(Option.filter(Option.fromUndefinedOr(JSON.stringify(thrown)), (text) => text.length > 0), {
480
+ onNone: () => "a non-Error value was thrown",
481
+ onSome: (text) => text
482
+ })));
471
483
  /**
472
484
  * Parses the raw text of a tsconfig file into a typed config, rejecting shapes this package cannot consume.
473
485
  * @param fileName The tsconfig file name, used for error reporting
@@ -481,17 +493,9 @@ function parseTsConfig(fileName, jsonText) {
481
493
  reason: error.message
482
494
  }));
483
495
  } catch (error) {
484
- let reason;
485
- if (error instanceof Error) reason = error.message;
486
- else if (typeof error === "string") reason = error;
487
- else {
488
- const stringified = JSON.stringify(error);
489
- if (stringified.length === 0) reason = "a non-Error value was thrown";
490
- else reason = stringified;
491
- }
492
496
  return Result.fail(new TsConfigParseError({
493
497
  file: fileName,
494
- reason
498
+ reason: reasonOfThrown(error)
495
499
  }));
496
500
  }
497
501
  }
@@ -503,36 +507,39 @@ const determineBuildModeEnabled = (tsconfigFileName, fsService) => Effect.gen(fu
503
507
  onSuccess: (config) => config.references !== void 0
504
508
  });
505
509
  });
506
- /**
507
- * Overrides compiler options to speed up compilation and disable code quality
508
- * checks irrelevant during mutation testing.
509
- */
510
- function overrideOptions(config, useBuildMode) {
511
- let extraOptions;
512
- if (useBuildMode) extraOptions = LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES;
513
- else extraOptions = NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT;
514
- const compilerOptions = {
515
- ...config.compilerOptions,
516
- ...COMPILER_OPTIONS_OVERRIDES,
517
- ...extraOptions
518
- };
519
- if (!useBuildMode && compilerOptions["declarationDir"] !== void 0 && compilerOptions["declarationDir"] !== null) delete compilerOptions["declarationDir"];
520
- if (useBuildMode) {
521
- delete compilerOptions["inlineSourceMap"];
522
- delete compilerOptions["inlineSources"];
523
- delete compilerOptions["mapRoute"];
524
- delete compilerOptions["sourceRoot"];
525
- delete compilerOptions["outFile"];
526
- }
527
- if (useBuildMode) return JSON.stringify({
510
+ const withCompilerOverrides = (config, extraOptions) => ({
511
+ ...config.compilerOptions,
512
+ ...COMPILER_OPTIONS_OVERRIDES,
513
+ ...extraOptions
514
+ });
515
+ const projectReferencesJson = (config) => {
516
+ const compilerOptions = withCompilerOverrides(config, LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES);
517
+ delete compilerOptions["inlineSourceMap"];
518
+ delete compilerOptions["inlineSources"];
519
+ delete compilerOptions["mapRoute"];
520
+ delete compilerOptions["sourceRoot"];
521
+ delete compilerOptions["outFile"];
522
+ return JSON.stringify({
528
523
  ...config,
529
524
  compilerOptions
530
525
  });
526
+ };
527
+ const singleProjectJson = (config) => {
528
+ const compilerOptions = withCompilerOverrides(config, NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT);
529
+ if (compilerOptions["declarationDir"] !== null) delete compilerOptions["declarationDir"];
531
530
  const { references: _references, ...withoutReferences } = config;
532
531
  return JSON.stringify({
533
532
  ...withoutReferences,
534
533
  compilerOptions
535
534
  });
535
+ };
536
+ /**
537
+ * Overrides compiler options to speed up compilation and disable code quality
538
+ * checks irrelevant during mutation testing.
539
+ */
540
+ function overrideOptions(config, useBuildMode) {
541
+ if (useBuildMode) return projectReferencesJson(config);
542
+ return singleProjectJson(config);
536
543
  }
537
544
  /**
538
545
  * Retrieves the referenced config files based on parsed configuration.
@@ -548,35 +555,71 @@ function retrieveReferencedProjects(config, fromDirName, pathService) {
548
555
  //#region src/Compiler.ts
549
556
  const normalizeFileName$1 = (fileName) => fileName.replace(/\\/g, "/");
550
557
  const findSourceMapRegex = /\/\/# sourceMappingURL=(.+)$/m;
558
+ /** A specifier that resolves relative to the importing file. */
559
+ const relativeSpecifierPattern = /^\.\.?\//;
560
+ /** A file that never belongs to the dependency graph: declarations and dependencies. */
561
+ const ignoredGraphFileNamePattern = /\.d\.ts$|node_modules/;
551
562
  function getSourceMappingURL(content) {
552
563
  return findSourceMapRegex.exec(content)?.[1];
553
564
  }
554
565
  let cachedTSVersion;
555
- const getTSVersion = (fsService, pathService) => Effect.gen(function* () {
556
- if (cachedTSVersion !== void 0) return cachedTSVersion;
566
+ const isString = (value) => typeof value === "string";
567
+ const isNonEmptyString = (value) => value !== void 0 && value !== "";
568
+ const versionFieldOf = (raw) => {
569
+ if (Predicate.hasProperty(raw, "version")) return Option.some(raw.version);
570
+ return Option.none();
571
+ };
572
+ const readTypescriptPackageVersion = (fsService, pathService) => Effect.gen(function* () {
557
573
  const urlString = import.meta.resolve("typescript/package.json");
558
574
  const pkgPath = yield* pathService.fromFileUrl(new URL(urlString));
559
575
  const text = yield* fsService.readFileString(pkgPath);
560
576
  const raw = JSON.parse(text);
561
- let version = "";
562
- if (Predicate.hasProperty(raw, "version") && typeof raw.version === "string") version = raw.version;
577
+ return Option.getOrElse(Option.flatMap(versionFieldOf(raw), (version) => Option.liftPredicate(version, isString)), () => "");
578
+ });
579
+ const getTSVersion = (fsService, pathService) => Effect.gen(function* () {
580
+ if (cachedTSVersion !== void 0) return cachedTSVersion;
581
+ const version = yield* readTypescriptPackageVersion(fsService, pathService);
563
582
  cachedTSVersion = version;
564
583
  return version;
565
584
  });
585
+ const minimumSupportedTypeScriptVersion = {
586
+ major: 7,
587
+ minor: 0,
588
+ patch: 0
589
+ };
590
+ const versionComponent = (parts, index) => {
591
+ const part = parts[index];
592
+ if (part === void 0) return 0;
593
+ return Number.parseInt(part, 10);
594
+ };
595
+ /** Drops any pre-release (`-`) or build (`+`) suffix, keeping the numeric base. */
596
+ const parseTypeScriptVersion = (version) => {
597
+ const parts = version.replace(/[-+][\s\S]*$/, "").split(".");
598
+ return {
599
+ major: versionComponent(parts, 0),
600
+ minor: versionComponent(parts, 1),
601
+ patch: versionComponent(parts, 2)
602
+ };
603
+ };
604
+ const compareVersionNumbers = (left, right) => {
605
+ return [
606
+ left.major - right.major,
607
+ left.minor - right.minor,
608
+ left.patch - right.patch
609
+ ].find((difference) => difference !== 0) ?? 0;
610
+ };
566
611
  /**
567
612
  * Whether a TypeScript version satisfies `>=7.0.0`. Pre-release suffixes are
568
613
  * stripped so `7.0.0-beta` compares as `7.0.0`.
569
614
  */
570
615
  function isSupportedTypescriptVersion(version) {
571
- const dashBase = version.split("-")[0] ?? version;
572
- const parts = (dashBase.split("+")[0] ?? dashBase).split(".").map((p) => Number.parseInt(p, 10));
573
- const major = parts[0] ?? 0;
574
- const minor = parts[1] ?? 0;
575
- const patch = parts[2] ?? 0;
576
- if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch)) return false;
577
- if (major !== 7) return major > 7;
578
- if (minor !== 0) return minor > 0;
579
- return patch >= 0;
616
+ const parsed = parseTypeScriptVersion(version);
617
+ if (![
618
+ parsed.major,
619
+ parsed.minor,
620
+ parsed.patch
621
+ ].every((part) => !Number.isNaN(part))) return false;
622
+ return compareVersionNumbers(parsed, minimumSupportedTypeScriptVersion) >= 0;
580
623
  }
581
624
  const guardTSVersion = (fsService, pathService) => Effect.gen(function* () {
582
625
  const version = yield* getTSVersion(fsService, pathService);
@@ -617,13 +660,10 @@ function resetScriptFile(file) {
617
660
  function getOffset(file, pos) {
618
661
  const lines = file.originalContent.split("\n");
619
662
  const lineCount = Math.min(pos.line, lines.length);
620
- let offset = 0;
621
- for (let i = 0; i < lineCount; i++) {
622
- const line = lines[i];
623
- if (line === void 0) break;
624
- offset += line.length + 1;
625
- }
626
- offset += pos.column;
663
+ let offset = pos.column;
664
+ lines.forEach((line, index) => {
665
+ if (index < lineCount) offset += line.length + 1;
666
+ });
627
667
  return offset;
628
668
  }
629
669
  const makeEmptyFilesMap = () => MutableHashMap.empty();
@@ -635,67 +675,63 @@ const setInPlace = (map, key, value) => {
635
675
  const makeHybridFileSystem = (fsService) => Effect.gen(function* () {
636
676
  const filesRef = yield* Ref.make(makeEmptyFilesMap());
637
677
  const overridesRef = yield* Ref.make(makeEmptyOverridesMap());
638
- const fileNameIsBuildInfo = (fileName) => fileName.endsWith(".tsbuildinfo");
678
+ const memoryContent = (file) => {
679
+ if (file === void 0) return null;
680
+ return file.content;
681
+ };
682
+ const readFromSources = (files, overrides, fileName) => {
683
+ const override = MutableHashMap.get(overrides, fileName);
684
+ if (Option.isSome(override)) return override.value;
685
+ return Option.match(MutableHashMap.get(files, fileName), {
686
+ onNone: () => void 0,
687
+ onSome: memoryContent
688
+ });
689
+ };
690
+ const existsInSources = (files, overrides, fileName) => {
691
+ const override = MutableHashMap.get(overrides, fileName);
692
+ if (Option.isSome(override)) return true;
693
+ return Option.match(MutableHashMap.get(files, fileName), {
694
+ onNone: () => void 0,
695
+ onSome: (file) => file !== void 0
696
+ });
697
+ };
639
698
  const fileSystem = {
640
699
  readFile: (fileName) => {
641
700
  const normalized = normalizeFileName$1(fileName);
642
- if (fileNameIsBuildInfo(normalized)) return null;
643
- const overrideOpt = MutableHashMap.get(overridesRef.ref.current, normalized);
644
- if (Option.isSome(overrideOpt)) return overrideOpt.value;
645
- const files = filesRef.ref.current;
646
- if (MutableHashMap.has(files, normalized)) {
647
- const fileOpt = MutableHashMap.get(files, normalized);
648
- if (Option.isSome(fileOpt)) {
649
- const file = fileOpt.value;
650
- if (file !== void 0) return file.content;
651
- return null;
652
- }
653
- }
701
+ if (normalized.endsWith(".tsbuildinfo")) return null;
702
+ return readFromSources(filesRef.ref.current, overridesRef.ref.current, normalized);
654
703
  },
655
704
  fileExists: (fileName) => {
656
705
  const normalized = normalizeFileName$1(fileName);
657
- if (fileNameIsBuildInfo(normalized)) return false;
658
- if (MutableHashMap.has(overridesRef.ref.current, normalized)) return true;
659
- const files = filesRef.ref.current;
660
- if (MutableHashMap.has(files, normalized)) {
661
- const opt = MutableHashMap.get(files, normalized);
662
- if (Option.isSome(opt)) return opt.value !== void 0;
663
- return false;
664
- }
706
+ if (normalized.endsWith(".tsbuildinfo")) return false;
707
+ return existsInSources(filesRef.ref.current, overridesRef.ref.current, normalized);
665
708
  },
666
709
  directoryExists: () => void 0,
667
710
  getAccessibleEntries: () => void 0,
668
711
  realpath: () => void 0
669
712
  };
713
+ const readFileFromDisk = (fileName) => Effect.gen(function* () {
714
+ const content = yield* fsService.readFileString(fileName).pipe(Effect.orElseSucceed(() => void 0));
715
+ const file = Option.getOrUndefined(Option.map(Option.fromUndefinedOr(content), (text) => makeScriptFile(text, fileName)));
716
+ yield* Ref.update(filesRef, (m) => setInPlace(m, fileName, file));
717
+ return file;
718
+ });
670
719
  const getFile = (fileName) => Effect.gen(function* () {
671
720
  const normalized = normalizeFileName$1(fileName);
672
721
  const files = yield* Ref.get(filesRef);
673
- if (MutableHashMap.has(files, normalized)) {
674
- const opt = MutableHashMap.get(files, normalized);
675
- if (Option.isSome(opt)) return opt.value;
676
- }
677
- const content = yield* fsService.readFileString(normalized).pipe(Effect.orElseSucceed(() => void 0));
678
- if (content === void 0) {
679
- yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, void 0));
680
- return;
681
- }
682
- const file = makeScriptFile(content, normalized);
683
- yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, file));
684
- return file;
722
+ const cached = MutableHashMap.get(files, normalized);
723
+ if (Option.isSome(cached)) return cached.value;
724
+ return yield* readFileFromDisk(normalized);
685
725
  });
726
+ const fileForWrite = (existing, data, fileName) => {
727
+ if (existing === void 0) return makeScriptFile(data, fileName);
728
+ return withContent(existing, data);
729
+ };
686
730
  const writeFile = (fileName, data) => Effect.gen(function* () {
687
731
  const normalized = normalizeFileName$1(fileName);
688
732
  const files = yield* Ref.get(filesRef);
689
- const existingOpt = MutableHashMap.get(files, normalized);
690
- let existing = void 0;
691
- if (Option.isSome(existingOpt)) existing = existingOpt.value;
692
- if (existing !== void 0) {
693
- const next = withContent(existing, data);
694
- yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, next));
695
- } else {
696
- const file = makeScriptFile(data, normalized);
697
- yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, file));
698
- }
733
+ const existing = Option.getOrUndefined(MutableHashMap.get(files, normalized));
734
+ yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, fileForWrite(existing, data, normalized)));
699
735
  });
700
736
  const mutateFile = (fileName, mutant) => Effect.gen(function* () {
701
737
  const file = yield* getFile(fileName);
@@ -707,20 +743,13 @@ const makeHybridFileSystem = (fsService) => Effect.gen(function* () {
707
743
  const resetFile = (fileName) => Effect.gen(function* () {
708
744
  const normalized = normalizeFileName$1(fileName);
709
745
  const files = yield* Ref.get(filesRef);
710
- const opt = MutableHashMap.get(files, normalized);
711
- let file = void 0;
712
- if (Option.isSome(opt)) file = opt.value;
713
- if (file !== void 0) {
714
- const next = resetScriptFile(file);
715
- yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, next));
716
- }
746
+ const file = Option.getOrUndefined(MutableHashMap.get(files, normalized));
747
+ if (file === void 0) return;
748
+ yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, resetScriptFile(file)));
717
749
  });
718
750
  const existsInMemory = (fileName) => Effect.gen(function* () {
719
751
  const files = yield* Ref.get(filesRef);
720
- const opt = MutableHashMap.get(files, normalizeFileName$1(fileName));
721
- let file = void 0;
722
- if (Option.isSome(opt)) file = opt.value;
723
- return file !== void 0;
752
+ return Option.getOrUndefined(MutableHashMap.get(files, normalizeFileName$1(fileName))) !== void 0;
724
753
  });
725
754
  const setTsConfigOverrides = (overrides) => Ref.set(overridesRef, overrides);
726
755
  return {
@@ -742,42 +771,50 @@ function makeTSFileNode(fileName) {
742
771
  }
743
772
  function getAllParentReferencesIncludingSelf(node, allParentReferences = MutableHashSet.empty()) {
744
773
  MutableHashSet.add(allParentReferences, node);
745
- for (const parent of node.parents) if (!MutableHashSet.has(allParentReferences, parent)) getAllParentReferencesIncludingSelf(parent, allParentReferences);
774
+ node.parents.forEach((parent) => collectParentReference(parent, allParentReferences));
746
775
  return allParentReferences;
747
776
  }
748
- function createGroups(mutants, nodes) {
749
- const groups = [];
750
- const mutantsToGroup = MutableHashSet.fromIterable(mutants);
751
- while (MutableHashSet.size(mutantsToGroup) > 0) {
752
- const group = [];
753
- const groupNodes = MutableHashSet.empty();
754
- const nodesToIgnore = MutableHashSet.empty();
755
- for (const currentMutant of mutantsToGroup) {
756
- const currentNode = findNode(currentMutant.fileName, nodes);
757
- if (!MutableHashSet.has(nodesToIgnore, currentNode) && !parentsHaveOverlapWith(currentNode, groupNodes)) {
758
- group.push(currentMutant.id);
759
- MutableHashSet.add(groupNodes, currentNode);
760
- MutableHashSet.remove(mutantsToGroup, currentMutant);
761
- addRangeOfNodesToSet(nodesToIgnore, getAllParentReferencesIncludingSelf(currentNode));
762
- }
763
- }
764
- groups.push(group);
765
- }
766
- return groups;
777
+ function collectParentReference(parent, allParentReferences) {
778
+ if (MutableHashSet.has(allParentReferences, parent)) return;
779
+ getAllParentReferencesIncludingSelf(parent, allParentReferences);
767
780
  }
768
781
  function addRangeOfNodesToSet(nodes, nodesToAdd) {
769
- for (const parent of nodesToAdd) MutableHashSet.add(nodes, parent);
782
+ for (const node of nodesToAdd) MutableHashSet.add(nodes, node);
770
783
  }
771
784
  function findNode(fileName, nodes) {
772
- const nodeOption = MutableHashMap.get(nodes, normalizeFileName$1(fileName));
773
- if (Option.isSome(nodeOption)) return nodeOption.value;
774
- const fallbackOption = MutableHashMap.get(nodes, fileName);
775
- if (Option.isSome(fallbackOption)) return fallbackOption.value;
776
- throw new Error(`Node not in graph: ${fileName}`);
785
+ const node = Option.firstSomeOf([MutableHashMap.get(nodes, normalizeFileName$1(fileName)), MutableHashMap.get(nodes, fileName)]);
786
+ if (Option.isNone(node)) throw new Error(`Node not in graph: ${fileName}`);
787
+ return node.value;
777
788
  }
778
789
  function parentsHaveOverlapWith(currentNode, groupNodes) {
779
- for (const parentNode of getAllParentReferencesIncludingSelf(currentNode)) if (MutableHashSet.has(groupNodes, parentNode)) return true;
780
- return false;
790
+ return Array.from(getAllParentReferencesIncludingSelf(currentNode)).some((parentNode) => MutableHashSet.has(groupNodes, parentNode));
791
+ }
792
+ function mutantCanJoinGroup(currentNode, group) {
793
+ if (MutableHashSet.has(group.ignoredNodes, currentNode)) return false;
794
+ return !parentsHaveOverlapWith(currentNode, group.nodes);
795
+ }
796
+ function addMutantToGroup(currentMutant, mutantsToGroup, group, nodes) {
797
+ const currentNode = findNode(currentMutant.fileName, nodes);
798
+ if (!mutantCanJoinGroup(currentNode, group)) return;
799
+ group.mutantIds.push(currentMutant.id);
800
+ MutableHashSet.add(group.nodes, currentNode);
801
+ MutableHashSet.remove(mutantsToGroup, currentMutant);
802
+ addRangeOfNodesToSet(group.ignoredNodes, getAllParentReferencesIncludingSelf(currentNode));
803
+ }
804
+ function takeGroup(mutantsToGroup, nodes) {
805
+ const group = {
806
+ mutantIds: [],
807
+ nodes: MutableHashSet.empty(),
808
+ ignoredNodes: MutableHashSet.empty()
809
+ };
810
+ for (const currentMutant of mutantsToGroup) addMutantToGroup(currentMutant, mutantsToGroup, group, nodes);
811
+ return group.mutantIds;
812
+ }
813
+ function createGroups(mutants, nodes) {
814
+ const groups = [];
815
+ const mutantsToGroup = MutableHashSet.fromIterable(mutants);
816
+ while (MutableHashSet.size(mutantsToGroup) > 0) groups.push(takeGroup(mutantsToGroup, nodes));
817
+ return groups;
781
818
  }
782
819
  var TypeScriptCompiler = class extends Context.Service()("@systemfsoftware/stryker-js-typescript-checker/TypeScriptCompiler") {};
783
820
  const makeDummy = Effect.gen(function* () {
@@ -828,63 +865,91 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
828
865
  tsconfigFile: rawTsconfigFile
829
866
  };
830
867
  const stateRef = Ref.makeUnsafe(initialState);
831
- const getProgramsEffect = () => Effect.gen(function* () {
832
- const s = yield* Ref.get(stateRef);
833
- if (!s.snapshot) return yield* new CompilerFailed({ reason: "not-initialized" });
834
- const projects = s.snapshot.getProjects();
835
- if (projects.length === 0) return yield* new CompilerFailed({
868
+ const snapshotOf = (state) => {
869
+ if (state.snapshot === void 0) return Effect.fail(new CompilerFailed({ reason: "not-initialized" }));
870
+ return Effect.succeed(state.snapshot);
871
+ };
872
+ const programsOf = (snapshot, tsconfigFile) => {
873
+ const projects = snapshot.getProjects();
874
+ if (projects.length === 0) return Effect.fail(new CompilerFailed({
836
875
  reason: "no-projects",
837
- subject: s.tsconfigFile
838
- });
839
- return projects.map((project) => project.program);
876
+ subject: tsconfigFile
877
+ }));
878
+ return Effect.succeed(projects.map((project) => project.program));
879
+ };
880
+ const getProgramsEffect = () => Effect.gen(function* () {
881
+ const state = yield* Ref.get(stateRef);
882
+ const snapshot = yield* snapshotOf(state);
883
+ return yield* programsOf(snapshot, state.tsconfigFile);
840
884
  });
841
885
  const guardTSConfigFileExistsEffect = Effect.gen(function* () {
842
886
  const s = yield* Ref.get(stateRef);
843
887
  yield* fsService.readFileString(s.tsconfigFile).pipe(Effect.mapError(() => new TsConfigNotFoundError({ file: s.tsconfigFile })));
844
888
  });
845
- const collectAllTSConfigFiles = (buildModeEnabled) => Effect.gen(function* () {
846
- const s = yield* Ref.get(stateRef);
847
- const tsConfigOverrides = MutableHashMap.empty();
848
- const toProcess = [s.tsconfigFile];
849
- const processed = MutableHashSet.empty();
850
- while (toProcess.length > 0) {
851
- const current = toProcess.pop();
852
- if (current === void 0 || current === "" || MutableHashSet.has(processed, current)) continue;
853
- MutableHashSet.add(processed, current);
854
- const content = yield* fsService.readFileString(current);
855
- const parsed = parseTsConfig(current, content);
856
- if (Result.isFailure(parsed)) {
857
- MutableHashMap.set(tsConfigOverrides, current, content);
858
- continue;
859
- }
860
- MutableHashMap.set(tsConfigOverrides, current, overrideOptions(parsed.success, buildModeEnabled));
861
- for (const referenced of retrieveReferencedProjects(parsed.success, pathService.dirname(current), pathService)) {
862
- const normalized = normalizeFileName$1(referenced);
863
- MutableHashSet.add(s.allTSConfigFiles, normalized);
864
- toProcess.push(referenced);
865
- }
889
+ const isBlankTsConfigPath = (current) => current === void 0 || current === "";
890
+ const isUnprocessedTsConfigPath = (current, processed) => !isBlankTsConfigPath(current) && !MutableHashSet.has(processed, current);
891
+ const recordParsedTsConfig = (current, config, traversal) => {
892
+ MutableHashMap.set(traversal.overrides, current, overrideOptions(config, traversal.buildModeEnabled));
893
+ for (const referenced of retrieveReferencedProjects(config, pathService.dirname(current), pathService)) {
894
+ MutableHashSet.add(traversal.allTsConfigFiles, normalizeFileName$1(referenced));
895
+ traversal.pending.push(referenced);
896
+ }
897
+ };
898
+ const recordTsConfig = (current, content, parsed, traversal) => {
899
+ if (Result.isFailure(parsed)) {
900
+ MutableHashMap.set(traversal.overrides, current, content);
901
+ return;
866
902
  }
867
- yield* fs.setTsConfigOverrides(tsConfigOverrides);
903
+ recordParsedTsConfig(current, parsed.success, traversal);
904
+ };
905
+ const processNextTsConfig = (traversal) => Effect.gen(function* () {
906
+ const current = traversal.pending.pop();
907
+ if (!isUnprocessedTsConfigPath(current, traversal.processed)) return;
908
+ MutableHashSet.add(traversal.processed, current);
909
+ const content = yield* fsService.readFileString(current);
910
+ recordTsConfig(current, content, parseTsConfig(current, content), traversal);
911
+ });
912
+ const collectAllTSConfigFiles = (buildModeEnabled) => Effect.gen(function* () {
913
+ const state = yield* Ref.get(stateRef);
914
+ const traversal = {
915
+ overrides: MutableHashMap.empty(),
916
+ pending: [state.tsconfigFile],
917
+ processed: MutableHashSet.empty(),
918
+ allTsConfigFiles: state.allTSConfigFiles,
919
+ buildModeEnabled
920
+ };
921
+ while (traversal.pending.length > 0) yield* processNextTsConfig(traversal);
922
+ yield* fs.setTsConfigOverrides(traversal.overrides);
868
923
  yield* Ref.update(stateRef, (prev) => ({
869
924
  ...prev,
870
- allTSConfigFiles: MutableHashSet.fromIterable(s.allTSConfigFiles)
925
+ allTSConfigFiles: MutableHashSet.fromIterable(traversal.allTsConfigFiles)
871
926
  }));
872
927
  });
873
- const extractImports = (sourceFile) => {
874
- const result = [];
875
- for (const statement of sourceFile.statements) if (statement.kind === SyntaxKind.ImportDeclaration) {
876
- let spec;
877
- statement.forEachChild((child) => {
878
- if (child.kind === SyntaxKind.StringLiteral) spec = child;
879
- });
880
- if (spec) result.push(spec.getText(sourceFile));
881
- } else if (statement.kind === SyntaxKind.ImportEqualsDeclaration) statement.forEachChild((child) => {
928
+ const importDeclarationSpecifierOf = (statement, sourceFile) => {
929
+ if (statement.kind !== SyntaxKind.ImportDeclaration) return Option.none();
930
+ let specifier;
931
+ statement.forEachChild((child) => {
932
+ if (child.kind === SyntaxKind.StringLiteral) specifier = child;
933
+ });
934
+ return Option.map(Option.fromUndefinedOr(specifier), (found) => found.getText(sourceFile));
935
+ };
936
+ const collectImportEqualsSpecifiers = (statement, sourceFile, into) => {
937
+ if (statement.kind !== SyntaxKind.ImportEqualsDeclaration) return;
938
+ statement.forEachChild((child) => {
882
939
  if (child.kind === SyntaxKind.ExternalModuleReference) child.forEachChild((refChild) => {
883
- if (refChild.kind === SyntaxKind.StringLiteral) result.push(refChild.getText(sourceFile));
940
+ if (refChild.kind === SyntaxKind.StringLiteral) into.push(refChild.getText(sourceFile));
884
941
  });
885
942
  });
886
- for (const ref of sourceFile.referencedFiles) result.push(ref.fileName);
887
- for (const ref of sourceFile.typeReferenceDirectives) result.push(ref.fileName);
943
+ };
944
+ const extractImports = (sourceFile) => {
945
+ const result = [];
946
+ sourceFile.statements.forEach((statement) => {
947
+ const specifier = importDeclarationSpecifierOf(statement, sourceFile);
948
+ if (Option.isSome(specifier)) result.push(specifier.value);
949
+ collectImportEqualsSpecifiers(statement, sourceFile, result);
950
+ });
951
+ sourceFile.referencedFiles.forEach((ref) => result.push(ref.fileName));
952
+ sourceFile.typeReferenceDirectives.forEach((ref) => result.push(ref.fileName));
888
953
  return result;
889
954
  };
890
955
  const getResolutionCandidates = (resolved, pathService) => {
@@ -922,127 +987,160 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
922
987
  };
923
988
  const resolveModuleSpecifier = (sourceFileName, specifier, sourceFiles, pathService) => {
924
989
  const cleaned = specifier.replace(/^['"]|['"]$/g, "");
925
- if (!cleaned.startsWith("./") && !cleaned.startsWith("../")) return;
990
+ if (!relativeSpecifierPattern.test(cleaned)) return;
926
991
  const baseDir = pathService.dirname(sourceFileName);
927
992
  const resolved = normalizeFileName$1(pathService.resolve(baseDir, cleaned));
928
- const candidates = getResolutionCandidates(resolved, pathService);
929
- for (const candidate of candidates) if (MutableHashMap.has(sourceFiles, candidate)) return candidate;
993
+ return getResolutionCandidates(resolved, pathService).find((candidate) => MutableHashMap.has(sourceFiles, candidate));
994
+ };
995
+ const readFileText = (fileName) => Option.liftPredicate(fs.fileSystem.readFile?.(fileName), isString);
996
+ const sourcesFieldOf = (rawMap) => {
997
+ if (!Predicate.hasProperty(rawMap, "sources")) return Option.none();
998
+ return Option.liftPredicate(rawMap.sources, Array.isArray);
999
+ };
1000
+ const onlySourceOf = (sources) => {
1001
+ const names = sources.filter(isString);
1002
+ if (names.length !== 1) return Option.none();
1003
+ return Option.fromUndefinedOr(names[0]);
1004
+ };
1005
+ const sourcePathFromMap = (declarationFileName, reference, pathService) => {
1006
+ const sourceMapFileName = normalizeFileName$1(pathService.resolve(pathService.dirname(declarationFileName), reference));
1007
+ return Option.flatMap(Option.flatMap(readFileText(sourceMapFileName), (content) => Option.flatMap(sourcesFieldOf(JSON.parse(content)), onlySourceOf)), (source) => Option.some(normalizeFileName$1(pathService.resolve(pathService.dirname(sourceMapFileName), source))));
930
1008
  };
1009
+ const sourceMappedFileName = (declarationFileName, pathService) => Option.flatMap(Option.flatMap(readFileText(declarationFileName), (content) => Option.liftPredicate(getSourceMappingURL(content), isNonEmptyString)), (reference) => sourcePathFromMap(declarationFileName, reference, pathService));
931
1010
  const resolveTSInputFile = (dependencyFileName, pathService) => {
932
1011
  if (!dependencyFileName.endsWith(".d.ts")) return dependencyFileName;
933
- const content = fs.fileSystem.readFile?.(dependencyFileName);
934
- if (typeof content !== "string") return dependencyFileName;
935
- const sourceMappingURL = getSourceMappingURL(content);
936
- if (sourceMappingURL === void 0 || sourceMappingURL === "") return dependencyFileName;
937
- const sourceMapFileName = normalizeFileName$1(pathService.resolve(pathService.dirname(dependencyFileName), sourceMappingURL));
938
- const sourceMapContent = fs.fileSystem.readFile?.(sourceMapFileName);
939
- if (typeof sourceMapContent !== "string") return dependencyFileName;
940
- const rawMap = JSON.parse(sourceMapContent);
941
- let sources;
942
- if (Predicate.hasProperty(rawMap, "sources") && Array.isArray(rawMap.sources)) sources = rawMap.sources.filter((s) => typeof s === "string");
943
- if (sources?.length === 1) {
944
- const sourcePath = sources[0];
945
- if (sourcePath === void 0) return dependencyFileName;
946
- return normalizeFileName$1(pathService.resolve(pathService.dirname(sourceMapFileName), sourcePath));
947
- }
948
- return dependencyFileName;
1012
+ return Option.getOrElse(sourceMappedFileName(dependencyFileName, pathService), () => dependencyFileName);
1013
+ };
1014
+ const registerGraphFile = (fileName, sourceFiles) => {
1015
+ if (ignoredGraphFileNamePattern.test(fileName)) return;
1016
+ const normalized = normalizeFileName$1(fileName);
1017
+ MutableHashMap.set(sourceFiles, normalized, {
1018
+ fileName: normalized,
1019
+ imports: MutableHashSet.empty()
1020
+ });
1021
+ };
1022
+ const registerSourceFiles = (programs, sourceFiles) => {
1023
+ for (const program of programs) program.getSourceFileNames().forEach((fileName) => registerGraphFile(fileName, sourceFiles));
1024
+ };
1025
+ const isUsableResolution = (resolved) => resolved !== void 0 && resolved !== "";
1026
+ const addImportEdge = (fileName, importedFileName, sourceFiles) => {
1027
+ if (!MutableHashMap.has(sourceFiles, importedFileName)) return;
1028
+ Option.match(MutableHashMap.get(sourceFiles, fileName), {
1029
+ onNone: () => void 0,
1030
+ onSome: (entry) => MutableHashSet.add(entry.imports, importedFileName)
1031
+ });
1032
+ };
1033
+ const linkImport = (fileName, specifier, sourceFiles) => {
1034
+ const resolved = resolveModuleSpecifier(fileName, specifier, sourceFiles, pathService);
1035
+ if (!isUsableResolution(resolved)) return;
1036
+ addImportEdge(fileName, resolveTSInputFile(resolved, pathService), sourceFiles);
1037
+ };
1038
+ const linkFileImports = (fileName, programs, sourceFiles) => {
1039
+ const sourceFile = programs.map((program) => program.getSourceFile(fileName)).find((candidate) => candidate != null);
1040
+ if (sourceFile === void 0) return;
1041
+ extractImports(sourceFile).forEach((specifier) => linkImport(fileName, specifier, sourceFiles));
949
1042
  };
950
1043
  const buildDependencyGraph = (programs) => Effect.gen(function* () {
951
- const s = yield* Ref.get(stateRef);
952
- for (const program of programs) for (const fileName of program.getSourceFileNames()) {
953
- if (fileName.endsWith(".d.ts") || fileName.includes("node_modules")) continue;
954
- const normalized = normalizeFileName$1(fileName);
955
- MutableHashMap.set(s.sourceFiles, normalized, {
956
- fileName: normalized,
957
- imports: MutableHashSet.empty()
958
- });
959
- }
960
- for (const [fileName] of s.sourceFiles) {
961
- const sourceFile = programs.map((p) => p.getSourceFile(fileName)).find((sf) => sf != null);
962
- if (!sourceFile) continue;
963
- const imports = extractImports(sourceFile);
964
- for (const specifier of imports) {
965
- const resolved = resolveModuleSpecifier(fileName, specifier, s.sourceFiles, pathService);
966
- if (resolved !== void 0 && resolved !== "") {
967
- const sourceFileName = resolveTSInputFile(resolved, pathService);
968
- if (MutableHashMap.has(s.sourceFiles, sourceFileName)) {
969
- const entryOpt = MutableHashMap.get(s.sourceFiles, fileName);
970
- if (Option.isSome(entryOpt)) MutableHashSet.add(entryOpt.value.imports, sourceFileName);
971
- }
972
- }
973
- }
974
- }
1044
+ const state = yield* Ref.get(stateRef);
1045
+ registerSourceFiles(programs, state.sourceFiles);
1046
+ for (const [fileName] of state.sourceFiles) linkFileImports(fileName, programs, state.sourceFiles);
975
1047
  yield* Ref.update(stateRef, (prev) => ({
976
1048
  ...prev,
977
- sourceFiles: MutableHashMap.fromIterable(s.sourceFiles)
1049
+ sourceFiles: MutableHashMap.fromIterable(state.sourceFiles)
978
1050
  }));
979
1051
  });
980
- const getNodesEffect = Effect.gen(function* () {
981
- const s = yield* Ref.get(stateRef);
982
- if (MutableHashMap.size(s.nodes) > 0) return s.nodes;
983
- for (const [fileName] of s.sourceFiles) {
984
- const node = makeTSFileNode(fileName);
985
- MutableHashMap.set(s.nodes, fileName, node);
986
- }
987
- const withChildren = MutableHashMap.empty();
988
- for (const [fileName, file] of s.sourceFiles) {
989
- const nodeOpt = MutableHashMap.get(s.nodes, fileName);
990
- if (Option.isNone(nodeOpt)) return yield* new CompilerFailed({
991
- reason: "unknown-file-node",
992
- subject: fileName
993
- });
994
- const node = nodeOpt.value;
995
- const children = Array.from(file.imports).map((importName) => Option.getOrUndefined(MutableHashMap.get(s.nodes, importName))).filter((n) => n !== void 0);
996
- MutableHashMap.set(withChildren, fileName, {
997
- ...node,
998
- children,
999
- parents: []
1000
- });
1052
+ const createEmptyNodes = (state) => {
1053
+ for (const [fileName] of state.sourceFiles) MutableHashMap.set(state.nodes, fileName, makeTSFileNode(fileName));
1054
+ };
1055
+ const childNodeOf = (state, fileName, imports) => Effect.gen(function* () {
1056
+ const node = MutableHashMap.get(state.nodes, fileName);
1057
+ if (Option.isNone(node)) return yield* new CompilerFailed({
1058
+ reason: "unknown-file-node",
1059
+ subject: fileName
1060
+ });
1061
+ const children = Array.from(imports).map((importName) => Option.getOrUndefined(MutableHashMap.get(state.nodes, importName))).filter((child) => child !== void 0);
1062
+ return {
1063
+ ...node.value,
1064
+ children,
1065
+ parents: []
1066
+ };
1067
+ });
1068
+ const collectChildNodes = (state, withChildren) => Effect.gen(function* () {
1069
+ for (const [fileName, file] of state.sourceFiles) {
1070
+ const node = yield* childNodeOf(state, fileName, file.imports);
1071
+ MutableHashMap.set(withChildren, fileName, node);
1001
1072
  }
1002
- MutableHashMap.clear(s.nodes);
1003
- for (const [k, v] of withChildren) MutableHashMap.set(s.nodes, k, v);
1073
+ });
1074
+ const replaceMapContents = (target, source) => {
1075
+ MutableHashMap.clear(target);
1076
+ for (const [key, value] of source) MutableHashMap.set(target, key, value);
1077
+ };
1078
+ const parentNodesOf = (node, nodes) => {
1079
+ const parents = [];
1080
+ MutableHashMap.forEach(nodes, (candidate) => {
1081
+ if (candidate.children.includes(node)) parents.push(candidate);
1082
+ });
1083
+ return parents;
1084
+ };
1085
+ const linkParentReferences = (state) => {
1004
1086
  const withParents = MutableHashMap.empty();
1005
- for (const [fileName, node] of s.nodes) {
1006
- const parents = [];
1007
- for (const [, n] of s.nodes) if (n.children.includes(node)) parents.push(n);
1008
- MutableHashMap.set(withParents, fileName, {
1009
- ...node,
1010
- parents
1011
- });
1012
- }
1013
- MutableHashMap.clear(s.nodes);
1014
- for (const [k, v] of withParents) MutableHashMap.set(s.nodes, k, v);
1087
+ for (const [fileName, node] of state.nodes) MutableHashMap.set(withParents, fileName, {
1088
+ ...node,
1089
+ parents: parentNodesOf(node, state.nodes)
1090
+ });
1091
+ replaceMapContents(state.nodes, withParents);
1092
+ };
1093
+ const buildFileNodes = (state) => Effect.gen(function* () {
1094
+ createEmptyNodes(state);
1095
+ const withChildren = MutableHashMap.empty();
1096
+ yield* collectChildNodes(state, withChildren);
1097
+ replaceMapContents(state.nodes, withChildren);
1098
+ linkParentReferences(state);
1099
+ yield* Ref.update(stateRef, (prev) => ({
1100
+ ...prev,
1101
+ nodes: MutableHashMap.fromIterable(state.nodes)
1102
+ }));
1103
+ });
1104
+ const getNodesEffect = Effect.gen(function* () {
1105
+ const state = yield* Ref.get(stateRef);
1106
+ if (MutableHashMap.size(state.nodes) > 0) return state.nodes;
1107
+ yield* buildFileNodes(state);
1108
+ return state.nodes;
1109
+ });
1110
+ const resetMutatedFiles = (mutants) => Effect.gen(function* () {
1111
+ for (const mutant of mutants) yield* fs.resetFile(mutant.fileName);
1112
+ });
1113
+ const applyMutant = (mutant) => Effect.gen(function* () {
1114
+ if ((yield* fs.getFile(mutant.fileName)) === void 0) return yield* new CompilerFailed({
1115
+ reason: "file-not-in-project",
1116
+ subject: mutant.fileName
1117
+ });
1118
+ yield* fs.mutateFile(mutant.fileName, mutant);
1119
+ });
1120
+ const applyMutants = (mutants) => Effect.gen(function* () {
1121
+ for (const mutant of mutants) yield* applyMutant(mutant);
1122
+ });
1123
+ const hasOpenSnapshot = (state) => state.api !== void 0 && state.snapshot !== void 0;
1124
+ const updateSnapshot = (state, changedFiles) => Effect.gen(function* () {
1125
+ const previous = state.snapshot;
1126
+ const next = state.api.updateSnapshot({
1127
+ openProjects: Array.from(state.allTSConfigFiles),
1128
+ fileChanges: { changed: changedFiles }
1129
+ });
1130
+ yield* Effect.sync(() => previous.dispose());
1015
1131
  yield* Ref.update(stateRef, (prev) => ({
1016
1132
  ...prev,
1017
- nodes: MutableHashMap.fromIterable(s.nodes)
1133
+ snapshot: next
1018
1134
  }));
1019
- return s.nodes;
1020
1135
  });
1021
1136
  const check = (mutants) => Effect.gen(function* () {
1022
1137
  const state = yield* Ref.get(stateRef);
1023
- for (const mutant of state.lastMutants) yield* fs.resetFile(mutant.fileName);
1024
- for (const mutant of mutants) {
1025
- if (!(yield* fs.getFile(mutant.fileName))) return yield* new CompilerFailed({
1026
- reason: "file-not-in-project",
1027
- subject: mutant.fileName
1028
- });
1029
- yield* fs.mutateFile(mutant.fileName, mutant);
1030
- }
1031
- const mutatedFileNames = Array.from(MutableHashSet.fromIterable(mutants.map((m) => normalizeFileName$1(m.fileName))));
1138
+ yield* resetMutatedFiles(state.lastMutants);
1139
+ yield* applyMutants(mutants);
1140
+ const mutatedFileNames = Array.from(MutableHashSet.fromIterable(mutants.map((mutant) => normalizeFileName$1(mutant.fileName))));
1032
1141
  const changedFiles = Array.from(MutableHashSet.fromIterable([...state.lastMutatedFileNames, ...mutatedFileNames]));
1033
1142
  const current = yield* Ref.get(stateRef);
1034
- if (current.api && current.snapshot) {
1035
- const oldSnapshot = current.snapshot;
1036
- const nextSnapshot = current.api.updateSnapshot({
1037
- openProjects: Array.from(current.allTSConfigFiles),
1038
- fileChanges: { changed: changedFiles }
1039
- });
1040
- yield* Effect.sync(() => oldSnapshot.dispose());
1041
- yield* Ref.update(stateRef, (prev) => ({
1042
- ...prev,
1043
- snapshot: nextSnapshot
1044
- }));
1045
- }
1143
+ if (hasOpenSnapshot(current)) yield* updateSnapshot(current, changedFiles);
1046
1144
  yield* Ref.update(stateRef, (prev) => ({
1047
1145
  ...prev,
1048
1146
  lastMutants: [...mutants],
@@ -1088,11 +1186,7 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
1088
1186
  }));
1089
1187
  });
1090
1188
  const getLineAndCharacterOfPosition = (fileName, position) => Effect.gen(function* () {
1091
- const programs = yield* getProgramsEffect();
1092
- for (const program of programs) {
1093
- const sourceFile = program.getSourceFile(fileName);
1094
- if (sourceFile) return sourceFile.getLineAndCharacterOfPosition(position);
1095
- }
1189
+ return (yield* getProgramsEffect()).map((program) => program.getSourceFile(fileName)).find((sourceFile) => sourceFile !== void 0)?.getLineAndCharacterOfPosition(position);
1096
1190
  });
1097
1191
  return {
1098
1192
  init,
@@ -1103,141 +1197,74 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
1103
1197
  };
1104
1198
  }
1105
1199
  //#endregion
1200
+ //#region src/mutant-groups.ts
1201
+ const groupsWithStrangers = (inside, outside, nodes) => {
1202
+ const groups = createGroups([...inside], nodes);
1203
+ if (outside.length > 0) return [outside.map((mutant) => mutant.id), ...groups];
1204
+ return groups;
1205
+ };
1206
+ const knownFileGroups = (mutants, nodes) => {
1207
+ const inside = mutants.filter((mutant) => Option.isSome(MutableHashMap.get(nodes, normalizeFileName(mutant.fileName))));
1208
+ const outside = mutants.filter((mutant) => Option.isNone(MutableHashMap.get(nodes, normalizeFileName(mutant.fileName))));
1209
+ if (inside.length === 0) return mutants.map((mutant) => [mutant.id]);
1210
+ return groupsWithStrangers(inside, outside, nodes);
1211
+ };
1212
+ const groupMutants = (mutants, nodes, prioritizePerformanceOverAccuracy) => {
1213
+ if (prioritizePerformanceOverAccuracy) return knownFileGroups(mutants, nodes);
1214
+ return [mutants.map((mutant) => mutant.id)];
1215
+ };
1216
+ //#endregion
1106
1217
  //#region src/Checker.ts
1107
- const normalizeFileName = (fileName) => fileName.replace(/\\/g, "/");
1108
- function partitionMutantsForGrouping(mutants, nodes, prioritizePerformanceOverAccuracy) {
1109
- if (!prioritizePerformanceOverAccuracy) return {
1110
- inside: [],
1111
- outside: [...mutants]
1112
- };
1113
- const outside = [];
1114
- const inside = [];
1115
- for (const m of mutants) if (Option.isNone(MutableHashMap.get(nodes, normalizeFileName(m.fileName)))) outside.push(m);
1116
- else inside.push(m);
1117
- return {
1118
- inside,
1119
- outside
1120
- };
1121
- }
1122
1218
  function getPrioritize(options) {
1123
- if (!Predicate.hasProperty(options, "typescriptChecker")) return false;
1124
- const tc = options["typescriptChecker"];
1125
- if (typeof tc !== "object" || tc === null) return false;
1126
- if (!Predicate.hasProperty(tc, "prioritizePerformanceOverAccuracy")) return false;
1127
- const val = tc["prioritizePerformanceOverAccuracy"];
1128
- if (typeof val === "boolean") return val;
1129
- return false;
1219
+ const decoded = Schema.decodeUnknownOption(TypescriptCheckerOptionsSchema)(options);
1220
+ return Option.getOrElse(Option.flatMap(decoded, (value) => Option.fromUndefinedOr(value.typescriptChecker?.prioritizePerformanceOverAccuracy)), () => false);
1130
1221
  }
1222
+ const refuse = (mutantIds, cause) => new CheckerFailed({
1223
+ checkerName: "typescript",
1224
+ mutantIds: [...mutantIds],
1225
+ cause: errorToString(cause)
1226
+ });
1227
+ const severityOf = (category) => Match.value(category).pipe(Match.when(DiagnosticCategory.Error, () => "error"), Match.when(DiagnosticCategory.Warning, () => "warning"), Match.when(DiagnosticCategory.Suggestion, () => "suggestion"), Match.orElse(() => "message"));
1228
+ const toCheckResult = (answer) => {
1229
+ if (answer.status === "passed") return { status: "passed" };
1230
+ return {
1231
+ status: "compileError",
1232
+ reason: answer.reason
1233
+ };
1234
+ };
1235
+ const mergeAnswers = (runs) => runs.reduce((merged, answers) => Object.entries(answers).reduce((into, [id, answer]) => HashMap.set(into, id, toCheckResult(answer)), merged), HashMap.empty());
1131
1236
  const checkCell = Cell.layer({
1132
- read: (command) => Effect.gen(function* () {
1133
- const compiler = yield* TypeScriptCompiler;
1134
- const nodesHm = yield* compiler.nodes.pipe(Effect.mapError((cause) => new CheckerFailed({
1135
- checkerName: "typescript",
1136
- mutantIds: command.mutants.map((m) => m.id),
1137
- cause: errorToString(cause)
1138
- })));
1139
- const nodes = {};
1140
- for (const [k, v] of nodesHm) nodes[k] = v;
1141
- const diagnostics = yield* compiler.check([...command.mutants]).pipe(Effect.mapError((cause) => new CheckerFailed({
1142
- checkerName: "typescript",
1143
- mutantIds: command.mutants.map((m) => m.id),
1144
- cause: errorToString(cause)
1145
- })));
1146
- return new CheckMutantsInput({
1147
- mutants: [...command.mutants],
1148
- diagnostics: [...diagnostics],
1149
- nodes
1150
- });
1151
- }),
1152
- decode: (raw) => Result.succeed(raw),
1237
+ read: (command) => Effect.flatMap(TypeScriptCompiler, (compiler) => Effect.zipWith(compiler.nodes, compiler.check([...command.mutants]), (nodes, diagnostics) => new CheckMutantsInput({
1238
+ mutants: [...command.mutants],
1239
+ diagnostics: [...diagnostics],
1240
+ nodes: Object.fromEntries(nodes)
1241
+ }))).pipe(Effect.mapError((cause) => refuse(command.mutants.map((mutant) => mutant.id), cause))),
1153
1242
  decide: checkMutants,
1154
- encode: (outcome) => outcome,
1155
1243
  write: (outcome) => Result.match(outcome, {
1156
- onFailure: (failure) => Effect.fail(new CheckerFailed({
1157
- checkerName: "typescript",
1158
- mutantIds: [],
1159
- cause: errorToString(failure)
1160
- })),
1161
- onSuccess: (decision) => Effect.succeed(decision)
1244
+ onFailure: (failure) => Effect.fail(refuse([], failure)),
1245
+ onSuccess: Effect.succeed
1162
1246
  })
1163
1247
  });
1164
1248
  function makeCheckerService({ options, compiler }) {
1165
- const formatDiagnostic = (error) => Effect.gen(function* () {
1166
- let severity;
1167
- if (error.category === DiagnosticCategory.Error) severity = "error";
1168
- else if (error.category === DiagnosticCategory.Warning) severity = "warning";
1169
- else if (error.category === DiagnosticCategory.Suggestion) severity = "suggestion";
1170
- else severity = "message";
1171
- let location = "";
1172
- const unknownError = error;
1173
- if (typeof unknownError === "object" && unknownError !== null && "fileName" in unknownError && typeof unknownError.fileName === "string") {
1174
- const fileName = unknownError.fileName;
1175
- const lineAndCharacter = yield* compiler.getLineAndCharacterOfPosition(fileName, error.pos).pipe(Effect.orElseSucceed(() => void 0));
1176
- location = `${fileName}(${(lineAndCharacter?.line ?? 0) + 1},${(lineAndCharacter?.character ?? 0) + 1}): `;
1177
- } else if (error.fileName !== void 0 && error.fileName !== "") {
1178
- const lineAndCharacter = yield* compiler.getLineAndCharacterOfPosition(error.fileName, error.pos).pipe(Effect.orElseSucceed(() => void 0));
1179
- const line = (lineAndCharacter?.line ?? 0) + 1;
1180
- const character = (lineAndCharacter?.character ?? 0) + 1;
1181
- location = `${error.fileName}(${line},${character}): `;
1182
- }
1183
- return `${location}${severity} TS${error.code}: ${error.text}`;
1184
- });
1185
- const createErrorText = (errors) => Effect.gen(function* () {
1186
- return (yield* Effect.forEach(errors, formatDiagnostic)).join("\n");
1249
+ const verify = Cell.provide(checkCell, Layer.succeed(TypeScriptCompiler, compiler));
1250
+ const positionOf = (error) => Option.match(Option.filter(Option.fromUndefinedOr(error.fileName), (fileName) => fileName !== ""), {
1251
+ onNone: () => Effect.succeed(""),
1252
+ onSome: (fileName) => compiler.getLineAndCharacterOfPosition(fileName, error.pos).pipe(Effect.orElseSucceed(() => void 0), Effect.map((at) => Option.match(Option.fromUndefinedOr(at), {
1253
+ onNone: () => `${fileName}(1,1): `,
1254
+ onSome: (position) => `${fileName}(${position.line + 1},${position.character + 1}): `
1255
+ })))
1187
1256
  });
1257
+ const formatDiagnostic = (error) => positionOf(error).pipe(Effect.map((position) => `${position}${severityOf(error.category)} TS${error.code}: ${error.text}`));
1258
+ const createErrorText = (errors) => Effect.map(Effect.forEach(errors, formatDiagnostic), (parts) => parts.join("\n"));
1259
+ const soloRound = (mutant) => Cell.run(verify, new CheckMutantsCommand({ mutants: [mutant] })).pipe(Effect.map((decision) => decision.results));
1260
+ const soloRounds = (decision) => Match.value(decision).pipe(Match.tag("CheckFinished", () => Effect.succeed([])), Match.tag("RetestRequired", (retest) => Cell.run(verify, new CheckMutantsCommand({ mutants: [] })).pipe(Effect.flatMap(() => Effect.forEach(retest.needsRetest, soloRound)))), Match.exhaustive);
1188
1261
  return {
1189
- init: Effect.gen(function* () {
1190
- const errors = yield* compiler.init.pipe(Effect.mapError((cause) => new CheckerFailed({
1191
- checkerName: "typescript",
1192
- mutantIds: [],
1193
- cause: errorToString(cause)
1194
- })));
1195
- if (errors.length > 0) {
1196
- const text = yield* createErrorText(errors);
1197
- return yield* new CheckerFailed({
1198
- checkerName: "typescript",
1199
- mutantIds: [],
1200
- cause: errorToString(/* @__PURE__ */ new Error(`Typescript error(s) found in dry run compilation: ${text}`))
1201
- });
1202
- }
1203
- }),
1204
- check: (mutants) => Effect.gen(function* () {
1205
- const applyOnce = (group) => Cell.run(checkCell, new CheckMutantsCommand({ mutants: [...group] })).pipe(Effect.provideService(TypeScriptCompiler, compiler));
1206
- const first = yield* applyOnce(mutants);
1207
- let map = HashMap.empty();
1208
- const mergeResults = (results) => {
1209
- for (const [id, value] of Object.entries(results)) if (value.status === "passed") map = HashMap.set(map, id, { status: "passed" });
1210
- else map = HashMap.set(map, id, {
1211
- status: "compileError",
1212
- reason: value.reason
1213
- });
1214
- };
1215
- mergeResults(first.results);
1216
- yield* Match.value(first).pipe(Match.tag("CheckFinished", () => Effect.void), Match.tag("RetestRequired", (retest) => Effect.gen(function* () {
1217
- yield* applyOnce([]);
1218
- const originals = {};
1219
- for (const m of mutants) originals[m.id] = m;
1220
- for (const pending of retest.needsRetest) {
1221
- const original = originals[pending.id];
1222
- if (original === void 0) continue;
1223
- const one = yield* applyOnce([original]);
1224
- mergeResults(one.results);
1225
- }
1226
- })), Match.exhaustive);
1227
- return map;
1228
- }),
1229
- group: (mutants) => Effect.gen(function* () {
1230
- const nodes = yield* compiler.nodes.pipe(Effect.mapError((cause) => new CheckerFailed({
1231
- checkerName: "typescript",
1232
- mutantIds: mutants.map((m) => m.id),
1233
- cause: errorToString(cause)
1234
- })));
1235
- const { inside, outside } = partitionMutantsForGrouping(mutants, nodes, getPrioritize(options));
1236
- if (inside.length === 0) return mutants.map((m) => [m.id]);
1237
- const groups = createGroups([...inside], nodes);
1238
- if (outside.length > 0) return [outside.map((m) => m.id), ...groups];
1239
- return groups;
1240
- })
1262
+ init: compiler.init.pipe(Effect.mapError((cause) => refuse([], cause)), Effect.flatMap((errors) => {
1263
+ if (errors.length === 0) return Effect.void;
1264
+ return createErrorText(errors).pipe(Effect.map((text) => refuse([], /* @__PURE__ */ new Error(`Typescript error(s) found in dry run compilation: ${text}`))), Effect.flatMap(Effect.fail));
1265
+ })),
1266
+ check: (mutants) => Cell.run(verify, new CheckMutantsCommand({ mutants: [...mutants] })).pipe(Effect.flatMap((first) => Effect.map(soloRounds(first), (rounds) => mergeAnswers([first.results, ...rounds])))),
1267
+ group: (mutants) => compiler.nodes.pipe(Effect.map((nodes) => groupMutants(mutants, nodes, getPrioritize(options))), Effect.mapError((cause) => refuse(mutants.map((mutant) => mutant.id), cause)))
1241
1268
  };
1242
1269
  }
1243
1270
  //#endregion