@systemfsoftware/stryker-js-typescript-checker 1.4.0 → 2.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 +58 -0
- package/dist/index.d.mts +2 -118
- package/dist/index.mjs +769 -459
- package/package.json +6 -11
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,454 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { CheckStatus, Checker, CheckerFailed } from "@systemfsoftware/stryker-js-plugin-api/check";
|
|
3
|
+
import { PluginKind, RunConfiguration, declarePlugin } from "@systemfsoftware/stryker-js-plugin-api/plugin";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as FileSystem from "effect/FileSystem";
|
|
6
|
+
import * as Layer from "effect/Layer";
|
|
7
|
+
import * as Path from "effect/Path";
|
|
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";
|
|
4
11
|
import { EOL } from "os";
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
12
|
+
import { Predicate, Result, Schema } from "effect";
|
|
13
|
+
import * as Match from "effect/Match";
|
|
7
14
|
import { API, DiagnosticCategory } from "typescript/unstable/sync";
|
|
8
|
-
import
|
|
9
|
-
import path from "path";
|
|
10
|
-
import semver from "semver";
|
|
15
|
+
import * as Context from "effect/Context";
|
|
11
16
|
import { SyntaxKind } from "typescript/unstable/ast";
|
|
17
|
+
import { createRequire } from "module";
|
|
18
|
+
//#region src/project/hybrid-file-system.schema.ts
|
|
19
|
+
var HybridFileNotFoundError = class extends S.TaggedError()("HybridFileNotFoundError", { fileName: S.String }) {};
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/project/script-file.ts
|
|
22
|
+
function makeScriptFile(content, fileName, modifiedTime = /* @__PURE__ */ new Date()) {
|
|
23
|
+
return {
|
|
24
|
+
content,
|
|
25
|
+
fileName,
|
|
26
|
+
originalContent: content,
|
|
27
|
+
modifiedTime
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function withContent(file, content) {
|
|
31
|
+
return {
|
|
32
|
+
...file,
|
|
33
|
+
content,
|
|
34
|
+
modifiedTime: /* @__PURE__ */ new Date()
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function mutateScriptFile(file, mutant) {
|
|
38
|
+
const start = getOffset(file, mutant.location.start);
|
|
39
|
+
const end = getOffset(file, mutant.location.end);
|
|
40
|
+
const content = `${file.originalContent.slice(0, start)}${mutant.replacement}${file.originalContent.slice(end)}`;
|
|
41
|
+
return {
|
|
42
|
+
...file,
|
|
43
|
+
content,
|
|
44
|
+
modifiedTime: /* @__PURE__ */ new Date()
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function resetScriptFile(file) {
|
|
48
|
+
return {
|
|
49
|
+
...file,
|
|
50
|
+
content: file.originalContent,
|
|
51
|
+
modifiedTime: /* @__PURE__ */ new Date()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function getOffset(file, pos) {
|
|
55
|
+
const lines = file.originalContent.split("\n");
|
|
56
|
+
const lineCount = Math.min(pos.line, lines.length);
|
|
57
|
+
let offset = 0;
|
|
58
|
+
for (let i = 0; i < lineCount; i++) {
|
|
59
|
+
const line = lines[i];
|
|
60
|
+
if (line === void 0) break;
|
|
61
|
+
offset += line.length + 1;
|
|
62
|
+
}
|
|
63
|
+
offset += pos.column;
|
|
64
|
+
return offset;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/project/hybrid-file-system.ts
|
|
68
|
+
const makeEmptyFilesMap = () => /* @__PURE__ */ new Map();
|
|
69
|
+
const makeEmptyOverridesMap = () => /* @__PURE__ */ new Map();
|
|
70
|
+
const copyAndSet = (map, key, value) => {
|
|
71
|
+
const next = new Map(map);
|
|
72
|
+
next.set(key, value);
|
|
73
|
+
return next;
|
|
74
|
+
};
|
|
75
|
+
const makeHybridFileSystem = (fsService) => Effect.gen(function* () {
|
|
76
|
+
const filesRef = yield* Ref.make(makeEmptyFilesMap());
|
|
77
|
+
const overridesRef = yield* Ref.make(makeEmptyOverridesMap());
|
|
78
|
+
const fileNameIsBuildInfo = (fileName) => fileName.endsWith(".tsbuildinfo");
|
|
79
|
+
const fileSystem = {
|
|
80
|
+
readFile: (fileName) => {
|
|
81
|
+
const normalized = normalizeFileName(fileName);
|
|
82
|
+
if (fileNameIsBuildInfo(normalized)) return null;
|
|
83
|
+
const override = overridesRef.ref.current.get(normalized);
|
|
84
|
+
if (override !== void 0) return override;
|
|
85
|
+
const files = filesRef.ref.current;
|
|
86
|
+
const file = files.get(normalized);
|
|
87
|
+
if (file) return file.content;
|
|
88
|
+
if (file === void 0 && files.has(normalized)) return null;
|
|
89
|
+
},
|
|
90
|
+
fileExists: (fileName) => {
|
|
91
|
+
const normalized = normalizeFileName(fileName);
|
|
92
|
+
if (fileNameIsBuildInfo(normalized)) return false;
|
|
93
|
+
if (overridesRef.ref.current.has(normalized)) return true;
|
|
94
|
+
const files = filesRef.ref.current;
|
|
95
|
+
if (files.has(normalized)) return files.get(normalized) !== void 0;
|
|
96
|
+
},
|
|
97
|
+
directoryExists: () => void 0,
|
|
98
|
+
getAccessibleEntries: () => void 0,
|
|
99
|
+
realpath: () => void 0
|
|
100
|
+
};
|
|
101
|
+
const getFile = (fileName) => Effect.gen(function* () {
|
|
102
|
+
const normalized = normalizeFileName(fileName);
|
|
103
|
+
const files = yield* Ref.get(filesRef);
|
|
104
|
+
if (files.has(normalized)) return files.get(normalized);
|
|
105
|
+
const content = yield* fsService.readFileString(normalized).pipe(Effect.orElseSucceed(() => void 0));
|
|
106
|
+
if (content === void 0) {
|
|
107
|
+
yield* Ref.update(filesRef, (m) => copyAndSet(m, normalized, void 0));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const file = makeScriptFile(content, normalized);
|
|
111
|
+
yield* Ref.update(filesRef, (m) => copyAndSet(m, normalized, file));
|
|
112
|
+
return file;
|
|
113
|
+
});
|
|
114
|
+
const writeFile = (fileName, data) => Effect.gen(function* () {
|
|
115
|
+
const normalized = normalizeFileName(fileName);
|
|
116
|
+
const existing = (yield* Ref.get(filesRef)).get(normalized);
|
|
117
|
+
if (existing) {
|
|
118
|
+
const next = withContent(existing, data);
|
|
119
|
+
yield* Ref.update(filesRef, (m) => copyAndSet(m, normalized, next));
|
|
120
|
+
} else {
|
|
121
|
+
const file = makeScriptFile(data, normalized);
|
|
122
|
+
yield* Ref.update(filesRef, (m) => copyAndSet(m, normalized, file));
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const mutateFile = (fileName, mutant) => Effect.gen(function* () {
|
|
126
|
+
const file = yield* getFile(fileName);
|
|
127
|
+
if (!file) return yield* new HybridFileNotFoundError({ fileName });
|
|
128
|
+
const next = mutateScriptFile(file, mutant);
|
|
129
|
+
const normalized = normalizeFileName(fileName);
|
|
130
|
+
yield* Ref.update(filesRef, (m) => copyAndSet(m, normalized, next));
|
|
131
|
+
});
|
|
132
|
+
const resetFile = (fileName) => Effect.gen(function* () {
|
|
133
|
+
const normalized = normalizeFileName(fileName);
|
|
134
|
+
const file = (yield* Ref.get(filesRef)).get(normalized);
|
|
135
|
+
if (file) {
|
|
136
|
+
const next = resetScriptFile(file);
|
|
137
|
+
yield* Ref.update(filesRef, (m) => copyAndSet(m, normalized, next));
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
const existsInMemory = (fileName) => Effect.gen(function* () {
|
|
141
|
+
return (yield* Ref.get(filesRef)).get(normalizeFileName(fileName)) !== void 0;
|
|
142
|
+
});
|
|
143
|
+
const setTsConfigOverrides = (overrides) => Ref.set(overridesRef, overrides);
|
|
144
|
+
return {
|
|
145
|
+
fileSystem,
|
|
146
|
+
getFile,
|
|
147
|
+
writeFile,
|
|
148
|
+
mutateFile,
|
|
149
|
+
resetFile,
|
|
150
|
+
existsInMemory,
|
|
151
|
+
setTsConfigOverrides
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/grouping/ts-file-node.ts
|
|
156
|
+
function makeTSFileNode(fileName) {
|
|
157
|
+
return {
|
|
158
|
+
fileName,
|
|
159
|
+
parents: [],
|
|
160
|
+
children: []
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function getAllParentReferencesIncludingSelf(node, allParentReferences = /* @__PURE__ */ new Set()) {
|
|
164
|
+
allParentReferences.add(node);
|
|
165
|
+
for (const parent of node.parents) if (!allParentReferences.has(parent)) getAllParentReferencesIncludingSelf(parent, allParentReferences);
|
|
166
|
+
return allParentReferences;
|
|
167
|
+
}
|
|
168
|
+
function getMutantsWithReferenceToChildrenOrSelf(node, mutants, nodesChecked = []) {
|
|
169
|
+
if (nodesChecked.includes(node.fileName)) return [];
|
|
170
|
+
nodesChecked.push(node.fileName);
|
|
171
|
+
const relatedMutants = mutants.filter((m) => normalizeFileName(m.fileName) === node.fileName);
|
|
172
|
+
const childResult = node.children.flatMap((c) => getMutantsWithReferenceToChildrenOrSelf(c, mutants, nodesChecked));
|
|
173
|
+
return [...relatedMutants, ...childResult];
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/diagnostics.schema.ts
|
|
177
|
+
/**
|
|
178
|
+
* The compiler returned a diagnostic without a file name. The checker cannot
|
|
179
|
+
* attribute it to a mutant and the run should be failed rather than silently
|
|
180
|
+
* marking every mutant as a compile error.
|
|
181
|
+
*/
|
|
182
|
+
var DiagnosticWithoutFileError = class extends S.TaggedError()("DiagnosticWithoutFileError", { text: S.String }) {};
|
|
183
|
+
/**
|
|
184
|
+
* The compiler reported a diagnostic in a file that is not part of the
|
|
185
|
+
* project graph built from the tsconfig. This indicates a graph or FS
|
|
186
|
+
* mismatch and must be surfaced as a checker failure rather than ignored.
|
|
187
|
+
*/
|
|
188
|
+
var DiagnosticInUnrelatedFileError = class extends S.TaggedError()("DiagnosticInUnrelatedFileError", {
|
|
189
|
+
text: S.String,
|
|
190
|
+
fileName: S.String
|
|
191
|
+
}) {};
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/diagnostics.ts
|
|
194
|
+
/**
|
|
195
|
+
* Pure decision: given a batch of diagnostics and the file graph, decide
|
|
196
|
+
* which mutants are definitely responsible and which need an individual
|
|
197
|
+
* re-check.
|
|
198
|
+
*
|
|
199
|
+
* No I/O, no clock, no throwing — diagnostics in, verdict out. The caller
|
|
200
|
+
* (executor) performs the compiler call and, if `needsRetest` is non-empty,
|
|
201
|
+
* re-invokes the compiler for each of those mutants individually.
|
|
202
|
+
*/
|
|
203
|
+
function classifyDiagnostics(diagnostics, mutants, nodes) {
|
|
204
|
+
const definitive = /* @__PURE__ */ new Map();
|
|
205
|
+
const needsRetest = /* @__PURE__ */ new Map();
|
|
206
|
+
if (diagnostics.length > 0 && mutants.length === 1) {
|
|
207
|
+
const only = mutants[0];
|
|
208
|
+
if (only !== void 0) definitive.set(only.id, [...diagnostics]);
|
|
209
|
+
return Result.succeed({
|
|
210
|
+
definitive,
|
|
211
|
+
needsRetest: []
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
for (const diagnostic of diagnostics) {
|
|
215
|
+
if (!diagnostic.fileName) return Result.fail(new DiagnosticWithoutFileError({ text: diagnostic.text }));
|
|
216
|
+
const node = nodes.get(diagnostic.fileName);
|
|
217
|
+
if (!node) return Result.fail(new DiagnosticInUnrelatedFileError({
|
|
218
|
+
text: diagnostic.text,
|
|
219
|
+
fileName: diagnostic.fileName
|
|
220
|
+
}));
|
|
221
|
+
const related = getMutantsWithReferenceToChildrenOrSelf(node, [...mutants]);
|
|
222
|
+
if (related.length === 0) for (const m of mutants) needsRetest.set(m.id, m);
|
|
223
|
+
else if (related.length === 1) {
|
|
224
|
+
const only = related[0];
|
|
225
|
+
if (only !== void 0) {
|
|
226
|
+
const existing = definitive.get(only.id);
|
|
227
|
+
if (existing) existing.push(diagnostic);
|
|
228
|
+
else definitive.set(only.id, [diagnostic]);
|
|
229
|
+
}
|
|
230
|
+
} else for (const m of related) needsRetest.set(m.id, m);
|
|
231
|
+
}
|
|
232
|
+
const filteredRetest = [...needsRetest.values()].filter((m) => !definitive.has(m.id));
|
|
233
|
+
return Result.succeed({
|
|
234
|
+
definitive,
|
|
235
|
+
needsRetest: filteredRetest
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Pure grouping decision: delegates to the existing `createGroups` but
|
|
240
|
+
* expressed as a kernel entry so the checker can state the boundary.
|
|
241
|
+
* `nodes` is the file graph; mutants outside the project are separated
|
|
242
|
+
* before grouping, matching the executor's `split` that was previously
|
|
243
|
+
* inline in `TypescriptChecker.group`.
|
|
244
|
+
*/
|
|
245
|
+
function partitionMutantsForGrouping(mutants, nodes, prioritizePerformanceOverAccuracy) {
|
|
246
|
+
if (!prioritizePerformanceOverAccuracy) return {
|
|
247
|
+
inside: [],
|
|
248
|
+
outside: [...mutants]
|
|
249
|
+
};
|
|
250
|
+
const outside = [];
|
|
251
|
+
const inside = [];
|
|
252
|
+
for (const m of mutants) if (nodes.get(normalizeFileName(m.fileName)) == null) outside.push(m);
|
|
253
|
+
else inside.push(m);
|
|
254
|
+
return {
|
|
255
|
+
inside,
|
|
256
|
+
outside
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region src/grouping/create-groups.ts
|
|
261
|
+
/**
|
|
262
|
+
* To speed up the type-checking we want to check multiple mutants at once.
|
|
263
|
+
* When multiple mutants in different files don't have overlap in affected files (or have small overlap), we can type-check them simultaneously.
|
|
264
|
+
* These mutants who can be tested at the same time are called a group.
|
|
265
|
+
* Therefore, the return type is an array of arrays, in other words: an array of groups.
|
|
266
|
+
*
|
|
267
|
+
* @param mutants All the mutants of the test project.
|
|
268
|
+
* @param nodes A graph representation of the test project.
|
|
269
|
+
*/
|
|
270
|
+
function createGroups(mutants, nodes) {
|
|
271
|
+
const groups = [];
|
|
272
|
+
const mutantsToGroup = new Set(mutants);
|
|
273
|
+
while (mutantsToGroup.size) {
|
|
274
|
+
const group = [];
|
|
275
|
+
const groupNodes = /* @__PURE__ */ new Set();
|
|
276
|
+
const nodesToIgnore = /* @__PURE__ */ new Set();
|
|
277
|
+
for (const currentMutant of mutantsToGroup) {
|
|
278
|
+
const currentNode = findNode(currentMutant.fileName, nodes);
|
|
279
|
+
if (!nodesToIgnore.has(currentNode) && !parentsHaveOverlapWith(currentNode, groupNodes)) {
|
|
280
|
+
group.push(currentMutant.id);
|
|
281
|
+
groupNodes.add(currentNode);
|
|
282
|
+
mutantsToGroup.delete(currentMutant);
|
|
283
|
+
addRangeOfNodesToSet(nodesToIgnore, getAllParentReferencesIncludingSelf(currentNode));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
groups.push(group);
|
|
287
|
+
}
|
|
288
|
+
return groups;
|
|
289
|
+
}
|
|
290
|
+
function addRangeOfNodesToSet(nodes, nodesToAdd) {
|
|
291
|
+
for (const parent of nodesToAdd) nodes.add(parent);
|
|
292
|
+
}
|
|
293
|
+
function findNode(fileName, nodes) {
|
|
294
|
+
const node = nodes.get(normalizeFileName(fileName));
|
|
295
|
+
if (node == null) throw new Error(`Node not in graph: ${fileName}`);
|
|
296
|
+
return node;
|
|
297
|
+
}
|
|
298
|
+
function parentsHaveOverlapWith(currentNode, groupNodes) {
|
|
299
|
+
for (const parentNode of getAllParentReferencesIncludingSelf(currentNode)) if (groupNodes.has(parentNode)) return true;
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/typescript-checker.ts
|
|
304
|
+
const emptyCheckResultMap = () => /* @__PURE__ */ new Map();
|
|
305
|
+
const emptyDiagnosticMap = () => /* @__PURE__ */ new Map();
|
|
306
|
+
function getPrioritize(options) {
|
|
307
|
+
if (!Predicate.hasProperty(options, "typescriptChecker")) return false;
|
|
308
|
+
const tc = options["typescriptChecker"];
|
|
309
|
+
if (typeof tc !== "object" || tc === null) return false;
|
|
310
|
+
if (!Predicate.hasProperty(tc, "prioritizePerformanceOverAccuracy")) return false;
|
|
311
|
+
const val = tc["prioritizePerformanceOverAccuracy"];
|
|
312
|
+
return typeof val === "boolean" ? val : false;
|
|
313
|
+
}
|
|
314
|
+
function makeCheckerService({ options, compiler }) {
|
|
315
|
+
const formatDiagnostic = (error) => Effect.gen(function* () {
|
|
316
|
+
const severity = error.category === DiagnosticCategory.Error ? "error" : error.category === DiagnosticCategory.Warning ? "warning" : error.category === DiagnosticCategory.Suggestion ? "suggestion" : "message";
|
|
317
|
+
let location = "";
|
|
318
|
+
if (error.fileName) {
|
|
319
|
+
const lineAndCharacter = yield* compiler.getLineAndCharacterOfPosition(error.fileName, error.pos).pipe(Effect.orElseSucceed(() => void 0));
|
|
320
|
+
const line = (lineAndCharacter?.line ?? 0) + 1;
|
|
321
|
+
const character = (lineAndCharacter?.character ?? 0) + 1;
|
|
322
|
+
location = `${error.fileName}(${line},${character}): `;
|
|
323
|
+
}
|
|
324
|
+
return `${location}${severity} TS${error.code}: ${error.text}`;
|
|
325
|
+
});
|
|
326
|
+
const createErrorText = (errors) => Effect.gen(function* () {
|
|
327
|
+
return (yield* Effect.forEach(errors, formatDiagnostic)).join(EOL);
|
|
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
|
+
}
|
|
413
|
+
//#endregion
|
|
414
|
+
//#region src/compiler-error.schema.ts
|
|
415
|
+
const TypeId = "~stryker/typescript-checker/CompilerFailed";
|
|
416
|
+
/**
|
|
417
|
+
* Every way the TypeScript compiler can fail while serving a check.
|
|
418
|
+
*
|
|
419
|
+
* One tagged error rather than four, because nothing branches on which of these
|
|
420
|
+
* happened — they all reach the caller as a failed check. The `reason` keeps the
|
|
421
|
+
* cases distinguishable in a report without inventing four types no consumer
|
|
422
|
+
* discriminates.
|
|
423
|
+
*/
|
|
424
|
+
var CompilerFailed = class extends S.TaggedError(TypeId)("CompilerFailed", {
|
|
425
|
+
reason: S.Literals([
|
|
426
|
+
"not-initialized",
|
|
427
|
+
"no-projects",
|
|
428
|
+
"unknown-file-node",
|
|
429
|
+
"file-not-in-project"
|
|
430
|
+
]),
|
|
431
|
+
/** The file or tsconfig the failure is about, when it is about one. */
|
|
432
|
+
subject: S.optional(S.String)
|
|
433
|
+
}) {
|
|
434
|
+
[TypeId] = TypeId;
|
|
435
|
+
get message() {
|
|
436
|
+
switch (this.reason) {
|
|
437
|
+
case "not-initialized": return "The TypeScript compiler was used before it was initialized";
|
|
438
|
+
case "no-projects": return `No projects were found for ${this.subject ?? "the tsconfig"}`;
|
|
439
|
+
case "unknown-file-node": return `The file graph has no node for '${this.subject ?? "a file"}', which should not happen`;
|
|
440
|
+
case "file-not-in-project": return `'${this.subject ?? "a file"}' is part of your TypeScript project but could not be found on disk`;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region src/declaration-source-mapping.ts
|
|
446
|
+
const findSourceMapRegex = /\/\/# sourceMappingURL=(.+)$/m;
|
|
447
|
+
function getSourceMappingURL(content) {
|
|
448
|
+
findSourceMapRegex.lastIndex = 0;
|
|
449
|
+
return findSourceMapRegex.exec(content)?.[1];
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
12
452
|
//#region ../../../../../node_modules/.pnpm/@jsr+std__jsonc@1.0.2/node_modules/@jsr/std__jsonc/parse.js
|
|
13
453
|
/**
|
|
14
454
|
* Converts a JSON with Comments (JSONC) string into an object.
|
|
@@ -229,7 +669,7 @@ function buildErrorMessage({ type, sourceText, position }) {
|
|
|
229
669
|
return `Cannot parse JSONC: unexpected token "${token}" in JSONC at position ${position}`;
|
|
230
670
|
}
|
|
231
671
|
//#endregion
|
|
232
|
-
//#region src/tsconfig
|
|
672
|
+
//#region src/tsconfig.schema.ts
|
|
233
673
|
/**
|
|
234
674
|
* Error returned when a tsconfig file fails to parse or does not match the
|
|
235
675
|
* shape this package consumes.
|
|
@@ -238,13 +678,21 @@ var TsConfigParseError = class extends Schema.TaggedError()("TsConfigParseError"
|
|
|
238
678
|
file: Schema.String,
|
|
239
679
|
reason: Schema.String
|
|
240
680
|
}) {};
|
|
681
|
+
/**
|
|
682
|
+
* The configured tsconfig file could not be read from disk.
|
|
683
|
+
*/
|
|
684
|
+
var TsConfigNotFoundError = class extends Schema.TaggedError()("TsConfigNotFoundError", { file: Schema.String }) {
|
|
685
|
+
get message() {
|
|
686
|
+
return `The tsconfig file does not exist at: "${this.file}". Please configure the tsconfig file in your stryker.conf file using "tsconfigFile"`;
|
|
687
|
+
}
|
|
688
|
+
};
|
|
241
689
|
const JsonRecord = Schema.Record(Schema.String, Schema.Unknown);
|
|
242
690
|
const TsConfigSchema = Schema.StructWithRest(Schema.Struct({
|
|
243
691
|
references: Schema.optional(Schema.Array(Schema.StructWithRest(Schema.Struct({ path: Schema.String }), [JsonRecord]))),
|
|
244
692
|
compilerOptions: Schema.optional(JsonRecord)
|
|
245
693
|
}), [JsonRecord]);
|
|
246
694
|
//#endregion
|
|
247
|
-
//#region src/tsconfig
|
|
695
|
+
//#region src/tsconfig.ts
|
|
248
696
|
const COMPILER_OPTIONS_OVERRIDES = Object.freeze({
|
|
249
697
|
allowUnreachableCode: true,
|
|
250
698
|
noUnusedLocals: false,
|
|
@@ -262,17 +710,6 @@ const LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES = Object.freeze({
|
|
|
262
710
|
declarationMap: true,
|
|
263
711
|
declaration: true
|
|
264
712
|
});
|
|
265
|
-
let cachedTSVersion;
|
|
266
|
-
function getTSVersion() {
|
|
267
|
-
if (cachedTSVersion === void 0) {
|
|
268
|
-
const require = createRequire(import.meta.url);
|
|
269
|
-
cachedTSVersion = Schema.decodeUnknownSync(Schema.Struct({ version: Schema.String }))(JSON.parse(readFileSync(require.resolve("typescript/package.json"), "utf-8"))).version;
|
|
270
|
-
}
|
|
271
|
-
return cachedTSVersion;
|
|
272
|
-
}
|
|
273
|
-
function guardTSVersion(version = getTSVersion()) {
|
|
274
|
-
if (!semver.satisfies(version, ">=7.0.0", { includePrerelease: true })) throw new Error(`@systemfsoftware/stryker-js-typescript-checker only supports typescript@7.0.0 or higher. Found typescript@${version}`);
|
|
275
|
-
}
|
|
276
713
|
/**
|
|
277
714
|
* Parses the raw text of a tsconfig file into a typed config, rejecting shapes this package cannot consume.
|
|
278
715
|
* @param fileName The tsconfig file name, used for error reporting
|
|
@@ -296,13 +733,13 @@ function parseTsConfig(fileName, jsonText) {
|
|
|
296
733
|
* Determines whether or not to use `--build` mode based on "references" being there in the config file
|
|
297
734
|
* @param tsconfigFileName The tsconfig file to parse
|
|
298
735
|
*/
|
|
299
|
-
|
|
300
|
-
const parsed = parseTsConfig(tsconfigFileName,
|
|
736
|
+
const determineBuildModeEnabled = (tsconfigFileName, fsService) => Effect.gen(function* () {
|
|
737
|
+
const parsed = parseTsConfig(tsconfigFileName, yield* fsService.readFileString(tsconfigFileName));
|
|
301
738
|
return Result.match(parsed, {
|
|
302
739
|
onFailure: () => false,
|
|
303
740
|
onSuccess: (config) => config.references !== void 0
|
|
304
741
|
});
|
|
305
|
-
}
|
|
742
|
+
});
|
|
306
743
|
/**
|
|
307
744
|
* Overrides some options to speed up compilation and disable some code quality checks we don't want during mutation testing
|
|
308
745
|
* @param config The parsed config file
|
|
@@ -332,354 +769,161 @@ function overrideOptions(config, useBuildMode) {
|
|
|
332
769
|
* @param config The parsed config file
|
|
333
770
|
* @param fromDirName The directory where to resolve from
|
|
334
771
|
*/
|
|
335
|
-
function retrieveReferencedProjects(config, fromDirName) {
|
|
772
|
+
function retrieveReferencedProjects(config, fromDirName, pathService) {
|
|
336
773
|
return (config.references ?? []).map((reference) => {
|
|
337
|
-
let resolved =
|
|
338
|
-
if (!
|
|
339
|
-
return
|
|
774
|
+
let resolved = pathService.resolve(fromDirName, reference.path);
|
|
775
|
+
if (!pathService.basename(resolved).endsWith(".json")) resolved = pathService.join(resolved, "tsconfig.json");
|
|
776
|
+
return normalizeFileName(resolved);
|
|
340
777
|
});
|
|
341
778
|
}
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/typescript-version.schema.ts
|
|
342
781
|
/**
|
|
343
|
-
*
|
|
344
|
-
* @param fileName The file name that may contain backslashes `\`
|
|
345
|
-
* @returns posix and ts complaint file name (with `/`)
|
|
346
|
-
*/
|
|
347
|
-
function toPosixFileName(fileName) {
|
|
348
|
-
return fileName.replace(/\\/g, "/");
|
|
349
|
-
}
|
|
350
|
-
/**
|
|
351
|
-
* Find source file in declaration file
|
|
352
|
-
* @param content The content of the declaration file
|
|
353
|
-
* @returns URL of the source file or undefined if not found
|
|
782
|
+
* The installed TypeScript version is below the supported floor.
|
|
354
783
|
*/
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return findSourceMapRegex.exec(content)?.[1];
|
|
359
|
-
}
|
|
360
|
-
//#endregion
|
|
361
|
-
//#region src/grouping/ts-file-node.ts
|
|
362
|
-
var TSFileNode = class {
|
|
363
|
-
fileName;
|
|
364
|
-
parents;
|
|
365
|
-
children;
|
|
366
|
-
constructor(fileName, parents, children) {
|
|
367
|
-
this.fileName = fileName;
|
|
368
|
-
this.parents = parents;
|
|
369
|
-
this.children = children;
|
|
370
|
-
}
|
|
371
|
-
getAllParentReferencesIncludingSelf(allParentReferences = /* @__PURE__ */ new Set()) {
|
|
372
|
-
allParentReferences.add(this);
|
|
373
|
-
this.parents.forEach((parent) => {
|
|
374
|
-
if (!allParentReferences.has(parent)) parent.getAllParentReferencesIncludingSelf(allParentReferences);
|
|
375
|
-
});
|
|
376
|
-
return allParentReferences;
|
|
377
|
-
}
|
|
378
|
-
getAllChildReferencesIncludingSelf(allChildReferences = /* @__PURE__ */ new Set()) {
|
|
379
|
-
allChildReferences.add(this);
|
|
380
|
-
this.children.forEach((child) => {
|
|
381
|
-
if (!allChildReferences.has(child)) child.getAllChildReferencesIncludingSelf(allChildReferences);
|
|
382
|
-
});
|
|
383
|
-
return allChildReferences;
|
|
384
|
-
}
|
|
385
|
-
getMutantsWithReferenceToChildrenOrSelf(mutants, nodesChecked = []) {
|
|
386
|
-
if (nodesChecked.includes(this.fileName)) return [];
|
|
387
|
-
nodesChecked.push(this.fileName);
|
|
388
|
-
const relatedMutants = mutants.filter((m) => toPosixFileName(m.fileName) === this.fileName);
|
|
389
|
-
const childResult = this.children.flatMap((c) => c.getMutantsWithReferenceToChildrenOrSelf(mutants, nodesChecked));
|
|
390
|
-
return [...relatedMutants, ...childResult];
|
|
784
|
+
var UnsupportedTypeScriptVersionError = class extends Schema.TaggedError()("UnsupportedTypeScriptVersionError", { version: Schema.String }) {
|
|
785
|
+
get message() {
|
|
786
|
+
return `@systemfsoftware/stryker-js-typescript-checker only supports typescript@7.0.0 or higher. Found typescript@${this.version}`;
|
|
391
787
|
}
|
|
392
788
|
};
|
|
393
789
|
//#endregion
|
|
394
|
-
//#region src/
|
|
790
|
+
//#region src/typescript-version.ts
|
|
791
|
+
let cachedTSVersion;
|
|
792
|
+
const getTSVersion = (fsService) => Effect.gen(function* () {
|
|
793
|
+
if (cachedTSVersion !== void 0) return cachedTSVersion;
|
|
794
|
+
const pkgPath = createRequire(import.meta.url).resolve("typescript/package.json");
|
|
795
|
+
const text = yield* fsService.readFileString(pkgPath);
|
|
796
|
+
const raw = JSON.parse(text);
|
|
797
|
+
let version = "";
|
|
798
|
+
if (Predicate.hasProperty(raw, "version") && typeof raw.version === "string") version = raw.version;
|
|
799
|
+
cachedTSVersion = version;
|
|
800
|
+
return version;
|
|
801
|
+
});
|
|
395
802
|
/**
|
|
396
|
-
*
|
|
397
|
-
* When multiple mutants in different files don't have overlap in affected files (or have small overlap), we can type-check them simultaneously.
|
|
398
|
-
* These mutants who can be tested at the same time are called a group.
|
|
399
|
-
* Therefore, the return type is an array of arrays, in other words: an array of groups.
|
|
803
|
+
* Whether a TypeScript version satisfies the supported floor `>=7.0.0`.
|
|
400
804
|
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
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.
|
|
403
810
|
*/
|
|
404
|
-
function
|
|
405
|
-
const
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
group.push(currentMutant.id);
|
|
415
|
-
groupNodes.add(currentNode);
|
|
416
|
-
mutantsToGroup.delete(currentMutant);
|
|
417
|
-
addRangeOfNodesToSet(nodesToIgnore, currentNode.getAllParentReferencesIncludingSelf());
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
groups.push(group);
|
|
421
|
-
}
|
|
422
|
-
return groups;
|
|
423
|
-
}
|
|
424
|
-
function addRangeOfNodesToSet(nodes, nodesToAdd) {
|
|
425
|
-
for (const parent of nodesToAdd) nodes.add(parent);
|
|
811
|
+
function isSupportedTypescriptVersion(version) {
|
|
812
|
+
const dashBase = version.split("-")[0] ?? version;
|
|
813
|
+
const parts = (dashBase.split("+")[0] ?? dashBase).split(".").map((p) => Number.parseInt(p, 10));
|
|
814
|
+
const major = parts[0] ?? 0;
|
|
815
|
+
const minor = parts[1] ?? 0;
|
|
816
|
+
const patch = parts[2] ?? 0;
|
|
817
|
+
if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch)) return false;
|
|
818
|
+
if (major !== 7) return major > 7;
|
|
819
|
+
if (minor !== 0) return minor > 0;
|
|
820
|
+
return patch >= 0;
|
|
426
821
|
}
|
|
427
|
-
function
|
|
428
|
-
const
|
|
429
|
-
if (
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
function parentsHaveOverlapWith(currentNode, groupNodes) {
|
|
433
|
-
for (const parentNode of currentNode.getAllParentReferencesIncludingSelf()) if (groupNodes.has(parentNode)) return true;
|
|
434
|
-
return false;
|
|
435
|
-
}
|
|
436
|
-
const tsCompiler = "tsCompiler";
|
|
437
|
-
//#endregion
|
|
438
|
-
//#region src/project/script-file.ts
|
|
439
|
-
var ScriptFile = class {
|
|
440
|
-
content;
|
|
441
|
-
fileName;
|
|
442
|
-
modifiedTime;
|
|
443
|
-
originalContent;
|
|
444
|
-
constructor(content, fileName, modifiedTime = /* @__PURE__ */ new Date()) {
|
|
445
|
-
this.content = content;
|
|
446
|
-
this.fileName = fileName;
|
|
447
|
-
this.modifiedTime = modifiedTime;
|
|
448
|
-
this.originalContent = content;
|
|
449
|
-
}
|
|
450
|
-
write(content) {
|
|
451
|
-
this.content = content;
|
|
452
|
-
this.touch();
|
|
453
|
-
}
|
|
454
|
-
mutate(mutant) {
|
|
455
|
-
const start = this.getOffset(mutant.location.start);
|
|
456
|
-
const end = this.getOffset(mutant.location.end);
|
|
457
|
-
this.content = `${this.originalContent.slice(0, start)}${mutant.replacement}${this.originalContent.slice(end)}`;
|
|
458
|
-
this.touch();
|
|
459
|
-
}
|
|
460
|
-
getOffset(pos) {
|
|
461
|
-
const lines = this.originalContent.split("\n");
|
|
462
|
-
const lineCount = Math.min(pos.line, lines.length);
|
|
463
|
-
let offset = 0;
|
|
464
|
-
for (let i = 0; i < lineCount; i++) {
|
|
465
|
-
const line = lines[i];
|
|
466
|
-
if (line === void 0) break;
|
|
467
|
-
offset += line.length + 1;
|
|
468
|
-
}
|
|
469
|
-
offset += pos.column;
|
|
470
|
-
return offset;
|
|
471
|
-
}
|
|
472
|
-
resetMutant() {
|
|
473
|
-
this.content = this.originalContent;
|
|
474
|
-
this.touch();
|
|
475
|
-
}
|
|
476
|
-
touch() {
|
|
477
|
-
this.modifiedTime = /* @__PURE__ */ new Date();
|
|
478
|
-
}
|
|
479
|
-
};
|
|
822
|
+
const guardTSVersion = (fsService) => Effect.gen(function* () {
|
|
823
|
+
const version = yield* getTSVersion(fsService);
|
|
824
|
+
if (!isSupportedTypescriptVersion(version)) return yield* new UnsupportedTypeScriptVersionError({ version });
|
|
825
|
+
});
|
|
480
826
|
//#endregion
|
|
481
|
-
//#region src/
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
827
|
+
//#region src/typescript-compiler.ts
|
|
828
|
+
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
|
+
const makeDummy = Effect.gen(function* () {
|
|
837
|
+
const stateRef = yield* Ref.make({
|
|
838
|
+
api: void 0,
|
|
839
|
+
snapshot: void 0,
|
|
840
|
+
sourceFiles: emptySourceFiles(),
|
|
841
|
+
nodes: emptyNodes(),
|
|
842
|
+
lastMutants: [],
|
|
843
|
+
lastMutatedFileNames: [],
|
|
844
|
+
allTSConfigFiles: setFromArray(["tsconfig.json"]),
|
|
845
|
+
tsconfigFile: "tsconfig.json"
|
|
846
|
+
});
|
|
847
|
+
yield* Effect.addFinalizer(() => Effect.gen(function* () {
|
|
848
|
+
const s = yield* Ref.get(stateRef);
|
|
849
|
+
yield* Effect.sync(() => s.snapshot?.dispose());
|
|
850
|
+
yield* Effect.sync(() => s.api?.close());
|
|
851
|
+
}));
|
|
852
|
+
return {
|
|
853
|
+
init: Effect.succeed([]),
|
|
854
|
+
check: () => Effect.succeed([]),
|
|
855
|
+
nodes: Ref.get(stateRef).pipe(Effect.map((s) => s.nodes)),
|
|
856
|
+
close: Effect.gen(function* () {
|
|
857
|
+
const s = yield* Ref.get(stateRef);
|
|
858
|
+
yield* Effect.sync(() => s.snapshot?.dispose());
|
|
859
|
+
yield* Effect.sync(() => s.api?.close());
|
|
860
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
861
|
+
...prev,
|
|
862
|
+
snapshot: void 0,
|
|
863
|
+
api: void 0
|
|
864
|
+
}));
|
|
865
|
+
}),
|
|
866
|
+
getLineAndCharacterOfPosition: () => Effect.succeed(void 0)
|
|
504
867
|
};
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
868
|
+
});
|
|
869
|
+
Layer.effect(TypeScriptCompiler)(makeDummy);
|
|
870
|
+
function makeTypescriptCompiler(options, fs, fsService, pathService) {
|
|
871
|
+
if (!S.is(StrykerOptionsSchema)(options)) throw new Error("Invalid StrykerOptions");
|
|
872
|
+
const rawTsconfigFile = normalizeFileName(options.tsconfigFile);
|
|
873
|
+
const initialState = {
|
|
874
|
+
api: void 0,
|
|
875
|
+
snapshot: void 0,
|
|
876
|
+
sourceFiles: emptySourceFiles(),
|
|
877
|
+
nodes: emptyNodes(),
|
|
878
|
+
lastMutants: [],
|
|
879
|
+
lastMutatedFileNames: [],
|
|
880
|
+
allTSConfigFiles: setFromArray([rawTsconfigFile]),
|
|
881
|
+
tsconfigFile: rawTsconfigFile
|
|
510
882
|
};
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
getFile(fileName) {
|
|
521
|
-
const normalized = toPosixFileName(fileName);
|
|
522
|
-
if (!this.files.has(normalized)) try {
|
|
523
|
-
const content = readFileSync(normalized, "utf-8");
|
|
524
|
-
this.files.set(normalized, new ScriptFile(content, normalized));
|
|
525
|
-
} catch {
|
|
526
|
-
this.files.set(normalized, void 0);
|
|
527
|
-
}
|
|
528
|
-
return this.files.get(normalized);
|
|
529
|
-
}
|
|
530
|
-
mutateFile(fileName, mutant) {
|
|
531
|
-
const file = this.getFile(fileName);
|
|
532
|
-
if (!file) throw new Error(`Tried to mutate file "${fileName}" but it could not be found.`);
|
|
533
|
-
file.mutate(mutant);
|
|
534
|
-
}
|
|
535
|
-
resetFile(fileName) {
|
|
536
|
-
this.getFile(fileName)?.resetMutant();
|
|
537
|
-
}
|
|
538
|
-
existsInMemory(fileName) {
|
|
539
|
-
return this.files.get(toPosixFileName(fileName)) !== void 0;
|
|
540
|
-
}
|
|
541
|
-
fileNameIsBuildInfo(fileName) {
|
|
542
|
-
return fileName.endsWith(".tsbuildinfo");
|
|
543
|
-
}
|
|
544
|
-
};
|
|
545
|
-
//#endregion
|
|
546
|
-
//#region src/typescript-compiler.ts
|
|
547
|
-
var TypescriptCompiler = class {
|
|
548
|
-
log;
|
|
549
|
-
options;
|
|
550
|
-
fs;
|
|
551
|
-
static inject = tokens(commonTokens.logger, commonTokens.options, "fs");
|
|
552
|
-
allTSConfigFiles;
|
|
553
|
-
tsconfigFile;
|
|
554
|
-
api;
|
|
555
|
-
snapshot;
|
|
556
|
-
sourceFiles = /* @__PURE__ */ new Map();
|
|
557
|
-
_nodes = /* @__PURE__ */ new Map();
|
|
558
|
-
lastMutants = [];
|
|
559
|
-
lastMutatedFileNames = [];
|
|
560
|
-
constructor(log, options, fs) {
|
|
561
|
-
this.log = log;
|
|
562
|
-
this.options = options;
|
|
563
|
-
this.fs = fs;
|
|
564
|
-
this.tsconfigFile = toPosixFileName(path.resolve(toPosixFileName(this.options.tsconfigFile)));
|
|
565
|
-
this.allTSConfigFiles = /* @__PURE__ */ new Set([this.tsconfigFile]);
|
|
566
|
-
}
|
|
567
|
-
async init() {
|
|
568
|
-
guardTSVersion();
|
|
569
|
-
this.guardTSConfigFileExists();
|
|
570
|
-
const buildModeEnabled = determineBuildModeEnabled(this.tsconfigFile);
|
|
571
|
-
this.collectAllTSConfigFiles(buildModeEnabled);
|
|
572
|
-
this.api = new API({ fs: this.fs });
|
|
573
|
-
this.snapshot = this.api.updateSnapshot({ openProjects: [...this.allTSConfigFiles] });
|
|
574
|
-
const programs = this.getPrograms();
|
|
575
|
-
this.buildDependencyGraph(programs);
|
|
576
|
-
return this.check([]);
|
|
577
|
-
}
|
|
578
|
-
async check(mutants) {
|
|
579
|
-
for (const mutant of this.lastMutants) this.fs.resetFile(mutant.fileName);
|
|
580
|
-
for (const mutant of mutants) {
|
|
581
|
-
const file = this.fs.getFile(mutant.fileName);
|
|
582
|
-
if (!file) throw new Error(`Tried to check file "${mutant.fileName}" (which is part of your typescript project), but it could not be found.`);
|
|
583
|
-
file.mutate(mutant);
|
|
584
|
-
}
|
|
585
|
-
const mutatedFileNames = [...new Set(mutants.map((m) => toPosixFileName(m.fileName)))];
|
|
586
|
-
const changedFiles = [.../* @__PURE__ */ new Set([...this.lastMutatedFileNames, ...mutatedFileNames])];
|
|
587
|
-
if (this.api && this.snapshot) {
|
|
588
|
-
const oldSnapshot = this.snapshot;
|
|
589
|
-
this.snapshot = this.api.updateSnapshot({
|
|
590
|
-
openProjects: [...this.allTSConfigFiles],
|
|
591
|
-
fileChanges: { changed: changedFiles }
|
|
592
|
-
});
|
|
593
|
-
oldSnapshot.dispose();
|
|
594
|
-
}
|
|
595
|
-
this.lastMutants = mutants;
|
|
596
|
-
this.lastMutatedFileNames = mutatedFileNames;
|
|
597
|
-
return this.getPrograms().flatMap((program) => [
|
|
598
|
-
...program.getConfigFileParsingDiagnostics(),
|
|
599
|
-
...program.getSemanticDiagnostics(),
|
|
600
|
-
...program.getProgramDiagnostics()
|
|
601
|
-
]).filter((diagnostic) => diagnostic.category === DiagnosticCategory.Error);
|
|
602
|
-
}
|
|
603
|
-
get nodes() {
|
|
604
|
-
if (!this._nodes.size) {
|
|
605
|
-
for (const [fileName] of this.sourceFiles) {
|
|
606
|
-
const node = new TSFileNode(fileName, [], []);
|
|
607
|
-
this._nodes.set(fileName, node);
|
|
608
|
-
}
|
|
609
|
-
for (const [fileName, file] of this.sourceFiles) {
|
|
610
|
-
const node = this._nodes.get(fileName);
|
|
611
|
-
if (node == null) throw new Error(`Node for file '${fileName}' could not be found. This should not happen.`);
|
|
612
|
-
node.children = [...file.imports].map((importName) => this._nodes.get(importName)).filter((n) => n != null);
|
|
613
|
-
}
|
|
614
|
-
for (const [, node] of this._nodes) {
|
|
615
|
-
node.parents = [];
|
|
616
|
-
for (const [, n] of this._nodes) if (n.children.includes(node)) node.parents.push(n);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
return this._nodes;
|
|
620
|
-
}
|
|
621
|
-
close() {
|
|
622
|
-
this.snapshot?.dispose();
|
|
623
|
-
this.api?.close();
|
|
624
|
-
}
|
|
625
|
-
getLineAndCharacterOfPosition(fileName, position) {
|
|
626
|
-
for (const program of this.getPrograms()) {
|
|
627
|
-
const sourceFile = program.getSourceFile(fileName);
|
|
628
|
-
if (sourceFile) return sourceFile.getLineAndCharacterOfPosition(position);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
getPrograms() {
|
|
632
|
-
if (!this.snapshot) throw new Error("TypescriptCompiler not initialized");
|
|
633
|
-
const projects = this.snapshot.getProjects();
|
|
634
|
-
if (projects.length === 0) throw new Error(`No projects found for ${this.tsconfigFile}`);
|
|
883
|
+
const stateRef = Ref.makeUnsafe(initialState);
|
|
884
|
+
const getProgramsEffect = () => Effect.gen(function* () {
|
|
885
|
+
const s = yield* Ref.get(stateRef);
|
|
886
|
+
if (!s.snapshot) return yield* new CompilerFailed({ reason: "not-initialized" });
|
|
887
|
+
const projects = s.snapshot.getProjects();
|
|
888
|
+
if (projects.length === 0) return yield* new CompilerFailed({
|
|
889
|
+
reason: "no-projects",
|
|
890
|
+
subject: s.tsconfigFile
|
|
891
|
+
});
|
|
635
892
|
return projects.map((project) => project.program);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
893
|
+
});
|
|
894
|
+
const guardTSConfigFileExistsEffect = Effect.gen(function* () {
|
|
895
|
+
const s = yield* Ref.get(stateRef);
|
|
896
|
+
yield* fsService.readFileString(s.tsconfigFile).pipe(Effect.mapError(() => new TsConfigNotFoundError({ file: s.tsconfigFile })));
|
|
897
|
+
});
|
|
898
|
+
const collectAllTSConfigFiles = (buildModeEnabled) => Effect.gen(function* () {
|
|
899
|
+
const s = yield* Ref.get(stateRef);
|
|
900
|
+
const tsConfigOverrides = emptyStringMap();
|
|
901
|
+
const toProcess = [s.tsconfigFile];
|
|
902
|
+
const processed = emptyStringSet();
|
|
641
903
|
while (toProcess.length > 0) {
|
|
642
904
|
const current = toProcess.pop();
|
|
643
905
|
if (!current || processed.has(current)) continue;
|
|
644
906
|
processed.add(current);
|
|
645
|
-
const content =
|
|
907
|
+
const content = yield* fsService.readFileString(current);
|
|
646
908
|
const parsed = parseTsConfig(current, content);
|
|
647
909
|
if (Result.isFailure(parsed)) {
|
|
648
|
-
this.log.warn(`Could not parse tsconfig file "%s": %s. Compiler-option overrides and project-reference walking were skipped for this file, so mutants may be misreported as compile errors.`, current, parsed.failure.reason);
|
|
649
910
|
tsConfigOverrides.set(current, content);
|
|
650
911
|
continue;
|
|
651
912
|
}
|
|
652
913
|
tsConfigOverrides.set(current, overrideOptions(parsed.success, buildModeEnabled));
|
|
653
|
-
for (const referenced of retrieveReferencedProjects(parsed.success,
|
|
654
|
-
|
|
914
|
+
for (const referenced of retrieveReferencedProjects(parsed.success, pathService.dirname(current), pathService)) {
|
|
915
|
+
const normalized = normalizeFileName(referenced);
|
|
916
|
+
s.allTSConfigFiles.add(normalized);
|
|
655
917
|
toProcess.push(referenced);
|
|
656
918
|
}
|
|
657
919
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
fileName: normalized,
|
|
666
|
-
imports: /* @__PURE__ */ new Set()
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
for (const [fileName] of this.sourceFiles) {
|
|
670
|
-
const sourceFile = programs.map((p) => p.getSourceFile(fileName)).find((sf) => sf != null);
|
|
671
|
-
if (!sourceFile) continue;
|
|
672
|
-
const imports = this.extractImports(sourceFile);
|
|
673
|
-
for (const specifier of imports) {
|
|
674
|
-
const resolved = this.resolveModuleSpecifier(fileName, specifier);
|
|
675
|
-
if (resolved) {
|
|
676
|
-
const sourceFileName = this.resolveTSInputFile(resolved);
|
|
677
|
-
if (this.sourceFiles.has(sourceFileName)) this.sourceFiles.get(fileName)?.imports.add(sourceFileName);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
extractImports(sourceFile) {
|
|
920
|
+
yield* fs.setTsConfigOverrides(tsConfigOverrides);
|
|
921
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
922
|
+
...prev,
|
|
923
|
+
allTSConfigFiles: setFromArray([...s.allTSConfigFiles])
|
|
924
|
+
}));
|
|
925
|
+
});
|
|
926
|
+
const extractImports = (sourceFile) => {
|
|
683
927
|
const result = [];
|
|
684
928
|
for (const statement of sourceFile.statements) if (statement.kind === SyntaxKind.ImportDeclaration) {
|
|
685
929
|
let spec;
|
|
@@ -695,17 +939,9 @@ var TypescriptCompiler = class {
|
|
|
695
939
|
for (const ref of sourceFile.referencedFiles) result.push(ref.fileName);
|
|
696
940
|
for (const ref of sourceFile.typeReferenceDirectives) result.push(ref.fileName);
|
|
697
941
|
return result;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const
|
|
701
|
-
if (!cleaned.startsWith("./") && !cleaned.startsWith("../")) return;
|
|
702
|
-
const baseDir = path.dirname(sourceFileName);
|
|
703
|
-
const resolved = toPosixFileName(path.resolve(baseDir, cleaned));
|
|
704
|
-
const candidates = this.getResolutionCandidates(resolved);
|
|
705
|
-
for (const candidate of candidates) if (this.sourceFiles.has(candidate)) return candidate;
|
|
706
|
-
}
|
|
707
|
-
getResolutionCandidates(resolved) {
|
|
708
|
-
const extension = path.extname(resolved);
|
|
942
|
+
};
|
|
943
|
+
const getResolutionCandidates = (resolved, pathService) => {
|
|
944
|
+
const extension = pathService.extname(resolved);
|
|
709
945
|
if (extension) {
|
|
710
946
|
const withoutExt = resolved.slice(0, -extension.length);
|
|
711
947
|
return [
|
|
@@ -736,124 +972,198 @@ var TypescriptCompiler = class {
|
|
|
736
972
|
`${resolved}/index.mjs`,
|
|
737
973
|
`${resolved}/index.cjs`
|
|
738
974
|
];
|
|
739
|
-
}
|
|
740
|
-
|
|
975
|
+
};
|
|
976
|
+
const resolveModuleSpecifier = (sourceFileName, specifier, sourceFiles, pathService) => {
|
|
977
|
+
const cleaned = specifier.replace(/^['"]|['"]$/g, "");
|
|
978
|
+
if (!cleaned.startsWith("./") && !cleaned.startsWith("../")) return;
|
|
979
|
+
const baseDir = pathService.dirname(sourceFileName);
|
|
980
|
+
const resolved = normalizeFileName(pathService.resolve(baseDir, cleaned));
|
|
981
|
+
const candidates = getResolutionCandidates(resolved, pathService);
|
|
982
|
+
for (const candidate of candidates) if (sourceFiles.has(candidate)) return candidate;
|
|
983
|
+
};
|
|
984
|
+
const resolveTSInputFile = (dependencyFileName, pathService) => {
|
|
741
985
|
if (!dependencyFileName.endsWith(".d.ts")) return dependencyFileName;
|
|
742
|
-
const
|
|
743
|
-
if (
|
|
744
|
-
const sourceMappingURL = getSourceMappingURL(
|
|
986
|
+
const content = fs.fileSystem.readFile?.(dependencyFileName);
|
|
987
|
+
if (typeof content !== "string") return dependencyFileName;
|
|
988
|
+
const sourceMappingURL = getSourceMappingURL(content);
|
|
745
989
|
if (!sourceMappingURL) return dependencyFileName;
|
|
746
|
-
const sourceMapFileName =
|
|
747
|
-
const
|
|
748
|
-
if (
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
const sources = Schema.decodeUnknownSync(Schema.Struct({ sources: Schema.optional(Schema.Array(Schema.String)) }))(JSON.parse(sourceMap.content)).sources;
|
|
990
|
+
const sourceMapFileName = normalizeFileName(pathService.resolve(pathService.dirname(dependencyFileName), sourceMappingURL));
|
|
991
|
+
const sourceMapContent = fs.fileSystem.readFile?.(sourceMapFileName);
|
|
992
|
+
if (typeof sourceMapContent !== "string") return dependencyFileName;
|
|
993
|
+
const rawMap = JSON.parse(sourceMapContent);
|
|
994
|
+
let sources;
|
|
995
|
+
if (Predicate.hasProperty(rawMap, "sources") && Array.isArray(rawMap.sources)) sources = rawMap.sources.filter((s) => typeof s === "string");
|
|
753
996
|
if (sources?.length === 1) {
|
|
754
997
|
const sourcePath = sources[0];
|
|
755
998
|
if (sourcePath === void 0) return dependencyFileName;
|
|
756
|
-
return
|
|
999
|
+
return normalizeFileName(pathService.resolve(pathService.dirname(sourceMapFileName), sourcePath));
|
|
757
1000
|
}
|
|
758
1001
|
return dependencyFileName;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
//#region src/typescript-checker.ts
|
|
770
|
-
const typescriptCheckerLoggerFactory = Object.assign((loggerFactory, target) => {
|
|
771
|
-
const targetName = target?.name ?? TypescriptChecker.name;
|
|
772
|
-
return loggerFactory(targetName === TypescriptChecker.name ? TypescriptChecker.name : `${TypescriptChecker.name}.${targetName}`);
|
|
773
|
-
}, { inject: tokens(commonTokens.getLogger, commonTokens.target) });
|
|
774
|
-
const create = Object.assign((injector) => injector.provideFactory(commonTokens.logger, typescriptCheckerLoggerFactory, Scope.Transient).provideClass("fs", HybridFileSystem).provideClass(tsCompiler, TypescriptCompiler).injectClass(TypescriptChecker), { inject: tokens(commonTokens.injector) });
|
|
775
|
-
var TypescriptChecker = class {
|
|
776
|
-
logger;
|
|
777
|
-
tsCompiler;
|
|
778
|
-
static inject = tokens(commonTokens.logger, commonTokens.options, tsCompiler);
|
|
779
|
-
options;
|
|
780
|
-
constructor(logger, options, tsCompiler) {
|
|
781
|
-
this.logger = logger;
|
|
782
|
-
this.tsCompiler = tsCompiler;
|
|
783
|
-
this.options = options;
|
|
784
|
-
}
|
|
785
|
-
async init() {
|
|
786
|
-
const errors = await this.tsCompiler.init();
|
|
787
|
-
if (errors.length) throw new Error(`Typescript error(s) found in dry run compilation: ${this.createErrorText(errors)}`);
|
|
788
|
-
}
|
|
789
|
-
async check(mutants) {
|
|
790
|
-
const result = Object.fromEntries(mutants.map((mutant) => [mutant.id, { status: CheckStatus.Passed }]));
|
|
791
|
-
const firstMutant = mutants[0];
|
|
792
|
-
if (!firstMutant || !this.tsCompiler.nodes.get(toPosixFileName(firstMutant.fileName))) return result;
|
|
793
|
-
const mutantErrorRelationMap = await this.checkErrors(mutants, {}, this.tsCompiler.nodes);
|
|
794
|
-
for (const [id, errors] of Object.entries(mutantErrorRelationMap)) result[id] = {
|
|
795
|
-
status: CheckStatus.CompileError,
|
|
796
|
-
reason: this.createErrorText(errors)
|
|
797
|
-
};
|
|
798
|
-
return result;
|
|
799
|
-
}
|
|
800
|
-
group(mutants) {
|
|
801
|
-
if (!this.options.typescriptChecker?.prioritizePerformanceOverAccuracy) return Promise.resolve(mutants.map((m) => [m.id]));
|
|
802
|
-
const { nodes } = this.tsCompiler;
|
|
803
|
-
const [mutantsOutsideProject, mutantsInProject] = split(mutants, (m) => nodes.get(toPosixFileName(m.fileName)) == null);
|
|
804
|
-
const groups = createGroups(mutantsInProject, nodes);
|
|
805
|
-
if (mutantsOutsideProject.length) return Promise.resolve([mutantsOutsideProject.map((m) => m.id), ...groups]);
|
|
806
|
-
else return Promise.resolve(groups);
|
|
807
|
-
}
|
|
808
|
-
async checkErrors(mutants, errorsMap, nodes) {
|
|
809
|
-
const errors = await this.tsCompiler.check(mutants);
|
|
810
|
-
const mutantsThatCouldNotBeTestedInGroups = /* @__PURE__ */ new Set();
|
|
811
|
-
if (errors.length && mutants.length === 1) {
|
|
812
|
-
const onlyMutant = mutants[0];
|
|
813
|
-
if (onlyMutant !== void 0) errorsMap[onlyMutant.id] = errors;
|
|
814
|
-
return errorsMap;
|
|
1002
|
+
};
|
|
1003
|
+
const buildDependencyGraph = (programs) => Effect.gen(function* () {
|
|
1004
|
+
const s = yield* Ref.get(stateRef);
|
|
1005
|
+
for (const program of programs) for (const fileName of program.getSourceFileNames()) {
|
|
1006
|
+
if (fileName.endsWith(".d.ts") || fileName.includes("node_modules")) continue;
|
|
1007
|
+
const normalized = normalizeFileName(fileName);
|
|
1008
|
+
s.sourceFiles.set(normalized, {
|
|
1009
|
+
fileName: normalized,
|
|
1010
|
+
imports: emptyStringSet()
|
|
1011
|
+
});
|
|
815
1012
|
}
|
|
816
|
-
for (const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
const mutantId = onlyRelatedMutant.id;
|
|
826
|
-
if (errorsMap[mutantId]) errorsMap[mutantId].push(error);
|
|
827
|
-
else errorsMap[mutantId] = [error];
|
|
1013
|
+
for (const [fileName] of s.sourceFiles) {
|
|
1014
|
+
const sourceFile = programs.map((p) => p.getSourceFile(fileName)).find((sf) => sf != null);
|
|
1015
|
+
if (!sourceFile) continue;
|
|
1016
|
+
const imports = extractImports(sourceFile);
|
|
1017
|
+
for (const specifier of imports) {
|
|
1018
|
+
const resolved = resolveModuleSpecifier(fileName, specifier, s.sourceFiles, pathService);
|
|
1019
|
+
if (resolved) {
|
|
1020
|
+
const sourceFileName = resolveTSInputFile(resolved, pathService);
|
|
1021
|
+
if (s.sourceFiles.has(sourceFileName)) s.sourceFiles.get(fileName)?.imports.add(sourceFileName);
|
|
828
1022
|
}
|
|
829
|
-
}
|
|
1023
|
+
}
|
|
830
1024
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1025
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
1026
|
+
...prev,
|
|
1027
|
+
sourceFiles: cloneMap(s.sourceFiles)
|
|
1028
|
+
}));
|
|
1029
|
+
});
|
|
1030
|
+
const getNodesEffect = Effect.gen(function* () {
|
|
1031
|
+
const s = yield* Ref.get(stateRef);
|
|
1032
|
+
if (s.nodes.size > 0) return s.nodes;
|
|
1033
|
+
for (const [fileName] of s.sourceFiles) {
|
|
1034
|
+
const node = makeTSFileNode(fileName);
|
|
1035
|
+
s.nodes.set(fileName, node);
|
|
835
1036
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1037
|
+
const withChildren = emptyNodes();
|
|
1038
|
+
for (const [fileName, file] of s.sourceFiles) {
|
|
1039
|
+
const node = s.nodes.get(fileName);
|
|
1040
|
+
if (node == null) return yield* new CompilerFailed({
|
|
1041
|
+
reason: "unknown-file-node",
|
|
1042
|
+
subject: fileName
|
|
1043
|
+
});
|
|
1044
|
+
const children = [...file.imports].map((importName) => s.nodes.get(importName)).filter((n) => n !== void 0);
|
|
1045
|
+
withChildren.set(fileName, {
|
|
1046
|
+
...node,
|
|
1047
|
+
children,
|
|
1048
|
+
parents: []
|
|
1049
|
+
});
|
|
849
1050
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1051
|
+
s.nodes.clear();
|
|
1052
|
+
for (const [k, v] of withChildren) s.nodes.set(k, v);
|
|
1053
|
+
const withParents = emptyNodes();
|
|
1054
|
+
for (const [fileName, node] of s.nodes) {
|
|
1055
|
+
const parents = [];
|
|
1056
|
+
for (const [, n] of s.nodes) if (n.children.includes(node)) parents.push(n);
|
|
1057
|
+
withParents.set(fileName, {
|
|
1058
|
+
...node,
|
|
1059
|
+
parents
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
s.nodes.clear();
|
|
1063
|
+
for (const [k, v] of withParents) s.nodes.set(k, v);
|
|
1064
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
1065
|
+
...prev,
|
|
1066
|
+
nodes: cloneMap(s.nodes)
|
|
1067
|
+
}));
|
|
1068
|
+
return s.nodes;
|
|
1069
|
+
});
|
|
1070
|
+
const check = (mutants) => Effect.gen(function* () {
|
|
1071
|
+
const state = yield* Ref.get(stateRef);
|
|
1072
|
+
for (const mutant of state.lastMutants) yield* fs.resetFile(mutant.fileName);
|
|
1073
|
+
for (const mutant of mutants) {
|
|
1074
|
+
if (!(yield* fs.getFile(mutant.fileName))) return yield* new CompilerFailed({
|
|
1075
|
+
reason: "file-not-in-project",
|
|
1076
|
+
subject: mutant.fileName
|
|
1077
|
+
});
|
|
1078
|
+
yield* fs.mutateFile(mutant.fileName, mutant);
|
|
1079
|
+
}
|
|
1080
|
+
const mutatedFileNames = unique(mutants.map((m) => normalizeFileName(m.fileName)));
|
|
1081
|
+
const changedFiles = unique([...state.lastMutatedFileNames, ...mutatedFileNames]);
|
|
1082
|
+
const current = yield* Ref.get(stateRef);
|
|
1083
|
+
if (current.api && current.snapshot) {
|
|
1084
|
+
const oldSnapshot = current.snapshot;
|
|
1085
|
+
const nextSnapshot = current.api.updateSnapshot({
|
|
1086
|
+
openProjects: [...current.allTSConfigFiles],
|
|
1087
|
+
fileChanges: { changed: changedFiles }
|
|
1088
|
+
});
|
|
1089
|
+
yield* Effect.sync(() => oldSnapshot.dispose());
|
|
1090
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
1091
|
+
...prev,
|
|
1092
|
+
snapshot: nextSnapshot
|
|
1093
|
+
}));
|
|
1094
|
+
}
|
|
1095
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
1096
|
+
...prev,
|
|
1097
|
+
lastMutants: [...mutants],
|
|
1098
|
+
lastMutatedFileNames: mutatedFileNames
|
|
1099
|
+
}));
|
|
1100
|
+
return (yield* getProgramsEffect()).flatMap((program) => [
|
|
1101
|
+
...program.getConfigFileParsingDiagnostics(),
|
|
1102
|
+
...program.getSemanticDiagnostics(),
|
|
1103
|
+
...program.getProgramDiagnostics()
|
|
1104
|
+
]).filter((diagnostic) => diagnostic.category === DiagnosticCategory.Error);
|
|
1105
|
+
});
|
|
1106
|
+
const init = Effect.gen(function* () {
|
|
1107
|
+
yield* guardTSVersion(fsService);
|
|
1108
|
+
const absoluteTsconfigFile = normalizeFileName(pathService.resolve(rawTsconfigFile));
|
|
1109
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
1110
|
+
...prev,
|
|
1111
|
+
tsconfigFile: absoluteTsconfigFile,
|
|
1112
|
+
allTSConfigFiles: setFromArray([absoluteTsconfigFile])
|
|
1113
|
+
}));
|
|
1114
|
+
yield* guardTSConfigFileExistsEffect;
|
|
1115
|
+
const buildModeEnabled = yield* determineBuildModeEnabled(absoluteTsconfigFile, fsService);
|
|
1116
|
+
yield* collectAllTSConfigFiles(buildModeEnabled);
|
|
1117
|
+
const s = yield* Ref.get(stateRef);
|
|
1118
|
+
const api = new API({ fs: fs.fileSystem });
|
|
1119
|
+
const snapshot = api.updateSnapshot({ openProjects: [...s.allTSConfigFiles] });
|
|
1120
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
1121
|
+
...prev,
|
|
1122
|
+
api,
|
|
1123
|
+
snapshot
|
|
1124
|
+
}));
|
|
1125
|
+
const programs = yield* getProgramsEffect();
|
|
1126
|
+
yield* buildDependencyGraph(programs);
|
|
1127
|
+
return yield* check([]);
|
|
1128
|
+
});
|
|
1129
|
+
const close = Effect.gen(function* () {
|
|
1130
|
+
const s = yield* Ref.get(stateRef);
|
|
1131
|
+
yield* Effect.sync(() => s.snapshot?.dispose());
|
|
1132
|
+
yield* Effect.sync(() => s.api?.close());
|
|
1133
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
1134
|
+
...prev,
|
|
1135
|
+
snapshot: void 0,
|
|
1136
|
+
api: void 0
|
|
1137
|
+
}));
|
|
1138
|
+
});
|
|
1139
|
+
const getLineAndCharacterOfPosition = (fileName, position) => Effect.gen(function* () {
|
|
1140
|
+
const programs = yield* getProgramsEffect();
|
|
1141
|
+
for (const program of programs) {
|
|
1142
|
+
const sourceFile = program.getSourceFile(fileName);
|
|
1143
|
+
if (sourceFile) return sourceFile.getLineAndCharacterOfPosition(position);
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
return {
|
|
1147
|
+
init,
|
|
1148
|
+
check,
|
|
1149
|
+
nodes: getNodesEffect,
|
|
1150
|
+
close,
|
|
1151
|
+
getLineAndCharacterOfPosition
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
853
1154
|
//#endregion
|
|
854
1155
|
//#region src/index.ts
|
|
855
|
-
const strykerPlugins = [
|
|
856
|
-
const
|
|
857
|
-
const
|
|
1156
|
+
const strykerPlugins = [declarePlugin(PluginKind.Checker, "typescript", Layer.effect(Checker, Effect.gen(function* () {
|
|
1157
|
+
const options = yield* RunConfiguration;
|
|
1158
|
+
const fsService = yield* FileSystem.FileSystem;
|
|
1159
|
+
const pathService = yield* Path.Path;
|
|
1160
|
+
return makeCheckerService({
|
|
1161
|
+
options,
|
|
1162
|
+
compiler: makeTypescriptCompiler(options, yield* makeHybridFileSystem(fsService), fsService, pathService)
|
|
1163
|
+
});
|
|
1164
|
+
})))];
|
|
1165
|
+
const rawSchema = JSON.parse(readFileSync(new URL("../schema/typescript-checker-options.json", import.meta.url), "utf-8"));
|
|
1166
|
+
if (!S.is(S.Record(S.String, S.Unknown))(rawSchema)) throw new Error("Invalid typescript-checker schema file");
|
|
1167
|
+
const strykerValidationSchema = rawSchema;
|
|
858
1168
|
//#endregion
|
|
859
|
-
export {
|
|
1169
|
+
export { strykerPlugins, strykerValidationSchema };
|