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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,453 +1,167 @@
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 the
63
+ * pass/compileError map 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);
129
+ };
130
+ const buildResult = (input) => {
131
+ const mutants = input.mutants;
132
+ const diagnostics = input.diagnostics;
133
+ const nodes = input.nodes;
134
+ const result = {};
135
+ for (const m of mutants) result[m.id] = { status: "passed" };
136
+ if (mutants.length === 0) return Result$1.succeed(result);
137
+ const first = mutants[0];
138
+ if (first === void 0 || nodes[normalizeFileName$3(first.fileName)] === void 0) return Result$1.succeed(result);
139
+ const classified = classifyDiagnosticsPure(diagnostics, mutants, nodes);
140
+ if (Result$1.isFailure(classified)) return Result$1.fail(classified.failure);
141
+ const { definitive } = classified.success;
142
+ for (const id of Object.keys(definitive)) {
143
+ const diags = definitive[id];
144
+ if (diags !== void 0) result[id] = {
145
+ status: "compileError",
146
+ reason: diags.map((d) => d.text).join("\n")
147
+ };
287
148
  }
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);
328
- });
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
- }
361
- });
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
- }
149
+ return Result$1.succeed(result);
150
+ };
151
+ const checkMutants = Workflow.make(CheckMutantsInput, (input) => buildResult(input));
413
152
  //#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.
153
+ //#region src/Compiler.schema.ts
154
+ /**
155
+ * Compiler — declarations for the TypeScript compiler and version guard.
423
156
  */
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;
157
+ /** The installed TypeScript version is below the supported floor. */
158
+ var UnsupportedTypeScriptVersionError = class extends Schema.TaggedError()("UnsupportedTypeScriptVersionError", { version: Schema.String }) {
435
159
  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
- }
160
+ return `@systemfsoftware/stryker-js-typescript-checker only supports typescript@7.0.0 or higher. Found typescript@${this.version}`;
442
161
  }
443
162
  };
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
- }
163
+ /** Requested file is not present in the hybrid in-memory file map. */
164
+ var HybridFileNotFoundError = class extends Schema.TaggedError()("HybridFileNotFoundError", { fileName: Schema.String }) {};
451
165
  //#endregion
452
166
  //#region ../../../../../node_modules/.pnpm/@jsr+std__jsonc@1.0.2/node_modules/@jsr/std__jsonc/parse.js
453
167
  /**
@@ -669,34 +383,43 @@ function buildErrorMessage({ type, sourceText, position }) {
669
383
  return `Cannot parse JSONC: unexpected token "${token}" in JSONC at position ${position}`;
670
384
  }
671
385
  //#endregion
672
- //#region src/tsconfig.schema.ts
386
+ //#region src/Tsconfig.schema.ts
673
387
  /**
674
- * Error returned when a tsconfig file fails to parse or does not match the
675
- * shape this package consumes.
388
+ * Tsconfig declarations for the TypeScript configuration consumed by the checker.
389
+ *
390
+ * Typed by Effect Schema and decoded at the boundary; the compiler capability
391
+ * consumes only validated shapes.
676
392
  */
393
+ /** The configured tsconfig failed to parse or is not a shape this package can consume. */
677
394
  var TsConfigParseError = class extends Schema.TaggedError()("TsConfigParseError", {
678
395
  file: Schema.String,
679
396
  reason: Schema.String
680
397
  }) {};
681
- /**
682
- * The configured tsconfig file could not be read from disk.
683
- */
398
+ /** The configured tsconfig file could not be read. */
684
399
  var TsConfigNotFoundError = class extends Schema.TaggedError()("TsConfigNotFoundError", { file: Schema.String }) {
685
400
  get message() {
686
401
  return `The tsconfig file does not exist at: "${this.file}". Please configure the tsconfig file in your stryker.conf file using "tsconfigFile"`;
687
402
  }
688
403
  };
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]);
404
+ const TsConfigSchema = Schema.Struct({
405
+ references: Schema.optional(Schema.Array(Schema.Struct({ path: Schema.String }))),
406
+ compilerOptions: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
407
+ });
694
408
  //#endregion
695
- //#region src/tsconfig.ts
409
+ //#region src/Tsconfig.ts
410
+ /**
411
+ * Tsconfig — capability for reading and normalizing TypeScript project configs.
412
+ *
413
+ * Normalizes via Effect Schema and tightens compilation options for mutation
414
+ * checking (disabling quality checks, toggling emit for build-mode vs
415
+ * single-project).
416
+ */
417
+ const normalizeFileName$2 = (fileName) => fileName.replace(/\\/g, "/");
696
418
  const COMPILER_OPTIONS_OVERRIDES = Object.freeze({
697
419
  allowUnreachableCode: true,
698
420
  noUnusedLocals: false,
699
- noUnusedParameters: false
421
+ noUnusedParameters: false,
422
+ skipLibCheck: true
700
423
  });
701
424
  const NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT = Object.freeze({
702
425
  noEmit: true,
@@ -708,7 +431,8 @@ const LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES = Object.freeze({
708
431
  emitDeclarationOnly: true,
709
432
  noEmit: false,
710
433
  declarationMap: true,
711
- declaration: true
434
+ declaration: true,
435
+ composite: true
712
436
  });
713
437
  /**
714
438
  * Parses the raw text of a tsconfig file into a typed config, rejecting shapes this package cannot consume.
@@ -723,16 +447,21 @@ function parseTsConfig(fileName, jsonText) {
723
447
  reason: error.message
724
448
  }));
725
449
  } catch (error) {
450
+ let reason;
451
+ if (error instanceof Error) reason = error.message;
452
+ else if (typeof error === "string") reason = error;
453
+ else {
454
+ const stringified = JSON.stringify(error);
455
+ if (stringified.length === 0) reason = "a non-Error value was thrown";
456
+ else reason = stringified;
457
+ }
726
458
  return Result.fail(new TsConfigParseError({
727
459
  file: fileName,
728
- reason: error instanceof Error ? error.message : typeof error === "string" ? error : JSON.stringify(error) ?? "a non-Error value was thrown"
460
+ reason
729
461
  }));
730
462
  }
731
463
  }
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
- */
464
+ /** Whether `--build` mode should be enabled based on `references` in the tsconfig. */
736
465
  const determineBuildModeEnabled = (tsconfigFileName, fsService) => Effect.gen(function* () {
737
466
  const parsed = parseTsConfig(tsconfigFileName, yield* fsService.readFileString(tsconfigFileName));
738
467
  return Result.match(parsed, {
@@ -741,15 +470,17 @@ const determineBuildModeEnabled = (tsconfigFileName, fsService) => Effect.gen(fu
741
470
  });
742
471
  });
743
472
  /**
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
473
+ * Overrides compiler options to speed up compilation and disable code quality
474
+ * checks irrelevant during mutation testing.
747
475
  */
748
476
  function overrideOptions(config, useBuildMode) {
477
+ let extraOptions;
478
+ if (useBuildMode) extraOptions = LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES;
479
+ else extraOptions = NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT;
749
480
  const compilerOptions = {
750
481
  ...config.compilerOptions,
751
482
  ...COMPILER_OPTIONS_OVERRIDES,
752
- ...useBuildMode ? LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES : NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT
483
+ ...extraOptions
753
484
  };
754
485
  if (!useBuildMode && compilerOptions["declarationDir"] !== void 0 && compilerOptions["declarationDir"] !== null) delete compilerOptions["declarationDir"];
755
486
  if (useBuildMode) {
@@ -759,35 +490,40 @@ function overrideOptions(config, useBuildMode) {
759
490
  delete compilerOptions["sourceRoot"];
760
491
  delete compilerOptions["outFile"];
761
492
  }
762
- return JSON.stringify({
493
+ if (useBuildMode) return JSON.stringify({
763
494
  ...config,
764
495
  compilerOptions
765
496
  });
497
+ const { references: _references, ...withoutReferences } = config;
498
+ return JSON.stringify({
499
+ ...withoutReferences,
500
+ compilerOptions
501
+ });
766
502
  }
767
503
  /**
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
504
+ * Retrieves the referenced config files based on parsed configuration.
771
505
  */
772
506
  function retrieveReferencedProjects(config, fromDirName, pathService) {
773
507
  return (config.references ?? []).map((reference) => {
774
508
  let resolved = pathService.resolve(fromDirName, reference.path);
775
509
  if (!pathService.basename(resolved).endsWith(".json")) resolved = pathService.join(resolved, "tsconfig.json");
776
- return normalizeFileName(resolved);
510
+ return normalizeFileName$2(resolved);
777
511
  });
778
512
  }
779
513
  //#endregion
780
- //#region src/typescript-version.schema.ts
514
+ //#region src/Compiler.ts
781
515
  /**
782
- * The installed TypeScript version is below the supported floor.
516
+ * Compiler capability that hosts the TypeScript language service, the
517
+ * in-memory file system, and the file-graph used for grouping.
518
+ *
519
+ * All `typescript/unstable/*` interaction is confined here; callers consume
520
+ * only the Effect-typed service surface.
783
521
  */
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
522
+ const normalizeFileName$1 = (fileName) => fileName.replace(/\\/g, "/");
523
+ const findSourceMapRegex = /\/\/# sourceMappingURL=(.+)$/m;
524
+ function getSourceMappingURL(content) {
525
+ return findSourceMapRegex.exec(content)?.[1];
526
+ }
791
527
  let cachedTSVersion;
792
528
  const getTSVersion = (fsService) => Effect.gen(function* () {
793
529
  if (cachedTSVersion !== void 0) return cachedTSVersion;
@@ -800,13 +536,8 @@ const getTSVersion = (fsService) => Effect.gen(function* () {
800
536
  return version;
801
537
  });
802
538
  /**
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.
539
+ * Whether a TypeScript version satisfies `>=7.0.0`. Pre-release suffixes are
540
+ * stripped so `7.0.0-beta` compares as `7.0.0`.
810
541
  */
811
542
  function isSupportedTypescriptVersion(version) {
812
543
  const dashBase = version.split("-")[0] ?? version;
@@ -823,25 +554,213 @@ const guardTSVersion = (fsService) => Effect.gen(function* () {
823
554
  const version = yield* getTSVersion(fsService);
824
555
  if (!isSupportedTypescriptVersion(version)) return yield* new UnsupportedTypeScriptVersionError({ version });
825
556
  });
826
- //#endregion
827
- //#region src/typescript-compiler.ts
557
+ function makeScriptFile(content, fileName, modifiedTime = /* @__PURE__ */ new Date()) {
558
+ return {
559
+ content,
560
+ fileName,
561
+ originalContent: content,
562
+ modifiedTime
563
+ };
564
+ }
565
+ function withContent(file, content) {
566
+ return {
567
+ ...file,
568
+ content,
569
+ modifiedTime: /* @__PURE__ */ new Date()
570
+ };
571
+ }
572
+ function mutateScriptFile(file, mutant) {
573
+ const start = getOffset(file, mutant.location.start);
574
+ const end = getOffset(file, mutant.location.end);
575
+ const content = `${file.originalContent.slice(0, start)}${mutant.replacement}${file.originalContent.slice(end)}`;
576
+ return {
577
+ ...file,
578
+ content,
579
+ modifiedTime: /* @__PURE__ */ new Date()
580
+ };
581
+ }
582
+ function resetScriptFile(file) {
583
+ return {
584
+ ...file,
585
+ content: file.originalContent,
586
+ modifiedTime: /* @__PURE__ */ new Date()
587
+ };
588
+ }
589
+ function getOffset(file, pos) {
590
+ const lines = file.originalContent.split("\n");
591
+ const lineCount = Math.min(pos.line, lines.length);
592
+ let offset = 0;
593
+ for (let i = 0; i < lineCount; i++) {
594
+ const line = lines[i];
595
+ if (line === void 0) break;
596
+ offset += line.length + 1;
597
+ }
598
+ offset += pos.column;
599
+ return offset;
600
+ }
601
+ const makeEmptyFilesMap = () => MutableHashMap.empty();
602
+ const makeEmptyOverridesMap = () => MutableHashMap.empty();
603
+ const setInPlace = (map, key, value) => {
604
+ MutableHashMap.set(map, key, value);
605
+ return map;
606
+ };
607
+ const makeHybridFileSystem = (fsService) => Effect.gen(function* () {
608
+ const filesRef = yield* Ref.make(makeEmptyFilesMap());
609
+ const overridesRef = yield* Ref.make(makeEmptyOverridesMap());
610
+ const fileNameIsBuildInfo = (fileName) => fileName.endsWith(".tsbuildinfo");
611
+ const fileSystem = {
612
+ readFile: (fileName) => {
613
+ const normalized = normalizeFileName$1(fileName);
614
+ if (fileNameIsBuildInfo(normalized)) return null;
615
+ const overrideOpt = MutableHashMap.get(overridesRef.ref.current, normalized);
616
+ if (Option.isSome(overrideOpt)) return overrideOpt.value;
617
+ const files = filesRef.ref.current;
618
+ if (MutableHashMap.has(files, normalized)) {
619
+ const fileOpt = MutableHashMap.get(files, normalized);
620
+ if (Option.isSome(fileOpt)) {
621
+ const file = fileOpt.value;
622
+ if (file !== void 0) return file.content;
623
+ return null;
624
+ }
625
+ }
626
+ },
627
+ fileExists: (fileName) => {
628
+ const normalized = normalizeFileName$1(fileName);
629
+ if (fileNameIsBuildInfo(normalized)) return false;
630
+ if (MutableHashMap.has(overridesRef.ref.current, normalized)) return true;
631
+ const files = filesRef.ref.current;
632
+ if (MutableHashMap.has(files, normalized)) {
633
+ const opt = MutableHashMap.get(files, normalized);
634
+ if (Option.isSome(opt)) return opt.value !== void 0;
635
+ return false;
636
+ }
637
+ },
638
+ directoryExists: () => void 0,
639
+ getAccessibleEntries: () => void 0,
640
+ realpath: () => void 0
641
+ };
642
+ const getFile = (fileName) => Effect.gen(function* () {
643
+ const normalized = normalizeFileName$1(fileName);
644
+ const files = yield* Ref.get(filesRef);
645
+ if (MutableHashMap.has(files, normalized)) {
646
+ const opt = MutableHashMap.get(files, normalized);
647
+ if (Option.isSome(opt)) return opt.value;
648
+ }
649
+ const content = yield* fsService.readFileString(normalized).pipe(Effect.orElseSucceed(() => void 0));
650
+ if (content === void 0) {
651
+ yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, void 0));
652
+ return;
653
+ }
654
+ const file = makeScriptFile(content, normalized);
655
+ yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, file));
656
+ return file;
657
+ });
658
+ const writeFile = (fileName, data) => Effect.gen(function* () {
659
+ const normalized = normalizeFileName$1(fileName);
660
+ const files = yield* Ref.get(filesRef);
661
+ const existingOpt = MutableHashMap.get(files, normalized);
662
+ let existing = void 0;
663
+ if (Option.isSome(existingOpt)) existing = existingOpt.value;
664
+ if (existing !== void 0) {
665
+ const next = withContent(existing, data);
666
+ yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, next));
667
+ } else {
668
+ const file = makeScriptFile(data, normalized);
669
+ yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, file));
670
+ }
671
+ });
672
+ const mutateFile = (fileName, mutant) => Effect.gen(function* () {
673
+ const file = yield* getFile(fileName);
674
+ if (file === void 0) return yield* new HybridFileNotFoundError({ fileName });
675
+ const next = mutateScriptFile(file, mutant);
676
+ const normalized = normalizeFileName$1(fileName);
677
+ yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, next));
678
+ });
679
+ const resetFile = (fileName) => Effect.gen(function* () {
680
+ const normalized = normalizeFileName$1(fileName);
681
+ const files = yield* Ref.get(filesRef);
682
+ const opt = MutableHashMap.get(files, normalized);
683
+ let file = void 0;
684
+ if (Option.isSome(opt)) file = opt.value;
685
+ if (file !== void 0) {
686
+ const next = resetScriptFile(file);
687
+ yield* Ref.update(filesRef, (m) => setInPlace(m, normalized, next));
688
+ }
689
+ });
690
+ const existsInMemory = (fileName) => Effect.gen(function* () {
691
+ const files = yield* Ref.get(filesRef);
692
+ const opt = MutableHashMap.get(files, normalizeFileName$1(fileName));
693
+ let file = void 0;
694
+ if (Option.isSome(opt)) file = opt.value;
695
+ return file !== void 0;
696
+ });
697
+ const setTsConfigOverrides = (overrides) => Ref.set(overridesRef, overrides);
698
+ return {
699
+ fileSystem,
700
+ getFile,
701
+ writeFile,
702
+ mutateFile,
703
+ resetFile,
704
+ existsInMemory,
705
+ setTsConfigOverrides
706
+ };
707
+ });
708
+ function makeTSFileNode(fileName) {
709
+ return {
710
+ fileName,
711
+ parents: [],
712
+ children: []
713
+ };
714
+ }
715
+ function getAllParentReferencesIncludingSelf(node, allParentReferences = MutableHashSet.empty()) {
716
+ MutableHashSet.add(allParentReferences, node);
717
+ for (const parent of node.parents) if (!MutableHashSet.has(allParentReferences, parent)) getAllParentReferencesIncludingSelf(parent, allParentReferences);
718
+ return allParentReferences;
719
+ }
720
+ function createGroups(mutants, nodes) {
721
+ const groups = [];
722
+ const mutantsToGroup = MutableHashSet.fromIterable(mutants);
723
+ while (MutableHashSet.size(mutantsToGroup) > 0) {
724
+ const group = [];
725
+ const groupNodes = MutableHashSet.empty();
726
+ const nodesToIgnore = MutableHashSet.empty();
727
+ for (const currentMutant of mutantsToGroup) {
728
+ const currentNode = findNode(currentMutant.fileName, nodes);
729
+ if (!MutableHashSet.has(nodesToIgnore, currentNode) && !parentsHaveOverlapWith(currentNode, groupNodes)) {
730
+ group.push(currentMutant.id);
731
+ MutableHashSet.add(groupNodes, currentNode);
732
+ MutableHashSet.remove(mutantsToGroup, currentMutant);
733
+ addRangeOfNodesToSet(nodesToIgnore, getAllParentReferencesIncludingSelf(currentNode));
734
+ }
735
+ }
736
+ groups.push(group);
737
+ }
738
+ return groups;
739
+ }
740
+ function addRangeOfNodesToSet(nodes, nodesToAdd) {
741
+ for (const parent of nodesToAdd) MutableHashSet.add(nodes, parent);
742
+ }
743
+ function findNode(fileName, nodes) {
744
+ const nodeOption = MutableHashMap.get(nodes, normalizeFileName$1(fileName));
745
+ if (Option.isSome(nodeOption)) return nodeOption.value;
746
+ const fallbackOption = MutableHashMap.get(nodes, fileName);
747
+ if (Option.isSome(fallbackOption)) return fallbackOption.value;
748
+ throw new Error(`Node not in graph: ${fileName}`);
749
+ }
750
+ function parentsHaveOverlapWith(currentNode, groupNodes) {
751
+ for (const parentNode of getAllParentReferencesIncludingSelf(currentNode)) if (MutableHashSet.has(groupNodes, parentNode)) return true;
752
+ return false;
753
+ }
828
754
  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
755
  const makeDummy = Effect.gen(function* () {
837
756
  const stateRef = yield* Ref.make({
838
757
  api: void 0,
839
758
  snapshot: void 0,
840
- sourceFiles: emptySourceFiles(),
841
- nodes: emptyNodes(),
759
+ sourceFiles: MutableHashMap.empty(),
760
+ nodes: MutableHashMap.empty(),
842
761
  lastMutants: [],
843
762
  lastMutatedFileNames: [],
844
- allTSConfigFiles: setFromArray(["tsconfig.json"]),
763
+ allTSConfigFiles: MutableHashSet.fromIterable(["tsconfig.json"]),
845
764
  tsconfigFile: "tsconfig.json"
846
765
  });
847
766
  yield* Effect.addFinalizer(() => Effect.gen(function* () {
@@ -869,15 +788,15 @@ const makeDummy = Effect.gen(function* () {
869
788
  Layer.effect(TypeScriptCompiler)(makeDummy);
870
789
  function makeTypescriptCompiler(options, fs, fsService, pathService) {
871
790
  if (!S.is(StrykerOptionsSchema)(options)) throw new Error("Invalid StrykerOptions");
872
- const rawTsconfigFile = normalizeFileName(options.tsconfigFile);
791
+ const rawTsconfigFile = normalizeFileName$1(options.tsconfigFile);
873
792
  const initialState = {
874
793
  api: void 0,
875
794
  snapshot: void 0,
876
- sourceFiles: emptySourceFiles(),
877
- nodes: emptyNodes(),
795
+ sourceFiles: MutableHashMap.empty(),
796
+ nodes: MutableHashMap.empty(),
878
797
  lastMutants: [],
879
798
  lastMutatedFileNames: [],
880
- allTSConfigFiles: setFromArray([rawTsconfigFile]),
799
+ allTSConfigFiles: MutableHashSet.fromIterable([rawTsconfigFile]),
881
800
  tsconfigFile: rawTsconfigFile
882
801
  };
883
802
  const stateRef = Ref.makeUnsafe(initialState);
@@ -897,30 +816,30 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
897
816
  });
898
817
  const collectAllTSConfigFiles = (buildModeEnabled) => Effect.gen(function* () {
899
818
  const s = yield* Ref.get(stateRef);
900
- const tsConfigOverrides = emptyStringMap();
819
+ const tsConfigOverrides = MutableHashMap.empty();
901
820
  const toProcess = [s.tsconfigFile];
902
- const processed = emptyStringSet();
821
+ const processed = MutableHashSet.empty();
903
822
  while (toProcess.length > 0) {
904
823
  const current = toProcess.pop();
905
- if (!current || processed.has(current)) continue;
906
- processed.add(current);
824
+ if (current === void 0 || current === "" || MutableHashSet.has(processed, current)) continue;
825
+ MutableHashSet.add(processed, current);
907
826
  const content = yield* fsService.readFileString(current);
908
827
  const parsed = parseTsConfig(current, content);
909
828
  if (Result.isFailure(parsed)) {
910
- tsConfigOverrides.set(current, content);
829
+ MutableHashMap.set(tsConfigOverrides, current, content);
911
830
  continue;
912
831
  }
913
- tsConfigOverrides.set(current, overrideOptions(parsed.success, buildModeEnabled));
832
+ MutableHashMap.set(tsConfigOverrides, current, overrideOptions(parsed.success, buildModeEnabled));
914
833
  for (const referenced of retrieveReferencedProjects(parsed.success, pathService.dirname(current), pathService)) {
915
- const normalized = normalizeFileName(referenced);
916
- s.allTSConfigFiles.add(normalized);
834
+ const normalized = normalizeFileName$1(referenced);
835
+ MutableHashSet.add(s.allTSConfigFiles, normalized);
917
836
  toProcess.push(referenced);
918
837
  }
919
838
  }
920
839
  yield* fs.setTsConfigOverrides(tsConfigOverrides);
921
840
  yield* Ref.update(stateRef, (prev) => ({
922
841
  ...prev,
923
- allTSConfigFiles: setFromArray([...s.allTSConfigFiles])
842
+ allTSConfigFiles: MutableHashSet.fromIterable(s.allTSConfigFiles)
924
843
  }));
925
844
  });
926
845
  const extractImports = (sourceFile) => {
@@ -977,17 +896,17 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
977
896
  const cleaned = specifier.replace(/^['"]|['"]$/g, "");
978
897
  if (!cleaned.startsWith("./") && !cleaned.startsWith("../")) return;
979
898
  const baseDir = pathService.dirname(sourceFileName);
980
- const resolved = normalizeFileName(pathService.resolve(baseDir, cleaned));
899
+ const resolved = normalizeFileName$1(pathService.resolve(baseDir, cleaned));
981
900
  const candidates = getResolutionCandidates(resolved, pathService);
982
- for (const candidate of candidates) if (sourceFiles.has(candidate)) return candidate;
901
+ for (const candidate of candidates) if (MutableHashMap.has(sourceFiles, candidate)) return candidate;
983
902
  };
984
903
  const resolveTSInputFile = (dependencyFileName, pathService) => {
985
904
  if (!dependencyFileName.endsWith(".d.ts")) return dependencyFileName;
986
905
  const content = fs.fileSystem.readFile?.(dependencyFileName);
987
906
  if (typeof content !== "string") return dependencyFileName;
988
907
  const sourceMappingURL = getSourceMappingURL(content);
989
- if (!sourceMappingURL) return dependencyFileName;
990
- const sourceMapFileName = normalizeFileName(pathService.resolve(pathService.dirname(dependencyFileName), sourceMappingURL));
908
+ if (sourceMappingURL === void 0 || sourceMappingURL === "") return dependencyFileName;
909
+ const sourceMapFileName = normalizeFileName$1(pathService.resolve(pathService.dirname(dependencyFileName), sourceMappingURL));
991
910
  const sourceMapContent = fs.fileSystem.readFile?.(sourceMapFileName);
992
911
  if (typeof sourceMapContent !== "string") return dependencyFileName;
993
912
  const rawMap = JSON.parse(sourceMapContent);
@@ -996,7 +915,7 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
996
915
  if (sources?.length === 1) {
997
916
  const sourcePath = sources[0];
998
917
  if (sourcePath === void 0) return dependencyFileName;
999
- return normalizeFileName(pathService.resolve(pathService.dirname(sourceMapFileName), sourcePath));
918
+ return normalizeFileName$1(pathService.resolve(pathService.dirname(sourceMapFileName), sourcePath));
1000
919
  }
1001
920
  return dependencyFileName;
1002
921
  };
@@ -1004,10 +923,10 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
1004
923
  const s = yield* Ref.get(stateRef);
1005
924
  for (const program of programs) for (const fileName of program.getSourceFileNames()) {
1006
925
  if (fileName.endsWith(".d.ts") || fileName.includes("node_modules")) continue;
1007
- const normalized = normalizeFileName(fileName);
1008
- s.sourceFiles.set(normalized, {
926
+ const normalized = normalizeFileName$1(fileName);
927
+ MutableHashMap.set(s.sourceFiles, normalized, {
1009
928
  fileName: normalized,
1010
- imports: emptyStringSet()
929
+ imports: MutableHashSet.empty()
1011
930
  });
1012
931
  }
1013
932
  for (const [fileName] of s.sourceFiles) {
@@ -1016,54 +935,58 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
1016
935
  const imports = extractImports(sourceFile);
1017
936
  for (const specifier of imports) {
1018
937
  const resolved = resolveModuleSpecifier(fileName, specifier, s.sourceFiles, pathService);
1019
- if (resolved) {
938
+ if (resolved !== void 0 && resolved !== "") {
1020
939
  const sourceFileName = resolveTSInputFile(resolved, pathService);
1021
- if (s.sourceFiles.has(sourceFileName)) s.sourceFiles.get(fileName)?.imports.add(sourceFileName);
940
+ if (MutableHashMap.has(s.sourceFiles, sourceFileName)) {
941
+ const entryOpt = MutableHashMap.get(s.sourceFiles, fileName);
942
+ if (Option.isSome(entryOpt)) MutableHashSet.add(entryOpt.value.imports, sourceFileName);
943
+ }
1022
944
  }
1023
945
  }
1024
946
  }
1025
947
  yield* Ref.update(stateRef, (prev) => ({
1026
948
  ...prev,
1027
- sourceFiles: cloneMap(s.sourceFiles)
949
+ sourceFiles: MutableHashMap.fromIterable(s.sourceFiles)
1028
950
  }));
1029
951
  });
1030
952
  const getNodesEffect = Effect.gen(function* () {
1031
953
  const s = yield* Ref.get(stateRef);
1032
- if (s.nodes.size > 0) return s.nodes;
954
+ if (MutableHashMap.size(s.nodes) > 0) return s.nodes;
1033
955
  for (const [fileName] of s.sourceFiles) {
1034
956
  const node = makeTSFileNode(fileName);
1035
- s.nodes.set(fileName, node);
957
+ MutableHashMap.set(s.nodes, fileName, node);
1036
958
  }
1037
- const withChildren = emptyNodes();
959
+ const withChildren = MutableHashMap.empty();
1038
960
  for (const [fileName, file] of s.sourceFiles) {
1039
- const node = s.nodes.get(fileName);
1040
- if (node == null) return yield* new CompilerFailed({
961
+ const nodeOpt = MutableHashMap.get(s.nodes, fileName);
962
+ if (Option.isNone(nodeOpt)) return yield* new CompilerFailed({
1041
963
  reason: "unknown-file-node",
1042
964
  subject: fileName
1043
965
  });
1044
- const children = [...file.imports].map((importName) => s.nodes.get(importName)).filter((n) => n !== void 0);
1045
- withChildren.set(fileName, {
966
+ const node = nodeOpt.value;
967
+ const children = Array.from(file.imports).map((importName) => Option.getOrUndefined(MutableHashMap.get(s.nodes, importName))).filter((n) => n !== void 0);
968
+ MutableHashMap.set(withChildren, fileName, {
1046
969
  ...node,
1047
970
  children,
1048
971
  parents: []
1049
972
  });
1050
973
  }
1051
- s.nodes.clear();
1052
- for (const [k, v] of withChildren) s.nodes.set(k, v);
1053
- const withParents = emptyNodes();
974
+ MutableHashMap.clear(s.nodes);
975
+ for (const [k, v] of withChildren) MutableHashMap.set(s.nodes, k, v);
976
+ const withParents = MutableHashMap.empty();
1054
977
  for (const [fileName, node] of s.nodes) {
1055
978
  const parents = [];
1056
979
  for (const [, n] of s.nodes) if (n.children.includes(node)) parents.push(n);
1057
- withParents.set(fileName, {
980
+ MutableHashMap.set(withParents, fileName, {
1058
981
  ...node,
1059
982
  parents
1060
983
  });
1061
984
  }
1062
- s.nodes.clear();
1063
- for (const [k, v] of withParents) s.nodes.set(k, v);
985
+ MutableHashMap.clear(s.nodes);
986
+ for (const [k, v] of withParents) MutableHashMap.set(s.nodes, k, v);
1064
987
  yield* Ref.update(stateRef, (prev) => ({
1065
988
  ...prev,
1066
- nodes: cloneMap(s.nodes)
989
+ nodes: MutableHashMap.fromIterable(s.nodes)
1067
990
  }));
1068
991
  return s.nodes;
1069
992
  });
@@ -1077,13 +1000,13 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
1077
1000
  });
1078
1001
  yield* fs.mutateFile(mutant.fileName, mutant);
1079
1002
  }
1080
- const mutatedFileNames = unique(mutants.map((m) => normalizeFileName(m.fileName)));
1081
- const changedFiles = unique([...state.lastMutatedFileNames, ...mutatedFileNames]);
1003
+ const mutatedFileNames = Array.from(MutableHashSet.fromIterable(mutants.map((m) => normalizeFileName$1(m.fileName))));
1004
+ const changedFiles = Array.from(MutableHashSet.fromIterable([...state.lastMutatedFileNames, ...mutatedFileNames]));
1082
1005
  const current = yield* Ref.get(stateRef);
1083
1006
  if (current.api && current.snapshot) {
1084
1007
  const oldSnapshot = current.snapshot;
1085
1008
  const nextSnapshot = current.api.updateSnapshot({
1086
- openProjects: [...current.allTSConfigFiles],
1009
+ openProjects: Array.from(current.allTSConfigFiles),
1087
1010
  fileChanges: { changed: changedFiles }
1088
1011
  });
1089
1012
  yield* Effect.sync(() => oldSnapshot.dispose());
@@ -1105,18 +1028,18 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
1105
1028
  });
1106
1029
  const init = Effect.gen(function* () {
1107
1030
  yield* guardTSVersion(fsService);
1108
- const absoluteTsconfigFile = normalizeFileName(pathService.resolve(rawTsconfigFile));
1031
+ const absoluteTsconfigFile = normalizeFileName$1(pathService.resolve(rawTsconfigFile));
1109
1032
  yield* Ref.update(stateRef, (prev) => ({
1110
1033
  ...prev,
1111
1034
  tsconfigFile: absoluteTsconfigFile,
1112
- allTSConfigFiles: setFromArray([absoluteTsconfigFile])
1035
+ allTSConfigFiles: MutableHashSet.fromIterable([absoluteTsconfigFile])
1113
1036
  }));
1114
1037
  yield* guardTSConfigFileExistsEffect;
1115
1038
  const buildModeEnabled = yield* determineBuildModeEnabled(absoluteTsconfigFile, fsService);
1116
1039
  yield* collectAllTSConfigFiles(buildModeEnabled);
1117
1040
  const s = yield* Ref.get(stateRef);
1118
1041
  const api = new API({ fs: fs.fileSystem });
1119
- const snapshot = api.updateSnapshot({ openProjects: [...s.allTSConfigFiles] });
1042
+ const snapshot = api.updateSnapshot({ openProjects: Array.from(s.allTSConfigFiles) });
1120
1043
  yield* Ref.update(stateRef, (prev) => ({
1121
1044
  ...prev,
1122
1045
  api,
@@ -1152,8 +1075,138 @@ function makeTypescriptCompiler(options, fs, fsService, pathService) {
1152
1075
  };
1153
1076
  }
1154
1077
  //#endregion
1078
+ //#region src/Checker.ts
1079
+ /**
1080
+ * Checker — capability that validates mutants against the TypeScript compiler.
1081
+ *
1082
+ * Bridges the checker plugin protocol (`@systemfsoftware/stryker-js/Checker`)
1083
+ * to the compiler service and the pure `checkMutants` workflow. Diagnostics
1084
+ * are classified without I/O; the file graph is sourced from the compiler.
1085
+ */
1086
+ const normalizeFileName = (fileName) => fileName.replace(/\\/g, "/");
1087
+ /**
1088
+ * Pure grouping decision: separates mutants inside the project graph from
1089
+ * those outside it, honouring `prioritizePerformanceOverAccuracy`.
1090
+ */
1091
+ function partitionMutantsForGrouping(mutants, nodes, prioritizePerformanceOverAccuracy) {
1092
+ if (!prioritizePerformanceOverAccuracy) return {
1093
+ inside: [],
1094
+ outside: [...mutants]
1095
+ };
1096
+ const outside = [];
1097
+ const inside = [];
1098
+ for (const m of mutants) if (Option.isNone(MutableHashMap.get(nodes, normalizeFileName(m.fileName)))) outside.push(m);
1099
+ else inside.push(m);
1100
+ return {
1101
+ inside,
1102
+ outside
1103
+ };
1104
+ }
1105
+ function getPrioritize(options) {
1106
+ if (!Predicate.hasProperty(options, "typescriptChecker")) return false;
1107
+ const tc = options["typescriptChecker"];
1108
+ if (typeof tc !== "object" || tc === null) return false;
1109
+ if (!Predicate.hasProperty(tc, "prioritizePerformanceOverAccuracy")) return false;
1110
+ const val = tc["prioritizePerformanceOverAccuracy"];
1111
+ if (typeof val === "boolean") return val;
1112
+ return false;
1113
+ }
1114
+ const makeCheckDescription = (compiler) => pipe(Cell.read((command) => Effect.gen(function* () {
1115
+ const nodesHm = yield* compiler.nodes.pipe(Effect.mapError((cause) => new CheckerFailed({
1116
+ checkerName: "typescript",
1117
+ mutantIds: command.mutants.map((m) => m.id),
1118
+ cause: errorToString(cause)
1119
+ })));
1120
+ const nodes = {};
1121
+ for (const [k, v] of nodesHm) nodes[k] = v;
1122
+ const diagnostics = yield* compiler.check([...command.mutants]).pipe(Effect.mapError((cause) => new CheckerFailed({
1123
+ checkerName: "typescript",
1124
+ mutantIds: command.mutants.map((m) => m.id),
1125
+ cause: errorToString(cause)
1126
+ })));
1127
+ return new CheckMutantsInput({
1128
+ mutants: [...command.mutants],
1129
+ diagnostics: [...diagnostics],
1130
+ nodes
1131
+ });
1132
+ })), Cell.decode((raw) => Result.succeed(raw)), Cell.decide(checkMutants), Cell.encode((outcome) => outcome), Cell.write((outcome) => Result.match(outcome, {
1133
+ onFailure: (failure) => Effect.fail(new CheckerFailed({
1134
+ checkerName: "typescript",
1135
+ mutantIds: [],
1136
+ cause: errorToString(failure)
1137
+ })),
1138
+ onSuccess: (record) => {
1139
+ let map = HashMap.empty();
1140
+ for (const [id, value] of Object.entries(record)) if (value.status === "passed") map = HashMap.set(map, id, { status: "passed" });
1141
+ else map = HashMap.set(map, id, {
1142
+ status: "compileError",
1143
+ reason: value.reason
1144
+ });
1145
+ return Effect.succeed(map);
1146
+ }
1147
+ })));
1148
+ function makeCheckerService({ options, compiler }) {
1149
+ const formatDiagnostic = (error) => Effect.gen(function* () {
1150
+ let severity;
1151
+ if (error.category === DiagnosticCategory.Error) severity = "error";
1152
+ else if (error.category === DiagnosticCategory.Warning) severity = "warning";
1153
+ else if (error.category === DiagnosticCategory.Suggestion) severity = "suggestion";
1154
+ else severity = "message";
1155
+ let location = "";
1156
+ const unknownError = error;
1157
+ if (typeof unknownError === "object" && unknownError !== null && "fileName" in unknownError && typeof unknownError.fileName === "string") {
1158
+ const fileName = unknownError.fileName;
1159
+ const lineAndCharacter = yield* compiler.getLineAndCharacterOfPosition(fileName, error.pos).pipe(Effect.orElseSucceed(() => void 0));
1160
+ location = `${fileName}(${(lineAndCharacter?.line ?? 0) + 1},${(lineAndCharacter?.character ?? 0) + 1}): `;
1161
+ } else if (error.fileName !== void 0 && error.fileName !== "") {
1162
+ const lineAndCharacter = yield* compiler.getLineAndCharacterOfPosition(error.fileName, error.pos).pipe(Effect.orElseSucceed(() => void 0));
1163
+ const line = (lineAndCharacter?.line ?? 0) + 1;
1164
+ const character = (lineAndCharacter?.character ?? 0) + 1;
1165
+ location = `${error.fileName}(${line},${character}): `;
1166
+ }
1167
+ return `${location}${severity} TS${error.code}: ${error.text}`;
1168
+ });
1169
+ const createErrorText = (errors) => Effect.gen(function* () {
1170
+ return (yield* Effect.forEach(errors, formatDiagnostic)).join(EOL);
1171
+ });
1172
+ return {
1173
+ init: Effect.gen(function* () {
1174
+ const errors = yield* compiler.init.pipe(Effect.mapError((cause) => new CheckerFailed({
1175
+ checkerName: "typescript",
1176
+ mutantIds: [],
1177
+ cause: errorToString(cause)
1178
+ })));
1179
+ if (errors.length > 0) {
1180
+ const text = yield* createErrorText(errors);
1181
+ return yield* new CheckerFailed({
1182
+ checkerName: "typescript",
1183
+ mutantIds: [],
1184
+ cause: errorToString(/* @__PURE__ */ new Error(`Typescript error(s) found in dry run compilation: ${text}`))
1185
+ });
1186
+ }
1187
+ }),
1188
+ check: (mutants) => Effect.gen(function* () {
1189
+ const command = new CheckMutantsCommand({ mutants: [...mutants] });
1190
+ const description = makeCheckDescription(compiler);
1191
+ return yield* Cell.apply(description, command);
1192
+ }),
1193
+ group: (mutants) => Effect.gen(function* () {
1194
+ const nodes = yield* compiler.nodes.pipe(Effect.mapError((cause) => new CheckerFailed({
1195
+ checkerName: "typescript",
1196
+ mutantIds: mutants.map((m) => m.id),
1197
+ cause: errorToString(cause)
1198
+ })));
1199
+ const { inside, outside } = partitionMutantsForGrouping(mutants, nodes, getPrioritize(options));
1200
+ if (inside.length === 0) return mutants.map((m) => [m.id]);
1201
+ const groups = createGroups([...inside], nodes);
1202
+ if (outside.length > 0) return [outside.map((m) => m.id), ...groups];
1203
+ return groups;
1204
+ })
1205
+ };
1206
+ }
1207
+ //#endregion
1155
1208
  //#region src/index.ts
1156
- const strykerPlugins = [declarePlugin(PluginKind.Checker, "typescript", Layer.effect(Checker, Effect.gen(function* () {
1209
+ const strykerPlugins = [declarePlugin("Checker", "typescript", Layer.effect(Checker, Effect.gen(function* () {
1157
1210
  const options = yield* RunConfiguration;
1158
1211
  const fsService = yield* FileSystem.FileSystem;
1159
1212
  const pathService = yield* Path.Path;