@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/CHANGELOG.md +62 -0
- package/dist/index.d.mts +1 -2
- package/dist/index.mjs +588 -510
- package/package.json +9 -4
package/dist/index.mjs
CHANGED
|
@@ -1,453 +1,182 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
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
|
|
32
|
+
var CheckMutantsCommand = class extends S.TaggedClass()("CheckMutantsCommand", { mutants: S.Array(Mutant) }) {};
|
|
183
33
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
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/
|
|
57
|
+
//#region src/Checker.workflow.ts
|
|
194
58
|
/**
|
|
195
|
-
*
|
|
196
|
-
* which mutants are definitely responsible and which need an individual
|
|
197
|
-
* re-check.
|
|
59
|
+
* Checker — pure check decision.
|
|
198
60
|
*
|
|
199
|
-
*
|
|
200
|
-
* (
|
|
201
|
-
*
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
111
|
+
fileName
|
|
220
112
|
}));
|
|
221
113
|
const related = getMutantsWithReferenceToChildrenOrSelf(node, [...mutants]);
|
|
222
|
-
if (related.length === 0) for (const m of mutants) needsRetest
|
|
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
|
|
227
|
-
if (existing) existing.push(diagnostic);
|
|
228
|
-
else definitive
|
|
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
|
|
122
|
+
} else for (const m of related) needsRetest[m.id] = m;
|
|
231
123
|
}
|
|
232
|
-
const filteredRetest =
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
363
|
-
|
|
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/
|
|
415
|
-
|
|
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
|
-
|
|
425
|
-
|
|
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
|
-
|
|
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
|
-
|
|
445
|
-
|
|
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/
|
|
401
|
+
//#region src/Tsconfig.schema.ts
|
|
673
402
|
/**
|
|
674
|
-
*
|
|
675
|
-
*
|
|
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
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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/
|
|
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
|
|
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
|
|
745
|
-
*
|
|
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
|
-
...
|
|
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/
|
|
529
|
+
//#region src/Compiler.ts
|
|
781
530
|
/**
|
|
782
|
-
*
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
|
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
|
-
|
|
827
|
-
|
|
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:
|
|
841
|
-
nodes:
|
|
774
|
+
sourceFiles: MutableHashMap.empty(),
|
|
775
|
+
nodes: MutableHashMap.empty(),
|
|
842
776
|
lastMutants: [],
|
|
843
777
|
lastMutatedFileNames: [],
|
|
844
|
-
allTSConfigFiles:
|
|
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:
|
|
877
|
-
nodes:
|
|
810
|
+
sourceFiles: MutableHashMap.empty(),
|
|
811
|
+
nodes: MutableHashMap.empty(),
|
|
878
812
|
lastMutants: [],
|
|
879
813
|
lastMutatedFileNames: [],
|
|
880
|
-
allTSConfigFiles:
|
|
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 =
|
|
834
|
+
const tsConfigOverrides = MutableHashMap.empty();
|
|
901
835
|
const toProcess = [s.tsconfigFile];
|
|
902
|
-
const processed =
|
|
836
|
+
const processed = MutableHashSet.empty();
|
|
903
837
|
while (toProcess.length > 0) {
|
|
904
838
|
const current = toProcess.pop();
|
|
905
|
-
if (
|
|
906
|
-
|
|
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
|
-
|
|
844
|
+
MutableHashMap.set(tsConfigOverrides, current, content);
|
|
911
845
|
continue;
|
|
912
846
|
}
|
|
913
|
-
|
|
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
|
|
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:
|
|
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 (
|
|
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 (
|
|
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
|
|
941
|
+
const normalized = normalizeFileName$1(fileName);
|
|
942
|
+
MutableHashMap.set(s.sourceFiles, normalized, {
|
|
1009
943
|
fileName: normalized,
|
|
1010
|
-
imports:
|
|
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 (
|
|
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:
|
|
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
|
|
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
|
|
972
|
+
MutableHashMap.set(s.nodes, fileName, node);
|
|
1036
973
|
}
|
|
1037
|
-
const withChildren =
|
|
974
|
+
const withChildren = MutableHashMap.empty();
|
|
1038
975
|
for (const [fileName, file] of s.sourceFiles) {
|
|
1039
|
-
const
|
|
1040
|
-
if (
|
|
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
|
|
1045
|
-
|
|
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
|
|
1052
|
-
for (const [k, v] of withChildren) s.nodes
|
|
1053
|
-
const withParents =
|
|
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
|
-
|
|
995
|
+
MutableHashMap.set(withParents, fileName, {
|
|
1058
996
|
...node,
|
|
1059
997
|
parents
|
|
1060
998
|
});
|
|
1061
999
|
}
|
|
1062
|
-
s.nodes
|
|
1063
|
-
for (const [k, v] of withParents) s.nodes
|
|
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:
|
|
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 =
|
|
1081
|
-
const changedFiles =
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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;
|