@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/CHANGELOG.md +54 -0
- package/dist/index.d.mts +1 -2
- package/dist/index.mjs +564 -511
- package/package.json +10 -5
package/dist/index.mjs
CHANGED
|
@@ -1,453 +1,167 @@
|
|
|
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 the
|
|
63
|
+
* pass/compileError map 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
|
-
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
};
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
289
|
-
}
|
|
290
|
-
|
|
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/
|
|
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.
|
|
153
|
+
//#region src/Compiler.schema.ts
|
|
154
|
+
/**
|
|
155
|
+
* Compiler — declarations for the TypeScript compiler and version guard.
|
|
423
156
|
*/
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
445
|
-
|
|
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/
|
|
386
|
+
//#region src/Tsconfig.schema.ts
|
|
673
387
|
/**
|
|
674
|
-
*
|
|
675
|
-
*
|
|
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
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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/
|
|
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
|
|
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
|
|
745
|
-
*
|
|
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
|
-
...
|
|
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/
|
|
514
|
+
//#region src/Compiler.ts
|
|
781
515
|
/**
|
|
782
|
-
*
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
|
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
|
-
|
|
827
|
-
|
|
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:
|
|
841
|
-
nodes:
|
|
759
|
+
sourceFiles: MutableHashMap.empty(),
|
|
760
|
+
nodes: MutableHashMap.empty(),
|
|
842
761
|
lastMutants: [],
|
|
843
762
|
lastMutatedFileNames: [],
|
|
844
|
-
allTSConfigFiles:
|
|
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:
|
|
877
|
-
nodes:
|
|
795
|
+
sourceFiles: MutableHashMap.empty(),
|
|
796
|
+
nodes: MutableHashMap.empty(),
|
|
878
797
|
lastMutants: [],
|
|
879
798
|
lastMutatedFileNames: [],
|
|
880
|
-
allTSConfigFiles:
|
|
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 =
|
|
819
|
+
const tsConfigOverrides = MutableHashMap.empty();
|
|
901
820
|
const toProcess = [s.tsconfigFile];
|
|
902
|
-
const processed =
|
|
821
|
+
const processed = MutableHashSet.empty();
|
|
903
822
|
while (toProcess.length > 0) {
|
|
904
823
|
const current = toProcess.pop();
|
|
905
|
-
if (
|
|
906
|
-
|
|
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
|
-
|
|
829
|
+
MutableHashMap.set(tsConfigOverrides, current, content);
|
|
911
830
|
continue;
|
|
912
831
|
}
|
|
913
|
-
|
|
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
|
|
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:
|
|
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 (
|
|
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 (
|
|
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
|
|
926
|
+
const normalized = normalizeFileName$1(fileName);
|
|
927
|
+
MutableHashMap.set(s.sourceFiles, normalized, {
|
|
1009
928
|
fileName: normalized,
|
|
1010
|
-
imports:
|
|
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 (
|
|
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:
|
|
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
|
|
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
|
|
957
|
+
MutableHashMap.set(s.nodes, fileName, node);
|
|
1036
958
|
}
|
|
1037
|
-
const withChildren =
|
|
959
|
+
const withChildren = MutableHashMap.empty();
|
|
1038
960
|
for (const [fileName, file] of s.sourceFiles) {
|
|
1039
|
-
const
|
|
1040
|
-
if (
|
|
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
|
|
1045
|
-
|
|
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
|
|
1052
|
-
for (const [k, v] of withChildren) s.nodes
|
|
1053
|
-
const withParents =
|
|
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
|
-
|
|
980
|
+
MutableHashMap.set(withParents, fileName, {
|
|
1058
981
|
...node,
|
|
1059
982
|
parents
|
|
1060
983
|
});
|
|
1061
984
|
}
|
|
1062
|
-
s.nodes
|
|
1063
|
-
for (const [k, v] of withParents) s.nodes
|
|
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:
|
|
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 =
|
|
1081
|
-
const changedFiles =
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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;
|