@systemfsoftware/stryker-js-typescript-checker 2.0.0 → 3.0.1

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
@@ -1,453 +1,182 @@
1
1
  import { readFileSync } from "fs";
2
- import { CheckStatus, Checker, CheckerFailed } from "@systemfsoftware/stryker-js-plugin-api/check";
3
- import { PluginKind, RunConfiguration, declarePlugin } from "@systemfsoftware/stryker-js-plugin-api/plugin";
2
+ import { Checker, CheckerFailed } from "@systemfsoftware/stryker-js/Checker";
3
+ import { RunConfiguration, declarePlugin } from "@systemfsoftware/stryker-js/Plugin";
4
4
  import * as Effect from "effect/Effect";
5
5
  import * as FileSystem from "effect/FileSystem";
6
6
  import * as Layer from "effect/Layer";
7
7
  import * as Path from "effect/Path";
8
8
  import * as S from "effect/Schema";
9
- import { StrykerOptionsSchema, normalizeFileName, strykerReportBugUrl } from "@systemfsoftware/stryker-js-plugin-api/core";
10
- import * as Ref from "effect/Ref";
11
9
  import { EOL } from "os";
10
+ import { Cell, Wire, Workflow } from "@systemfsoftware/effect-cell-types";
11
+ import { Mutant, errorToString } from "@systemfsoftware/stryker-js/Mutant";
12
12
  import { Predicate, Result, Schema } from "effect";
13
- import * as Match from "effect/Match";
13
+ import { pipe } from "effect/Function";
14
+ import * as HashMap from "effect/HashMap";
15
+ import * as MutableHashMap from "effect/MutableHashMap";
16
+ import * as Option from "effect/Option";
14
17
  import { API, DiagnosticCategory } from "typescript/unstable/sync";
18
+ import * as Result$1 from "effect/Result";
19
+ import { createRequire } from "module";
20
+ import { StrykerOptionsSchema } from "@systemfsoftware/stryker-js/Schema";
15
21
  import * as Context from "effect/Context";
22
+ import * as MutableHashSet from "effect/MutableHashSet";
23
+ import * as Ref from "effect/Ref";
16
24
  import { SyntaxKind } from "typescript/unstable/ast";
17
- import { createRequire } from "module";
18
- //#region src/project/hybrid-file-system.schema.ts
19
- var HybridFileNotFoundError = class extends S.TaggedError()("HybridFileNotFoundError", { fileName: S.String }) {};
20
- //#endregion
21
- //#region src/project/script-file.ts
22
- function makeScriptFile(content, fileName, modifiedTime = /* @__PURE__ */ new Date()) {
23
- return {
24
- content,
25
- fileName,
26
- originalContent: content,
27
- modifiedTime
28
- };
29
- }
30
- function withContent(file, content) {
31
- return {
32
- ...file,
33
- content,
34
- modifiedTime: /* @__PURE__ */ new Date()
35
- };
36
- }
37
- function mutateScriptFile(file, mutant) {
38
- const start = getOffset(file, mutant.location.start);
39
- const end = getOffset(file, mutant.location.end);
40
- const content = `${file.originalContent.slice(0, start)}${mutant.replacement}${file.originalContent.slice(end)}`;
41
- return {
42
- ...file,
43
- content,
44
- modifiedTime: /* @__PURE__ */ new Date()
45
- };
46
- }
47
- function resetScriptFile(file) {
48
- return {
49
- ...file,
50
- content: file.originalContent,
51
- modifiedTime: /* @__PURE__ */ new Date()
52
- };
53
- }
54
- function getOffset(file, pos) {
55
- const lines = file.originalContent.split("\n");
56
- const lineCount = Math.min(pos.line, lines.length);
57
- let offset = 0;
58
- for (let i = 0; i < lineCount; i++) {
59
- const line = lines[i];
60
- if (line === void 0) break;
61
- offset += line.length + 1;
62
- }
63
- offset += pos.column;
64
- return offset;
65
- }
66
- //#endregion
67
- //#region src/project/hybrid-file-system.ts
68
- const makeEmptyFilesMap = () => /* @__PURE__ */ new Map();
69
- const makeEmptyOverridesMap = () => /* @__PURE__ */ new Map();
70
- const copyAndSet = (map, key, value) => {
71
- const next = new Map(map);
72
- next.set(key, value);
73
- return next;
74
- };
75
- const makeHybridFileSystem = (fsService) => Effect.gen(function* () {
76
- const filesRef = yield* Ref.make(makeEmptyFilesMap());
77
- const overridesRef = yield* Ref.make(makeEmptyOverridesMap());
78
- const fileNameIsBuildInfo = (fileName) => fileName.endsWith(".tsbuildinfo");
79
- const fileSystem = {
80
- readFile: (fileName) => {
81
- const normalized = normalizeFileName(fileName);
82
- if (fileNameIsBuildInfo(normalized)) return null;
83
- const override = overridesRef.ref.current.get(normalized);
84
- if (override !== void 0) return override;
85
- const files = filesRef.ref.current;
86
- const file = files.get(normalized);
87
- if (file) return file.content;
88
- if (file === void 0 && files.has(normalized)) return null;
89
- },
90
- fileExists: (fileName) => {
91
- const normalized = normalizeFileName(fileName);
92
- if (fileNameIsBuildInfo(normalized)) return false;
93
- if (overridesRef.ref.current.has(normalized)) return true;
94
- const files = filesRef.ref.current;
95
- if (files.has(normalized)) return files.get(normalized) !== void 0;
96
- },
97
- directoryExists: () => void 0,
98
- getAccessibleEntries: () => void 0,
99
- realpath: () => void 0
100
- };
101
- const getFile = (fileName) => Effect.gen(function* () {
102
- const normalized = normalizeFileName(fileName);
103
- const files = yield* Ref.get(filesRef);
104
- if (files.has(normalized)) return files.get(normalized);
105
- const content = yield* fsService.readFileString(normalized).pipe(Effect.orElseSucceed(() => void 0));
106
- if (content === void 0) {
107
- yield* Ref.update(filesRef, (m) => copyAndSet(m, normalized, void 0));
108
- return;
109
- }
110
- const file = makeScriptFile(content, normalized);
111
- yield* Ref.update(filesRef, (m) => copyAndSet(m, normalized, file));
112
- return file;
113
- });
114
- const writeFile = (fileName, data) => Effect.gen(function* () {
115
- const normalized = normalizeFileName(fileName);
116
- const existing = (yield* Ref.get(filesRef)).get(normalized);
117
- if (existing) {
118
- const next = withContent(existing, data);
119
- yield* Ref.update(filesRef, (m) => copyAndSet(m, normalized, next));
120
- } else {
121
- const file = makeScriptFile(data, normalized);
122
- yield* Ref.update(filesRef, (m) => copyAndSet(m, normalized, file));
123
- }
124
- });
125
- const mutateFile = (fileName, mutant) => Effect.gen(function* () {
126
- const file = yield* getFile(fileName);
127
- if (!file) return yield* new HybridFileNotFoundError({ fileName });
128
- const next = mutateScriptFile(file, mutant);
129
- const normalized = normalizeFileName(fileName);
130
- yield* Ref.update(filesRef, (m) => copyAndSet(m, normalized, next));
131
- });
132
- const resetFile = (fileName) => Effect.gen(function* () {
133
- const normalized = normalizeFileName(fileName);
134
- const file = (yield* Ref.get(filesRef)).get(normalized);
135
- if (file) {
136
- const next = resetScriptFile(file);
137
- yield* Ref.update(filesRef, (m) => copyAndSet(m, normalized, next));
138
- }
139
- });
140
- const existsInMemory = (fileName) => Effect.gen(function* () {
141
- return (yield* Ref.get(filesRef)).get(normalizeFileName(fileName)) !== void 0;
142
- });
143
- const setTsConfigOverrides = (overrides) => Ref.set(overridesRef, overrides);
144
- return {
145
- fileSystem,
146
- getFile,
147
- writeFile,
148
- mutateFile,
149
- resetFile,
150
- existsInMemory,
151
- setTsConfigOverrides
152
- };
153
- });
154
- //#endregion
155
- //#region src/grouping/ts-file-node.ts
156
- function makeTSFileNode(fileName) {
157
- return {
158
- fileName,
159
- parents: [],
160
- children: []
161
- };
162
- }
163
- function getAllParentReferencesIncludingSelf(node, allParentReferences = /* @__PURE__ */ new Set()) {
164
- allParentReferences.add(node);
165
- for (const parent of node.parents) if (!allParentReferences.has(parent)) getAllParentReferencesIncludingSelf(parent, allParentReferences);
166
- return allParentReferences;
167
- }
168
- function getMutantsWithReferenceToChildrenOrSelf(node, mutants, nodesChecked = []) {
169
- if (nodesChecked.includes(node.fileName)) return [];
170
- nodesChecked.push(node.fileName);
171
- const relatedMutants = mutants.filter((m) => normalizeFileName(m.fileName) === node.fileName);
172
- const childResult = node.children.flatMap((c) => getMutantsWithReferenceToChildrenOrSelf(c, mutants, nodesChecked));
173
- return [...relatedMutants, ...childResult];
174
- }
175
- //#endregion
176
- //#region src/diagnostics.schema.ts
25
+ //#region src/Checker.schema.ts
177
26
  /**
178
- * The compiler returned a diagnostic without a file name. The checker cannot
179
- * attribute it to a mutant and the run should be failed rather than silently
180
- * marking every mutant as a compile error.
27
+ * Checker declarations for the TypeScript checker.
28
+ *
29
+ * Houses the wire types and error variants shared by the capability and its
30
+ * workflow. Decoded at the checker boundary; no I/O.
181
31
  */
182
- var DiagnosticWithoutFileError = class extends S.TaggedError()("DiagnosticWithoutFileError", { text: S.String }) {};
32
+ var CheckMutantsCommand = class extends S.TaggedClass()("CheckMutantsCommand", { mutants: S.Array(Mutant) }) {};
183
33
  /**
184
- * The compiler reported a diagnostic in a file that is not part of the
185
- * project graph built from the tsconfig. This indicates a graph or FS
186
- * mismatch and must be surfaced as a checker failure rather than ignored.
34
+ * Every way the TypeScript compiler can fail while serving a check.
35
+ * One tagged error callers branch only on failure itself; `reason` keeps
36
+ * cases distinguishable in reports.
187
37
  */
188
- var DiagnosticInUnrelatedFileError = class extends S.TaggedError()("DiagnosticInUnrelatedFileError", {
189
- text: S.String,
190
- fileName: S.String
191
- }) {};
38
+ var CompilerFailed = class extends S.TaggedError()("CompilerFailed", {
39
+ reason: S.Literals([
40
+ "not-initialized",
41
+ "no-projects",
42
+ "unknown-file-node",
43
+ "file-not-in-project"
44
+ ]),
45
+ subject: S.optional(S.String)
46
+ }) {
47
+ get message() {
48
+ switch (this.reason) {
49
+ case "not-initialized": return "The TypeScript compiler was used before it was initialized";
50
+ case "no-projects": return `No projects were found for ${this.subject ?? "the tsconfig"}`;
51
+ case "unknown-file-node": return `The file graph has no node for '${this.subject ?? "a file"}', which should not happen`;
52
+ case "file-not-in-project": return `'${this.subject ?? "a file"}' is part of your TypeScript project but could not be found on disk`;
53
+ }
54
+ }
55
+ };
192
56
  //#endregion
193
- //#region src/diagnostics.ts
57
+ //#region src/Checker.workflow.ts
194
58
  /**
195
- * Pure decision: given a batch of diagnostics and the file graph, decide
196
- * which mutants are definitely responsible and which need an individual
197
- * re-check.
59
+ * Checker pure check decision.
198
60
  *
199
- * No I/O, no clock, no throwing diagnostics in, verdict out. The caller
200
- * (executor) performs the compiler call and, if `needsRetest` is non-empty,
201
- * re-invokes the compiler for each of those mutants individually.
61
+ * `Workflow.make` lives here and only here. The workflow receives a fully
62
+ * decoded input (mutants + diagnostics + file graph) and produces results plus
63
+ * `needsRetest` without touching I/O.
202
64
  */
203
- function classifyDiagnostics(diagnostics, mutants, nodes) {
204
- const definitive = /* @__PURE__ */ new Map();
205
- const needsRetest = /* @__PURE__ */ new Map();
65
+ var DiagnosticWithoutFileError = class extends S.TaggedError()("DiagnosticWithoutFileError", { text: Wire.mint(S.String) }) {};
66
+ var DiagnosticInUnrelatedFileError = class extends S.TaggedError()("DiagnosticInUnrelatedFileError", {
67
+ text: Wire.mint(S.String),
68
+ fileName: Wire.mint(S.String)
69
+ }) {};
70
+ const DiagnosticSchema = Wire.wire({
71
+ fileName: Wire.mint(S.optional(Wire.mint(S.String))),
72
+ text: Wire.mint(S.String)
73
+ });
74
+ const TSFileNodeSchema = Wire.mint(S.suspend(() => Wire.wire({
75
+ fileName: Wire.mint(S.String),
76
+ parents: Wire.mint(S.Array(TSFileNodeSchema)),
77
+ children: Wire.mint(S.Array(TSFileNodeSchema))
78
+ })));
79
+ var CheckMutantsInput = class extends S.TaggedClass()("CheckMutantsInput", {
80
+ mutants: S.Array(Mutant),
81
+ diagnostics: S.Array(DiagnosticSchema),
82
+ nodes: Wire.mint(S.Record(Wire.mint(S.String), TSFileNodeSchema))
83
+ }) {};
84
+ const normalizeFileName$3 = (fileName) => fileName.replace(/\\/g, "/");
85
+ const getMutantsWithReferenceToChildrenOrSelf = (node, mutants, nodesChecked = []) => {
86
+ if (nodesChecked.includes(node.fileName)) return [];
87
+ nodesChecked.push(node.fileName);
88
+ const relatedMutants = mutants.filter((m) => normalizeFileName$3(m.fileName) === node.fileName);
89
+ const childResult = node.children.flatMap((c) => getMutantsWithReferenceToChildrenOrSelf(c, mutants, nodesChecked));
90
+ return [...relatedMutants, ...childResult];
91
+ };
92
+ const classifyDiagnosticsPure = (diagnostics, mutants, nodes) => {
93
+ const definitive = {};
94
+ const needsRetest = {};
206
95
  if (diagnostics.length > 0 && mutants.length === 1) {
207
96
  const only = mutants[0];
208
- if (only !== void 0) definitive.set(only.id, [...diagnostics]);
209
- return Result.succeed({
210
- definitive,
211
- needsRetest: []
212
- });
97
+ if (only !== void 0) {
98
+ definitive[only.id] = [...diagnostics];
99
+ return Result$1.succeed({
100
+ definitive,
101
+ needsRetest: []
102
+ });
103
+ }
213
104
  }
214
105
  for (const diagnostic of diagnostics) {
215
- if (!diagnostic.fileName) return Result.fail(new DiagnosticWithoutFileError({ text: diagnostic.text }));
216
- const node = nodes.get(diagnostic.fileName);
217
- if (!node) return Result.fail(new DiagnosticInUnrelatedFileError({
106
+ const fileName = diagnostic.fileName;
107
+ if (fileName === void 0 || fileName === "") return Result$1.fail(new DiagnosticWithoutFileError({ text: diagnostic.text }));
108
+ const node = nodes[fileName];
109
+ if (node === void 0) return Result$1.fail(new DiagnosticInUnrelatedFileError({
218
110
  text: diagnostic.text,
219
- fileName: diagnostic.fileName
111
+ fileName
220
112
  }));
221
113
  const related = getMutantsWithReferenceToChildrenOrSelf(node, [...mutants]);
222
- if (related.length === 0) for (const m of mutants) needsRetest.set(m.id, m);
114
+ if (related.length === 0) for (const m of mutants) needsRetest[m.id] = m;
223
115
  else if (related.length === 1) {
224
116
  const only = related[0];
225
117
  if (only !== void 0) {
226
- const existing = definitive.get(only.id);
227
- if (existing) existing.push(diagnostic);
228
- else definitive.set(only.id, [diagnostic]);
118
+ const existing = definitive[only.id];
119
+ if (existing !== void 0) existing.push(diagnostic);
120
+ else definitive[only.id] = [diagnostic];
229
121
  }
230
- } else for (const m of related) needsRetest.set(m.id, m);
122
+ } else for (const m of related) needsRetest[m.id] = m;
231
123
  }
232
- const filteredRetest = [...needsRetest.values()].filter((m) => !definitive.has(m.id));
233
- return Result.succeed({
124
+ const filteredRetest = Object.values(needsRetest).filter((m) => definitive[m.id] === void 0);
125
+ return Result$1.succeed({
234
126
  definitive,
235
127
  needsRetest: filteredRetest
236
128
  });
237
- }
238
- /**
239
- * Pure grouping decision: delegates to the existing `createGroups` but
240
- * expressed as a kernel entry so the checker can state the boundary.
241
- * `nodes` is the file graph; mutants outside the project are separated
242
- * before grouping, matching the executor's `split` that was previously
243
- * inline in `TypescriptChecker.group`.
244
- */
245
- function partitionMutantsForGrouping(mutants, nodes, prioritizePerformanceOverAccuracy) {
246
- if (!prioritizePerformanceOverAccuracy) return {
247
- inside: [],
248
- outside: [...mutants]
249
- };
250
- const outside = [];
251
- const inside = [];
252
- for (const m of mutants) if (nodes.get(normalizeFileName(m.fileName)) == null) outside.push(m);
253
- else inside.push(m);
254
- return {
255
- inside,
256
- outside
257
- };
258
- }
259
- //#endregion
260
- //#region src/grouping/create-groups.ts
261
- /**
262
- * To speed up the type-checking we want to check multiple mutants at once.
263
- * When multiple mutants in different files don't have overlap in affected files (or have small overlap), we can type-check them simultaneously.
264
- * These mutants who can be tested at the same time are called a group.
265
- * Therefore, the return type is an array of arrays, in other words: an array of groups.
266
- *
267
- * @param mutants All the mutants of the test project.
268
- * @param nodes A graph representation of the test project.
269
- */
270
- function createGroups(mutants, nodes) {
271
- const groups = [];
272
- const mutantsToGroup = new Set(mutants);
273
- while (mutantsToGroup.size) {
274
- const group = [];
275
- const groupNodes = /* @__PURE__ */ new Set();
276
- const nodesToIgnore = /* @__PURE__ */ new Set();
277
- for (const currentMutant of mutantsToGroup) {
278
- const currentNode = findNode(currentMutant.fileName, nodes);
279
- if (!nodesToIgnore.has(currentNode) && !parentsHaveOverlapWith(currentNode, groupNodes)) {
280
- group.push(currentMutant.id);
281
- groupNodes.add(currentNode);
282
- mutantsToGroup.delete(currentMutant);
283
- addRangeOfNodesToSet(nodesToIgnore, getAllParentReferencesIncludingSelf(currentNode));
284
- }
285
- }
286
- groups.push(group);
287
- }
288
- return groups;
289
- }
290
- function addRangeOfNodesToSet(nodes, nodesToAdd) {
291
- for (const parent of nodesToAdd) nodes.add(parent);
292
- }
293
- function findNode(fileName, nodes) {
294
- const node = nodes.get(normalizeFileName(fileName));
295
- if (node == null) throw new Error(`Node not in graph: ${fileName}`);
296
- return node;
297
- }
298
- function parentsHaveOverlapWith(currentNode, groupNodes) {
299
- for (const parentNode of getAllParentReferencesIncludingSelf(currentNode)) if (groupNodes.has(parentNode)) return true;
300
- return false;
301
- }
302
- //#endregion
303
- //#region src/typescript-checker.ts
304
- const emptyCheckResultMap = () => /* @__PURE__ */ new Map();
305
- const emptyDiagnosticMap = () => /* @__PURE__ */ new Map();
306
- function getPrioritize(options) {
307
- if (!Predicate.hasProperty(options, "typescriptChecker")) return false;
308
- const tc = options["typescriptChecker"];
309
- if (typeof tc !== "object" || tc === null) return false;
310
- if (!Predicate.hasProperty(tc, "prioritizePerformanceOverAccuracy")) return false;
311
- const val = tc["prioritizePerformanceOverAccuracy"];
312
- return typeof val === "boolean" ? val : false;
313
- }
314
- function makeCheckerService({ options, compiler }) {
315
- const formatDiagnostic = (error) => Effect.gen(function* () {
316
- const severity = error.category === DiagnosticCategory.Error ? "error" : error.category === DiagnosticCategory.Warning ? "warning" : error.category === DiagnosticCategory.Suggestion ? "suggestion" : "message";
317
- let location = "";
318
- if (error.fileName) {
319
- const lineAndCharacter = yield* compiler.getLineAndCharacterOfPosition(error.fileName, error.pos).pipe(Effect.orElseSucceed(() => void 0));
320
- const line = (lineAndCharacter?.line ?? 0) + 1;
321
- const character = (lineAndCharacter?.character ?? 0) + 1;
322
- location = `${error.fileName}(${line},${character}): `;
323
- }
324
- return `${location}${severity} TS${error.code}: ${error.text}`;
325
- });
326
- const createErrorText = (errors) => Effect.gen(function* () {
327
- return (yield* Effect.forEach(errors, formatDiagnostic)).join(EOL);
129
+ };
130
+ const buildResult = (input) => {
131
+ const mutants = input.mutants;
132
+ const diagnostics = input.diagnostics;
133
+ const nodes = input.nodes;
134
+ if (mutants.length === 0) return Result$1.succeed({
135
+ results: {},
136
+ needsRetest: []
328
137
  });
329
- const checkErrors = (mutants, errorsMap, nodes) => Effect.gen(function* () {
330
- const classified = classifyDiagnostics(yield* compiler.check([...mutants]).pipe(Effect.mapError((cause) => new CheckerFailed({
331
- checkerName: "typescript",
332
- mutantIds: mutants.map((m) => m.id),
333
- cause
334
- }))), mutants, nodes);
335
- if (Result.isFailure(classified)) {
336
- const failure = classified.failure;
337
- const message = Match.value(failure).pipe(Match.tag("DiagnosticWithoutFileError", (f) => `Typescript error: '${f.text}' was reported without a corresponding file. This shouldn't happen. Please open an issue using this link: ${strykerReportBugUrl(`[BUG]: TypeScript checker reports compile error without a corresponding file: ${f.text}`)}`), Match.tag("DiagnosticInUnrelatedFileError", (f) => `Typescript error: '${f.text}' was reported in an unrelated file (${f.fileName}). This file is not part of your project, or referenced from your project. This shouldn't happen, please open an issue using this link: ${strykerReportBugUrl(`[BUG]: TypeScript checker reports compile error in an unrelated file: ${f.text}`)}`), Match.exhaustive);
338
- return yield* new CheckerFailed({
339
- checkerName: "typescript",
340
- mutantIds: mutants.map((m) => m.id),
341
- cause: new Error(message)
342
- });
343
- }
344
- const { definitive, needsRetest } = classified.success;
345
- for (const [id, errors] of definitive.entries()) {
346
- const existing = errorsMap.get(id);
347
- if (existing) existing.push(...errors);
348
- else errorsMap.set(id, [...errors]);
349
- }
350
- if (needsRetest.length > 0) {
351
- yield* compiler.check([]).pipe(Effect.mapError((cause) => new CheckerFailed({
352
- checkerName: "typescript",
353
- mutantIds: needsRetest.map((m) => m.id),
354
- cause
355
- })));
356
- for (const mutant of needsRetest) {
357
- if (errorsMap.has(mutant.id)) continue;
358
- yield* checkErrors([mutant], errorsMap, nodes);
359
- }
360
- }
138
+ const first = mutants[0];
139
+ if (first === void 0 || nodes[normalizeFileName$3(first.fileName)] === void 0) {
140
+ const results = {};
141
+ for (const m of mutants) results[m.id] = { status: "passed" };
142
+ return Result$1.succeed({
143
+ results,
144
+ needsRetest: []
145
+ });
146
+ }
147
+ const classified = classifyDiagnosticsPure(diagnostics, mutants, nodes);
148
+ if (Result$1.isFailure(classified)) return Result$1.fail(classified.failure);
149
+ const { definitive, needsRetest } = classified.success;
150
+ const retestIds = {};
151
+ for (const m of needsRetest) retestIds[m.id] = true;
152
+ const results = {};
153
+ for (const m of mutants) {
154
+ const diags = definitive[m.id];
155
+ if (diags !== void 0) results[m.id] = {
156
+ status: "compileError",
157
+ reason: diags.map((d) => d.text).join("\n")
158
+ };
159
+ else if (retestIds[m.id] !== true) results[m.id] = { status: "passed" };
160
+ }
161
+ return Result$1.succeed({
162
+ results,
163
+ needsRetest
361
164
  });
362
- return {
363
- init: Effect.gen(function* () {
364
- const errors = yield* compiler.init.pipe(Effect.mapError((cause) => new CheckerFailed({
365
- checkerName: "typescript",
366
- mutantIds: [],
367
- cause
368
- })));
369
- if (errors.length > 0) {
370
- const text = yield* createErrorText(errors);
371
- return yield* new CheckerFailed({
372
- checkerName: "typescript",
373
- mutantIds: [],
374
- cause: /* @__PURE__ */ new Error(`Typescript error(s) found in dry run compilation: ${text}`)
375
- });
376
- }
377
- }),
378
- check: (mutants) => Effect.gen(function* () {
379
- const nodes = yield* compiler.nodes.pipe(Effect.mapError((cause) => new CheckerFailed({
380
- checkerName: "typescript",
381
- mutantIds: mutants.map((m) => m.id),
382
- cause
383
- })));
384
- const result = emptyCheckResultMap();
385
- for (const mutant of mutants) result.set(mutant.id, { status: CheckStatus.Passed });
386
- const firstMutant = mutants[0];
387
- if (!firstMutant || !nodes.get(normalizeFileName(firstMutant.fileName))) return result;
388
- const errorsMap = emptyDiagnosticMap();
389
- yield* checkErrors(mutants, errorsMap, nodes);
390
- for (const [id, errors] of errorsMap.entries()) {
391
- const text = yield* createErrorText(errors);
392
- result.set(id, {
393
- status: CheckStatus.CompileError,
394
- reason: text
395
- });
396
- }
397
- return result;
398
- }),
399
- group: (mutants) => Effect.gen(function* () {
400
- const nodes = yield* compiler.nodes.pipe(Effect.mapError((cause) => new CheckerFailed({
401
- checkerName: "typescript",
402
- mutantIds: mutants.map((m) => m.id),
403
- cause
404
- })));
405
- const { inside, outside } = partitionMutantsForGrouping(mutants, nodes, getPrioritize(options));
406
- if (inside.length === 0) return mutants.map((m) => [m.id]);
407
- const groups = createGroups([...inside], nodes);
408
- if (outside.length > 0) return [outside.map((m) => m.id), ...groups];
409
- return groups;
410
- })
411
- };
412
- }
165
+ };
166
+ const checkMutants = Workflow.make(CheckMutantsInput, (input) => buildResult(input));
413
167
  //#endregion
414
- //#region src/compiler-error.schema.ts
415
- const TypeId = "~stryker/typescript-checker/CompilerFailed";
416
- /**
417
- * Every way the TypeScript compiler can fail while serving a check.
418
- *
419
- * One tagged error rather than four, because nothing branches on which of these
420
- * happened — they all reach the caller as a failed check. The `reason` keeps the
421
- * cases distinguishable in a report without inventing four types no consumer
422
- * discriminates.
168
+ //#region src/Compiler.schema.ts
169
+ /**
170
+ * Compiler — declarations for the TypeScript compiler and version guard.
423
171
  */
424
- var CompilerFailed = class extends S.TaggedError(TypeId)("CompilerFailed", {
425
- reason: S.Literals([
426
- "not-initialized",
427
- "no-projects",
428
- "unknown-file-node",
429
- "file-not-in-project"
430
- ]),
431
- /** The file or tsconfig the failure is about, when it is about one. */
432
- subject: S.optional(S.String)
433
- }) {
434
- [TypeId] = TypeId;
172
+ /** The installed TypeScript version is below the supported floor. */
173
+ var UnsupportedTypeScriptVersionError = class extends Schema.TaggedError()("UnsupportedTypeScriptVersionError", { version: Schema.String }) {
435
174
  get message() {
436
- switch (this.reason) {
437
- case "not-initialized": return "The TypeScript compiler was used before it was initialized";
438
- case "no-projects": return `No projects were found for ${this.subject ?? "the tsconfig"}`;
439
- case "unknown-file-node": return `The file graph has no node for '${this.subject ?? "a file"}', which should not happen`;
440
- case "file-not-in-project": return `'${this.subject ?? "a file"}' is part of your TypeScript project but could not be found on disk`;
441
- }
175
+ return `@systemfsoftware/stryker-js-typescript-checker only supports typescript@7.0.0 or higher. Found typescript@${this.version}`;
442
176
  }
443
177
  };
444
- //#endregion
445
- //#region src/declaration-source-mapping.ts
446
- const findSourceMapRegex = /\/\/# sourceMappingURL=(.+)$/m;
447
- function getSourceMappingURL(content) {
448
- findSourceMapRegex.lastIndex = 0;
449
- return findSourceMapRegex.exec(content)?.[1];
450
- }
178
+ /** Requested file is not present in the hybrid in-memory file map. */
179
+ var HybridFileNotFoundError = class extends Schema.TaggedError()("HybridFileNotFoundError", { fileName: Schema.String }) {};
451
180
  //#endregion
452
181
  //#region ../../../../../node_modules/.pnpm/@jsr+std__jsonc@1.0.2/node_modules/@jsr/std__jsonc/parse.js
453
182
  /**
@@ -669,34 +398,43 @@ function buildErrorMessage({ type, sourceText, position }) {
669
398
  return `Cannot parse JSONC: unexpected token "${token}" in JSONC at position ${position}`;
670
399
  }
671
400
  //#endregion
672
- //#region src/tsconfig.schema.ts
401
+ //#region src/Tsconfig.schema.ts
673
402
  /**
674
- * Error returned when a tsconfig file fails to parse or does not match the
675
- * shape this package consumes.
403
+ * Tsconfig declarations for the TypeScript configuration consumed by the checker.
404
+ *
405
+ * Typed by Effect Schema and decoded at the boundary; the compiler capability
406
+ * consumes only validated shapes.
676
407
  */
408
+ /** The configured tsconfig failed to parse or is not a shape this package can consume. */
677
409
  var TsConfigParseError = class extends Schema.TaggedError()("TsConfigParseError", {
678
410
  file: Schema.String,
679
411
  reason: Schema.String
680
412
  }) {};
681
- /**
682
- * The configured tsconfig file could not be read from disk.
683
- */
413
+ /** The configured tsconfig file could not be read. */
684
414
  var TsConfigNotFoundError = class extends Schema.TaggedError()("TsConfigNotFoundError", { file: Schema.String }) {
685
415
  get message() {
686
416
  return `The tsconfig file does not exist at: "${this.file}". Please configure the tsconfig file in your stryker.conf file using "tsconfigFile"`;
687
417
  }
688
418
  };
689
- const JsonRecord = Schema.Record(Schema.String, Schema.Unknown);
690
- const TsConfigSchema = Schema.StructWithRest(Schema.Struct({
691
- references: Schema.optional(Schema.Array(Schema.StructWithRest(Schema.Struct({ path: Schema.String }), [JsonRecord]))),
692
- compilerOptions: Schema.optional(JsonRecord)
693
- }), [JsonRecord]);
419
+ const TsConfigSchema = Schema.Struct({
420
+ references: Schema.optional(Schema.Array(Schema.Struct({ path: Schema.String }))),
421
+ compilerOptions: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
422
+ });
694
423
  //#endregion
695
- //#region src/tsconfig.ts
424
+ //#region src/Tsconfig.ts
425
+ /**
426
+ * Tsconfig — capability for reading and normalizing TypeScript project configs.
427
+ *
428
+ * Normalizes via Effect Schema and tightens compilation options for mutation
429
+ * checking (disabling quality checks, toggling emit for build-mode vs
430
+ * single-project).
431
+ */
432
+ const normalizeFileName$2 = (fileName) => fileName.replace(/\\/g, "/");
696
433
  const COMPILER_OPTIONS_OVERRIDES = Object.freeze({
697
434
  allowUnreachableCode: true,
698
435
  noUnusedLocals: false,
699
- noUnusedParameters: false
436
+ noUnusedParameters: false,
437
+ skipLibCheck: true
700
438
  });
701
439
  const NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT = Object.freeze({
702
440
  noEmit: true,
@@ -708,7 +446,8 @@ const LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES = Object.freeze({
708
446
  emitDeclarationOnly: true,
709
447
  noEmit: false,
710
448
  declarationMap: true,
711
- declaration: true
449
+ declaration: true,
450
+ composite: true
712
451
  });
713
452
  /**
714
453
  * Parses the raw text of a tsconfig file into a typed config, rejecting shapes this package cannot consume.
@@ -723,16 +462,21 @@ function parseTsConfig(fileName, jsonText) {
723
462
  reason: error.message
724
463
  }));
725
464
  } catch (error) {
465
+ let reason;
466
+ if (error instanceof Error) reason = error.message;
467
+ else if (typeof error === "string") reason = error;
468
+ else {
469
+ const stringified = JSON.stringify(error);
470
+ if (stringified.length === 0) reason = "a non-Error value was thrown";
471
+ else reason = stringified;
472
+ }
726
473
  return Result.fail(new TsConfigParseError({
727
474
  file: fileName,
728
- reason: error instanceof Error ? error.message : typeof error === "string" ? error : JSON.stringify(error) ?? "a non-Error value was thrown"
475
+ reason
729
476
  }));
730
477
  }
731
478
  }
732
- /**
733
- * Determines whether or not to use `--build` mode based on "references" being there in the config file
734
- * @param tsconfigFileName The tsconfig file to parse
735
- */
479
+ /** Whether `--build` mode should be enabled based on `references` in the tsconfig. */
736
480
  const determineBuildModeEnabled = (tsconfigFileName, fsService) => Effect.gen(function* () {
737
481
  const parsed = parseTsConfig(tsconfigFileName, yield* fsService.readFileString(tsconfigFileName));
738
482
  return Result.match(parsed, {
@@ -741,15 +485,17 @@ const determineBuildModeEnabled = (tsconfigFileName, fsService) => Effect.gen(fu
741
485
  });
742
486
  });
743
487
  /**
744
- * Overrides some options to speed up compilation and disable some code quality checks we don't want during mutation testing
745
- * @param config The parsed config file
746
- * @param useBuildMode whether or not `--build` mode is used
488
+ * Overrides compiler options to speed up compilation and disable code quality
489
+ * checks irrelevant during mutation testing.
747
490
  */
748
491
  function overrideOptions(config, useBuildMode) {
492
+ let extraOptions;
493
+ if (useBuildMode) extraOptions = LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES;
494
+ else extraOptions = NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT;
749
495
  const compilerOptions = {
750
496
  ...config.compilerOptions,
751
497
  ...COMPILER_OPTIONS_OVERRIDES,
752
- ...useBuildMode ? LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES : NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT
498
+ ...extraOptions
753
499
  };
754
500
  if (!useBuildMode && compilerOptions["declarationDir"] !== void 0 && compilerOptions["declarationDir"] !== null) delete compilerOptions["declarationDir"];
755
501
  if (useBuildMode) {
@@ -759,35 +505,40 @@ function overrideOptions(config, useBuildMode) {
759
505
  delete compilerOptions["sourceRoot"];
760
506
  delete compilerOptions["outFile"];
761
507
  }
762
- return JSON.stringify({
508
+ if (useBuildMode) return JSON.stringify({
763
509
  ...config,
764
510
  compilerOptions
765
511
  });
512
+ const { references: _references, ...withoutReferences } = config;
513
+ return JSON.stringify({
514
+ ...withoutReferences,
515
+ compilerOptions
516
+ });
766
517
  }
767
518
  /**
768
- * Retrieves the referenced config files based on parsed configuration
769
- * @param config The parsed config file
770
- * @param fromDirName The directory where to resolve from
519
+ * Retrieves the referenced config files based on parsed configuration.
771
520
  */
772
521
  function retrieveReferencedProjects(config, fromDirName, pathService) {
773
522
  return (config.references ?? []).map((reference) => {
774
523
  let resolved = pathService.resolve(fromDirName, reference.path);
775
524
  if (!pathService.basename(resolved).endsWith(".json")) resolved = pathService.join(resolved, "tsconfig.json");
776
- return normalizeFileName(resolved);
525
+ return normalizeFileName$2(resolved);
777
526
  });
778
527
  }
779
528
  //#endregion
780
- //#region src/typescript-version.schema.ts
529
+ //#region src/Compiler.ts
781
530
  /**
782
- * The installed TypeScript version is below the supported floor.
531
+ * Compiler capability that hosts the TypeScript language service, the
532
+ * in-memory file system, and the file-graph used for grouping.
533
+ *
534
+ * All `typescript/unstable/*` interaction is confined here; callers consume
535
+ * only the Effect-typed service surface.
783
536
  */
784
- var UnsupportedTypeScriptVersionError = class extends Schema.TaggedError()("UnsupportedTypeScriptVersionError", { version: Schema.String }) {
785
- get message() {
786
- return `@systemfsoftware/stryker-js-typescript-checker only supports typescript@7.0.0 or higher. Found typescript@${this.version}`;
787
- }
788
- };
789
- //#endregion
790
- //#region src/typescript-version.ts
537
+ const normalizeFileName$1 = (fileName) => fileName.replace(/\\/g, "/");
538
+ const findSourceMapRegex = /\/\/# sourceMappingURL=(.+)$/m;
539
+ function getSourceMappingURL(content) {
540
+ return findSourceMapRegex.exec(content)?.[1];
541
+ }
791
542
  let cachedTSVersion;
792
543
  const getTSVersion = (fsService) => Effect.gen(function* () {
793
544
  if (cachedTSVersion !== void 0) return cachedTSVersion;
@@ -800,13 +551,8 @@ const getTSVersion = (fsService) => Effect.gen(function* () {
800
551
  return version;
801
552
  });
802
553
  /**
803
- * Whether a TypeScript version satisfies the supported floor `>=7.0.0`.
804
- *
805
- * Prerelease handling: the suffix after `-` (e.g. `7.0.0-beta`, `7.0.0-rc.1`) and any
806
- * `+` build metadata is stripped before numeric comparison, so a `7.0.0` prerelease
807
- * compares as `7.0.0` and satisfies the floor. This preserves the prior
808
- * `satisfies(version, '>=7.0.0', { includePrerelease: true })` behaviour
809
- * where a TypeScript 7 prerelease must still pass the guard.
554
+ * Whether a TypeScript version satisfies `>=7.0.0`. Pre-release suffixes are
555
+ * stripped so `7.0.0-beta` compares as `7.0.0`.
810
556
  */
811
557
  function isSupportedTypescriptVersion(version) {
812
558
  const dashBase = version.split("-")[0] ?? version;
@@ -823,25 +569,213 @@ const guardTSVersion = (fsService) => Effect.gen(function* () {
823
569
  const version = yield* getTSVersion(fsService);
824
570
  if (!isSupportedTypescriptVersion(version)) return yield* new UnsupportedTypeScriptVersionError({ version });
825
571
  });
826
- //#endregion
827
- //#region src/typescript-compiler.ts
572
+ function makeScriptFile(content, fileName, modifiedTime = /* @__PURE__ */ new Date()) {
573
+ return {
574
+ content,
575
+ fileName,
576
+ originalContent: content,
577
+ modifiedTime
578
+ };
579
+ }
580
+ function withContent(file, content) {
581
+ return {
582
+ ...file,
583
+ content,
584
+ modifiedTime: /* @__PURE__ */ new Date()
585
+ };
586
+ }
587
+ function mutateScriptFile(file, mutant) {
588
+ const start = getOffset(file, mutant.location.start);
589
+ const end = getOffset(file, mutant.location.end);
590
+ const content = `${file.originalContent.slice(0, start)}${mutant.replacement}${file.originalContent.slice(end)}`;
591
+ return {
592
+ ...file,
593
+ content,
594
+ modifiedTime: /* @__PURE__ */ new Date()
595
+ };
596
+ }
597
+ function resetScriptFile(file) {
598
+ return {
599
+ ...file,
600
+ content: file.originalContent,
601
+ modifiedTime: /* @__PURE__ */ new Date()
602
+ };
603
+ }
604
+ function getOffset(file, pos) {
605
+ const lines = file.originalContent.split("\n");
606
+ const lineCount = Math.min(pos.line, lines.length);
607
+ let offset = 0;
608
+ for (let i = 0; i < lineCount; i++) {
609
+ const line = lines[i];
610
+ if (line === void 0) break;
611
+ offset += line.length + 1;
612
+ }
613
+ offset += pos.column;
614
+ return offset;
615
+ }
616
+ const makeEmptyFilesMap = () => MutableHashMap.empty();
617
+ const makeEmptyOverridesMap = () => MutableHashMap.empty();
618
+ const setInPlace = (map, key, value) => {
619
+ MutableHashMap.set(map, key, value);
620
+ return map;
621
+ };
622
+ const makeHybridFileSystem = (fsService) => Effect.gen(function* () {
623
+ const filesRef = yield* Ref.make(makeEmptyFilesMap());
624
+ const overridesRef = yield* Ref.make(makeEmptyOverridesMap());
625
+ const fileNameIsBuildInfo = (fileName) => fileName.endsWith(".tsbuildinfo");
626
+ const fileSystem = {
627
+ readFile: (fileName) => {
628
+ const normalized = normalizeFileName$1(fileName);
629
+ if (fileNameIsBuildInfo(normalized)) return null;
630
+ const overrideOpt = MutableHashMap.get(overridesRef.ref.current, normalized);
631
+ if (Option.isSome(overrideOpt)) return overrideOpt.value;
632
+ const files = filesRef.ref.current;
633
+ if (MutableHashMap.has(files, normalized)) {
634
+ const fileOpt = MutableHashMap.get(files, normalized);
635
+ if (Option.isSome(fileOpt)) {
636
+ const file = fileOpt.value;
637
+ if (file !== void 0) return file.content;
638
+ return null;
639
+ }
640
+ }
641
+ },
642
+ fileExists: (fileName) => {
643
+ const normalized = normalizeFileName$1(fileName);
644
+ if (fileNameIsBuildInfo(normalized)) return false;
645
+ if (MutableHashMap.has(overridesRef.ref.current, normalized)) return true;
646
+ const files = filesRef.ref.current;
647
+ if (MutableHashMap.has(files, normalized)) {
648
+ const opt = MutableHashMap.get(files, normalized);
649
+ if (Option.isSome(opt)) return opt.value !== void 0;
650
+ return false;
651
+ }
652
+ },
653
+ directoryExists: () => void 0,
654
+ getAccessibleEntries: () => void 0,
655
+ realpath: () => void 0
656
+ };
657
+ const getFile = (fileName) => Effect.gen(function* () {
658
+ const normalized = normalizeFileName$1(fileName);
659
+ const files = yield* Ref.get(filesRef);
660
+ if (MutableHashMap.has(files, normalized)) {
661
+ const opt = MutableHashMap.get(files, normalized);
662
+ if (Option.isSome(opt)) return opt.value;
663
+ }
664
+ const content = yield* fsService.readFileString(normalized).pipe(Effect.orElseSucceed(() => void 0));
665
+ if (content === void 0) {
666
+ yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, void 0));
667
+ return;
668
+ }
669
+ const file = makeScriptFile(content, normalized);
670
+ yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, file));
671
+ return file;
672
+ });
673
+ const writeFile = (fileName, data) => Effect.gen(function* () {
674
+ const normalized = normalizeFileName$1(fileName);
675
+ const files = yield* Ref.get(filesRef);
676
+ const existingOpt = MutableHashMap.get(files, normalized);
677
+ let existing = void 0;
678
+ if (Option.isSome(existingOpt)) existing = existingOpt.value;
679
+ if (existing !== void 0) {
680
+ const next = withContent(existing, data);
681
+ yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, next));
682
+ } else {
683
+ const file = makeScriptFile(data, normalized);
684
+ yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, file));
685
+ }
686
+ });
687
+ const mutateFile = (fileName, mutant) => Effect.gen(function* () {
688
+ const file = yield* getFile(fileName);
689
+ if (file === void 0) return yield* new HybridFileNotFoundError({ fileName });
690
+ const next = mutateScriptFile(file, mutant);
691
+ const normalized = normalizeFileName$1(fileName);
692
+ yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, next));
693
+ });
694
+ const resetFile = (fileName) => Effect.gen(function* () {
695
+ const normalized = normalizeFileName$1(fileName);
696
+ const files = yield* Ref.get(filesRef);
697
+ const opt = MutableHashMap.get(files, normalized);
698
+ let file = void 0;
699
+ if (Option.isSome(opt)) file = opt.value;
700
+ if (file !== void 0) {
701
+ const next = resetScriptFile(file);
702
+ yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, next));
703
+ }
704
+ });
705
+ const existsInMemory = (fileName) => Effect.gen(function* () {
706
+ const files = yield* Ref.get(filesRef);
707
+ const opt = MutableHashMap.get(files, normalizeFileName$1(fileName));
708
+ let file = void 0;
709
+ if (Option.isSome(opt)) file = opt.value;
710
+ return file !== void 0;
711
+ });
712
+ const setTsConfigOverrides = (overrides) => Ref.set(overridesRef, overrides);
713
+ return {
714
+ fileSystem,
715
+ getFile,
716
+ writeFile,
717
+ mutateFile,
718
+ resetFile,
719
+ existsInMemory,
720
+ setTsConfigOverrides
721
+ };
722
+ });
723
+ function makeTSFileNode(fileName) {
724
+ return {
725
+ fileName,
726
+ parents: [],
727
+ children: []
728
+ };
729
+ }
730
+ function getAllParentReferencesIncludingSelf(node, allParentReferences = MutableHashSet.empty()) {
731
+ MutableHashSet.add(allParentReferences, node);
732
+ for (const parent of node.parents) if (!MutableHashSet.has(allParentReferences, parent)) getAllParentReferencesIncludingSelf(parent, allParentReferences);
733
+ return allParentReferences;
734
+ }
735
+ function createGroups(mutants, nodes) {
736
+ const groups = [];
737
+ const mutantsToGroup = MutableHashSet.fromIterable(mutants);
738
+ while (MutableHashSet.size(mutantsToGroup) > 0) {
739
+ const group = [];
740
+ const groupNodes = MutableHashSet.empty();
741
+ const nodesToIgnore = MutableHashSet.empty();
742
+ for (const currentMutant of mutantsToGroup) {
743
+ const currentNode = findNode(currentMutant.fileName, nodes);
744
+ if (!MutableHashSet.has(nodesToIgnore, currentNode) && !parentsHaveOverlapWith(currentNode, groupNodes)) {
745
+ group.push(currentMutant.id);
746
+ MutableHashSet.add(groupNodes, currentNode);
747
+ MutableHashSet.remove(mutantsToGroup, currentMutant);
748
+ addRangeOfNodesToSet(nodesToIgnore, getAllParentReferencesIncludingSelf(currentNode));
749
+ }
750
+ }
751
+ groups.push(group);
752
+ }
753
+ return groups;
754
+ }
755
+ function addRangeOfNodesToSet(nodes, nodesToAdd) {
756
+ for (const parent of nodesToAdd) MutableHashSet.add(nodes, parent);
757
+ }
758
+ function findNode(fileName, nodes) {
759
+ const nodeOption = MutableHashMap.get(nodes, normalizeFileName$1(fileName));
760
+ if (Option.isSome(nodeOption)) return nodeOption.value;
761
+ const fallbackOption = MutableHashMap.get(nodes, fileName);
762
+ if (Option.isSome(fallbackOption)) return fallbackOption.value;
763
+ throw new Error(`Node not in graph: ${fileName}`);
764
+ }
765
+ function parentsHaveOverlapWith(currentNode, groupNodes) {
766
+ for (const parentNode of getAllParentReferencesIncludingSelf(currentNode)) if (MutableHashSet.has(groupNodes, parentNode)) return true;
767
+ return false;
768
+ }
828
769
  var TypeScriptCompiler = class extends Context.Service()("@systemfsoftware/stryker-js-typescript-checker/TypeScriptCompiler") {};
829
- const emptySourceFiles = () => /* @__PURE__ */ new Map();
830
- const emptyNodes = () => /* @__PURE__ */ new Map();
831
- const emptyStringMap = () => /* @__PURE__ */ new Map();
832
- const emptyStringSet = () => /* @__PURE__ */ new Set();
833
- const setFromArray = (arr) => new Set(arr);
834
- const unique = (arr) => [...new Set(arr)];
835
- const cloneMap = (m) => new Map(m);
836
770
  const makeDummy = Effect.gen(function* () {
837
771
  const stateRef = yield* Ref.make({
838
772
  api: void 0,
839
773
  snapshot: void 0,
840
- sourceFiles: emptySourceFiles(),
841
- nodes: emptyNodes(),
774
+ sourceFiles: MutableHashMap.empty(),
775
+ nodes: MutableHashMap.empty(),
842
776
  lastMutants: [],
843
777
  lastMutatedFileNames: [],
844
- allTSConfigFiles: setFromArray(["tsconfig.json"]),
778
+ allTSConfigFiles: MutableHashSet.fromIterable(["tsconfig.json"]),
845
779
  tsconfigFile: "tsconfig.json"
846
780
  });
847
781
  yield* Effect.addFinalizer(() => Effect.gen(function* () {
@@ -869,15 +803,15 @@ const makeDummy = Effect.gen(function* () {
869
803
  Layer.effect(TypeScriptCompiler)(makeDummy);
870
804
  function makeTypescriptCompiler(options, fs, fsService, pathService) {
871
805
  if (!S.is(StrykerOptionsSchema)(options)) throw new Error("Invalid StrykerOptions");
872
- const rawTsconfigFile = normalizeFileName(options.tsconfigFile);
806
+ const rawTsconfigFile = normalizeFileName$1(options.tsconfigFile);
873
807
  const initialState = {
874
808
  api: void 0,
875
809
  snapshot: void 0,
876
- sourceFiles: emptySourceFiles(),
877
- nodes: emptyNodes(),
810
+ sourceFiles: MutableHashMap.empty(),
811
+ nodes: MutableHashMap.empty(),
878
812
  lastMutants: [],
879
813
  lastMutatedFileNames: [],
880
- allTSConfigFiles: setFromArray([rawTsconfigFile]),
814
+ allTSConfigFiles: MutableHashSet.fromIterable([rawTsconfigFile]),
881
815
  tsconfigFile: rawTsconfigFile
882
816
  };
883
817
  const stateRef = Ref.makeUnsafe(initialState);
@@ -897,30 +831,30 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
897
831
  });
898
832
  const collectAllTSConfigFiles = (buildModeEnabled) => Effect.gen(function* () {
899
833
  const s = yield* Ref.get(stateRef);
900
- const tsConfigOverrides = emptyStringMap();
834
+ const tsConfigOverrides = MutableHashMap.empty();
901
835
  const toProcess = [s.tsconfigFile];
902
- const processed = emptyStringSet();
836
+ const processed = MutableHashSet.empty();
903
837
  while (toProcess.length > 0) {
904
838
  const current = toProcess.pop();
905
- if (!current || processed.has(current)) continue;
906
- processed.add(current);
839
+ if (current === void 0 || current === "" || MutableHashSet.has(processed, current)) continue;
840
+ MutableHashSet.add(processed, current);
907
841
  const content = yield* fsService.readFileString(current);
908
842
  const parsed = parseTsConfig(current, content);
909
843
  if (Result.isFailure(parsed)) {
910
- tsConfigOverrides.set(current, content);
844
+ MutableHashMap.set(tsConfigOverrides, current, content);
911
845
  continue;
912
846
  }
913
- tsConfigOverrides.set(current, overrideOptions(parsed.success, buildModeEnabled));
847
+ MutableHashMap.set(tsConfigOverrides, current, overrideOptions(parsed.success, buildModeEnabled));
914
848
  for (const referenced of retrieveReferencedProjects(parsed.success, pathService.dirname(current), pathService)) {
915
- const normalized = normalizeFileName(referenced);
916
- s.allTSConfigFiles.add(normalized);
849
+ const normalized = normalizeFileName$1(referenced);
850
+ MutableHashSet.add(s.allTSConfigFiles, normalized);
917
851
  toProcess.push(referenced);
918
852
  }
919
853
  }
920
854
  yield* fs.setTsConfigOverrides(tsConfigOverrides);
921
855
  yield* Ref.update(stateRef, (prev) => ({
922
856
  ...prev,
923
- allTSConfigFiles: setFromArray([...s.allTSConfigFiles])
857
+ allTSConfigFiles: MutableHashSet.fromIterable(s.allTSConfigFiles)
924
858
  }));
925
859
  });
926
860
  const extractImports = (sourceFile) => {
@@ -977,17 +911,17 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
977
911
  const cleaned = specifier.replace(/^['"]|['"]$/g, "");
978
912
  if (!cleaned.startsWith("./") && !cleaned.startsWith("../")) return;
979
913
  const baseDir = pathService.dirname(sourceFileName);
980
- const resolved = normalizeFileName(pathService.resolve(baseDir, cleaned));
914
+ const resolved = normalizeFileName$1(pathService.resolve(baseDir, cleaned));
981
915
  const candidates = getResolutionCandidates(resolved, pathService);
982
- for (const candidate of candidates) if (sourceFiles.has(candidate)) return candidate;
916
+ for (const candidate of candidates) if (MutableHashMap.has(sourceFiles, candidate)) return candidate;
983
917
  };
984
918
  const resolveTSInputFile = (dependencyFileName, pathService) => {
985
919
  if (!dependencyFileName.endsWith(".d.ts")) return dependencyFileName;
986
920
  const content = fs.fileSystem.readFile?.(dependencyFileName);
987
921
  if (typeof content !== "string") return dependencyFileName;
988
922
  const sourceMappingURL = getSourceMappingURL(content);
989
- if (!sourceMappingURL) return dependencyFileName;
990
- const sourceMapFileName = normalizeFileName(pathService.resolve(pathService.dirname(dependencyFileName), sourceMappingURL));
923
+ if (sourceMappingURL === void 0 || sourceMappingURL === "") return dependencyFileName;
924
+ const sourceMapFileName = normalizeFileName$1(pathService.resolve(pathService.dirname(dependencyFileName), sourceMappingURL));
991
925
  const sourceMapContent = fs.fileSystem.readFile?.(sourceMapFileName);
992
926
  if (typeof sourceMapContent !== "string") return dependencyFileName;
993
927
  const rawMap = JSON.parse(sourceMapContent);
@@ -996,7 +930,7 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
996
930
  if (sources?.length === 1) {
997
931
  const sourcePath = sources[0];
998
932
  if (sourcePath === void 0) return dependencyFileName;
999
- return normalizeFileName(pathService.resolve(pathService.dirname(sourceMapFileName), sourcePath));
933
+ return normalizeFileName$1(pathService.resolve(pathService.dirname(sourceMapFileName), sourcePath));
1000
934
  }
1001
935
  return dependencyFileName;
1002
936
  };
@@ -1004,10 +938,10 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
1004
938
  const s = yield* Ref.get(stateRef);
1005
939
  for (const program of programs) for (const fileName of program.getSourceFileNames()) {
1006
940
  if (fileName.endsWith(".d.ts") || fileName.includes("node_modules")) continue;
1007
- const normalized = normalizeFileName(fileName);
1008
- s.sourceFiles.set(normalized, {
941
+ const normalized = normalizeFileName$1(fileName);
942
+ MutableHashMap.set(s.sourceFiles, normalized, {
1009
943
  fileName: normalized,
1010
- imports: emptyStringSet()
944
+ imports: MutableHashSet.empty()
1011
945
  });
1012
946
  }
1013
947
  for (const [fileName] of s.sourceFiles) {
@@ -1016,54 +950,58 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
1016
950
  const imports = extractImports(sourceFile);
1017
951
  for (const specifier of imports) {
1018
952
  const resolved = resolveModuleSpecifier(fileName, specifier, s.sourceFiles, pathService);
1019
- if (resolved) {
953
+ if (resolved !== void 0 && resolved !== "") {
1020
954
  const sourceFileName = resolveTSInputFile(resolved, pathService);
1021
- if (s.sourceFiles.has(sourceFileName)) s.sourceFiles.get(fileName)?.imports.add(sourceFileName);
955
+ if (MutableHashMap.has(s.sourceFiles, sourceFileName)) {
956
+ const entryOpt = MutableHashMap.get(s.sourceFiles, fileName);
957
+ if (Option.isSome(entryOpt)) MutableHashSet.add(entryOpt.value.imports, sourceFileName);
958
+ }
1022
959
  }
1023
960
  }
1024
961
  }
1025
962
  yield* Ref.update(stateRef, (prev) => ({
1026
963
  ...prev,
1027
- sourceFiles: cloneMap(s.sourceFiles)
964
+ sourceFiles: MutableHashMap.fromIterable(s.sourceFiles)
1028
965
  }));
1029
966
  });
1030
967
  const getNodesEffect = Effect.gen(function* () {
1031
968
  const s = yield* Ref.get(stateRef);
1032
- if (s.nodes.size > 0) return s.nodes;
969
+ if (MutableHashMap.size(s.nodes) > 0) return s.nodes;
1033
970
  for (const [fileName] of s.sourceFiles) {
1034
971
  const node = makeTSFileNode(fileName);
1035
- s.nodes.set(fileName, node);
972
+ MutableHashMap.set(s.nodes, fileName, node);
1036
973
  }
1037
- const withChildren = emptyNodes();
974
+ const withChildren = MutableHashMap.empty();
1038
975
  for (const [fileName, file] of s.sourceFiles) {
1039
- const node = s.nodes.get(fileName);
1040
- if (node == null) return yield* new CompilerFailed({
976
+ const nodeOpt = MutableHashMap.get(s.nodes, fileName);
977
+ if (Option.isNone(nodeOpt)) return yield* new CompilerFailed({
1041
978
  reason: "unknown-file-node",
1042
979
  subject: fileName
1043
980
  });
1044
- const children = [...file.imports].map((importName) => s.nodes.get(importName)).filter((n) => n !== void 0);
1045
- withChildren.set(fileName, {
981
+ const node = nodeOpt.value;
982
+ const children = Array.from(file.imports).map((importName) => Option.getOrUndefined(MutableHashMap.get(s.nodes, importName))).filter((n) => n !== void 0);
983
+ MutableHashMap.set(withChildren, fileName, {
1046
984
  ...node,
1047
985
  children,
1048
986
  parents: []
1049
987
  });
1050
988
  }
1051
- s.nodes.clear();
1052
- for (const [k, v] of withChildren) s.nodes.set(k, v);
1053
- const withParents = emptyNodes();
989
+ MutableHashMap.clear(s.nodes);
990
+ for (const [k, v] of withChildren) MutableHashMap.set(s.nodes, k, v);
991
+ const withParents = MutableHashMap.empty();
1054
992
  for (const [fileName, node] of s.nodes) {
1055
993
  const parents = [];
1056
994
  for (const [, n] of s.nodes) if (n.children.includes(node)) parents.push(n);
1057
- withParents.set(fileName, {
995
+ MutableHashMap.set(withParents, fileName, {
1058
996
  ...node,
1059
997
  parents
1060
998
  });
1061
999
  }
1062
- s.nodes.clear();
1063
- for (const [k, v] of withParents) s.nodes.set(k, v);
1000
+ MutableHashMap.clear(s.nodes);
1001
+ for (const [k, v] of withParents) MutableHashMap.set(s.nodes, k, v);
1064
1002
  yield* Ref.update(stateRef, (prev) => ({
1065
1003
  ...prev,
1066
- nodes: cloneMap(s.nodes)
1004
+ nodes: MutableHashMap.fromIterable(s.nodes)
1067
1005
  }));
1068
1006
  return s.nodes;
1069
1007
  });
@@ -1077,13 +1015,13 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
1077
1015
  });
1078
1016
  yield* fs.mutateFile(mutant.fileName, mutant);
1079
1017
  }
1080
- const mutatedFileNames = unique(mutants.map((m) => normalizeFileName(m.fileName)));
1081
- const changedFiles = unique([...state.lastMutatedFileNames, ...mutatedFileNames]);
1018
+ const mutatedFileNames = Array.from(MutableHashSet.fromIterable(mutants.map((m) => normalizeFileName$1(m.fileName))));
1019
+ const changedFiles = Array.from(MutableHashSet.fromIterable([...state.lastMutatedFileNames, ...mutatedFileNames]));
1082
1020
  const current = yield* Ref.get(stateRef);
1083
1021
  if (current.api && current.snapshot) {
1084
1022
  const oldSnapshot = current.snapshot;
1085
1023
  const nextSnapshot = current.api.updateSnapshot({
1086
- openProjects: [...current.allTSConfigFiles],
1024
+ openProjects: Array.from(current.allTSConfigFiles),
1087
1025
  fileChanges: { changed: changedFiles }
1088
1026
  });
1089
1027
  yield* Effect.sync(() => oldSnapshot.dispose());
@@ -1105,18 +1043,18 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
1105
1043
  });
1106
1044
  const init = Effect.gen(function* () {
1107
1045
  yield* guardTSVersion(fsService);
1108
- const absoluteTsconfigFile = normalizeFileName(pathService.resolve(rawTsconfigFile));
1046
+ const absoluteTsconfigFile = normalizeFileName$1(pathService.resolve(rawTsconfigFile));
1109
1047
  yield* Ref.update(stateRef, (prev) => ({
1110
1048
  ...prev,
1111
1049
  tsconfigFile: absoluteTsconfigFile,
1112
- allTSConfigFiles: setFromArray([absoluteTsconfigFile])
1050
+ allTSConfigFiles: MutableHashSet.fromIterable([absoluteTsconfigFile])
1113
1051
  }));
1114
1052
  yield* guardTSConfigFileExistsEffect;
1115
1053
  const buildModeEnabled = yield* determineBuildModeEnabled(absoluteTsconfigFile, fsService);
1116
1054
  yield* collectAllTSConfigFiles(buildModeEnabled);
1117
1055
  const s = yield* Ref.get(stateRef);
1118
1056
  const api = new API({ fs: fs.fileSystem });
1119
- const snapshot = api.updateSnapshot({ openProjects: [...s.allTSConfigFiles] });
1057
+ const snapshot = api.updateSnapshot({ openProjects: Array.from(s.allTSConfigFiles) });
1120
1058
  yield* Ref.update(stateRef, (prev) => ({
1121
1059
  ...prev,
1122
1060
  api,
@@ -1152,8 +1090,148 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
1152
1090
  };
1153
1091
  }
1154
1092
  //#endregion
1093
+ //#region src/Checker.ts
1094
+ /**
1095
+ * Checker — capability that validates mutants against the TypeScript compiler.
1096
+ *
1097
+ * Bridges the checker plugin protocol (`@systemfsoftware/stryker-js/Checker`)
1098
+ * to the compiler service and the pure `checkMutants` workflow. Diagnostics
1099
+ * are classified without I/O; the file graph is sourced from the compiler.
1100
+ */
1101
+ const normalizeFileName = (fileName) => fileName.replace(/\\/g, "/");
1102
+ /**
1103
+ * Pure grouping decision: separates mutants inside the project graph from
1104
+ * those outside it, honouring `prioritizePerformanceOverAccuracy`.
1105
+ */
1106
+ function partitionMutantsForGrouping(mutants, nodes, prioritizePerformanceOverAccuracy) {
1107
+ if (!prioritizePerformanceOverAccuracy) return {
1108
+ inside: [],
1109
+ outside: [...mutants]
1110
+ };
1111
+ const outside = [];
1112
+ const inside = [];
1113
+ for (const m of mutants) if (Option.isNone(MutableHashMap.get(nodes, normalizeFileName(m.fileName)))) outside.push(m);
1114
+ else inside.push(m);
1115
+ return {
1116
+ inside,
1117
+ outside
1118
+ };
1119
+ }
1120
+ function getPrioritize(options) {
1121
+ if (!Predicate.hasProperty(options, "typescriptChecker")) return false;
1122
+ const tc = options["typescriptChecker"];
1123
+ if (typeof tc !== "object" || tc === null) return false;
1124
+ if (!Predicate.hasProperty(tc, "prioritizePerformanceOverAccuracy")) return false;
1125
+ const val = tc["prioritizePerformanceOverAccuracy"];
1126
+ if (typeof val === "boolean") return val;
1127
+ return false;
1128
+ }
1129
+ const makeCheckDescription = (compiler) => pipe(Cell.read((command) => Effect.gen(function* () {
1130
+ const nodesHm = yield* compiler.nodes.pipe(Effect.mapError((cause) => new CheckerFailed({
1131
+ checkerName: "typescript",
1132
+ mutantIds: command.mutants.map((m) => m.id),
1133
+ cause: errorToString(cause)
1134
+ })));
1135
+ const nodes = {};
1136
+ for (const [k, v] of nodesHm) nodes[k] = v;
1137
+ const diagnostics = yield* compiler.check([...command.mutants]).pipe(Effect.mapError((cause) => new CheckerFailed({
1138
+ checkerName: "typescript",
1139
+ mutantIds: command.mutants.map((m) => m.id),
1140
+ cause: errorToString(cause)
1141
+ })));
1142
+ return new CheckMutantsInput({
1143
+ mutants: [...command.mutants],
1144
+ diagnostics: [...diagnostics],
1145
+ nodes
1146
+ });
1147
+ })), Cell.decode((raw) => Result.succeed(raw)), Cell.decide(checkMutants), Cell.encode((outcome) => outcome), Cell.write((outcome) => Result.match(outcome, {
1148
+ onFailure: (failure) => Effect.fail(new CheckerFailed({
1149
+ checkerName: "typescript",
1150
+ mutantIds: [],
1151
+ cause: errorToString(failure)
1152
+ })),
1153
+ onSuccess: (decision) => Effect.succeed(decision)
1154
+ })));
1155
+ function makeCheckerService({ options, compiler }) {
1156
+ const formatDiagnostic = (error) => Effect.gen(function* () {
1157
+ let severity;
1158
+ if (error.category === DiagnosticCategory.Error) severity = "error";
1159
+ else if (error.category === DiagnosticCategory.Warning) severity = "warning";
1160
+ else if (error.category === DiagnosticCategory.Suggestion) severity = "suggestion";
1161
+ else severity = "message";
1162
+ let location = "";
1163
+ const unknownError = error;
1164
+ if (typeof unknownError === "object" && unknownError !== null && "fileName" in unknownError && typeof unknownError.fileName === "string") {
1165
+ const fileName = unknownError.fileName;
1166
+ const lineAndCharacter = yield* compiler.getLineAndCharacterOfPosition(fileName, error.pos).pipe(Effect.orElseSucceed(() => void 0));
1167
+ location = `${fileName}(${(lineAndCharacter?.line ?? 0) + 1},${(lineAndCharacter?.character ?? 0) + 1}): `;
1168
+ } else if (error.fileName !== void 0 && error.fileName !== "") {
1169
+ const lineAndCharacter = yield* compiler.getLineAndCharacterOfPosition(error.fileName, error.pos).pipe(Effect.orElseSucceed(() => void 0));
1170
+ const line = (lineAndCharacter?.line ?? 0) + 1;
1171
+ const character = (lineAndCharacter?.character ?? 0) + 1;
1172
+ location = `${error.fileName}(${line},${character}): `;
1173
+ }
1174
+ return `${location}${severity} TS${error.code}: ${error.text}`;
1175
+ });
1176
+ const createErrorText = (errors) => Effect.gen(function* () {
1177
+ return (yield* Effect.forEach(errors, formatDiagnostic)).join(EOL);
1178
+ });
1179
+ return {
1180
+ init: Effect.gen(function* () {
1181
+ const errors = yield* compiler.init.pipe(Effect.mapError((cause) => new CheckerFailed({
1182
+ checkerName: "typescript",
1183
+ mutantIds: [],
1184
+ cause: errorToString(cause)
1185
+ })));
1186
+ if (errors.length > 0) {
1187
+ const text = yield* createErrorText(errors);
1188
+ return yield* new CheckerFailed({
1189
+ checkerName: "typescript",
1190
+ mutantIds: [],
1191
+ cause: errorToString(/* @__PURE__ */ new Error(`Typescript error(s) found in dry run compilation: ${text}`))
1192
+ });
1193
+ }
1194
+ }),
1195
+ check: (mutants) => Effect.gen(function* () {
1196
+ const description = makeCheckDescription(compiler);
1197
+ const applyOnce = (group) => Cell.apply(description, new CheckMutantsCommand({ mutants: [...group] }));
1198
+ const first = yield* applyOnce(mutants);
1199
+ let map = HashMap.empty();
1200
+ const mergeResults = (results) => {
1201
+ for (const [id, value] of Object.entries(results)) if (value.status === "passed") map = HashMap.set(map, id, { status: "passed" });
1202
+ else map = HashMap.set(map, id, {
1203
+ status: "compileError",
1204
+ reason: value.reason
1205
+ });
1206
+ };
1207
+ mergeResults(first.results);
1208
+ if (first.needsRetest.length > 0) yield* applyOnce([]);
1209
+ const originals = {};
1210
+ for (const m of mutants) originals[m.id] = m;
1211
+ for (const pending of first.needsRetest) {
1212
+ const original = originals[pending.id];
1213
+ if (original === void 0) continue;
1214
+ mergeResults((yield* applyOnce([original])).results);
1215
+ }
1216
+ return map;
1217
+ }),
1218
+ group: (mutants) => Effect.gen(function* () {
1219
+ const nodes = yield* compiler.nodes.pipe(Effect.mapError((cause) => new CheckerFailed({
1220
+ checkerName: "typescript",
1221
+ mutantIds: mutants.map((m) => m.id),
1222
+ cause: errorToString(cause)
1223
+ })));
1224
+ const { inside, outside } = partitionMutantsForGrouping(mutants, nodes, getPrioritize(options));
1225
+ if (inside.length === 0) return mutants.map((m) => [m.id]);
1226
+ const groups = createGroups([...inside], nodes);
1227
+ if (outside.length > 0) return [outside.map((m) => m.id), ...groups];
1228
+ return groups;
1229
+ })
1230
+ };
1231
+ }
1232
+ //#endregion
1155
1233
  //#region src/index.ts
1156
- const strykerPlugins = [declarePlugin(PluginKind.Checker, "typescript", Layer.effect(Checker, Effect.gen(function* () {
1234
+ const strykerPlugins = [declarePlugin("Checker", "typescript", Layer.effect(Checker, Effect.gen(function* () {
1157
1235
  const options = yield* RunConfiguration;
1158
1236
  const fsService = yield* FileSystem.FileSystem;
1159
1237
  const pathService = yield* Path.Path;