@systemfsoftware/stryker-js-typescript-checker 1.4.1 → 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 +95 -0
- package/dist/index.d.mts +2 -119
- package/dist/index.mjs +825 -462
- package/package.json +9 -9
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,168 @@
|
|
|
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
|
+
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";
|
|
4
9
|
import { EOL } from "os";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
10
|
+
import { Cell, Wire, Workflow } from "@systemfsoftware/effect-cell-types";
|
|
11
|
+
import { Mutant, errorToString } from "@systemfsoftware/stryker-js/Mutant";
|
|
12
|
+
import { Predicate, Result, Schema } from "effect";
|
|
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";
|
|
7
17
|
import { API, DiagnosticCategory } from "typescript/unstable/sync";
|
|
18
|
+
import * as Result$1 from "effect/Result";
|
|
8
19
|
import { createRequire } from "module";
|
|
9
|
-
import
|
|
10
|
-
import
|
|
20
|
+
import { StrykerOptionsSchema } from "@systemfsoftware/stryker-js/Schema";
|
|
21
|
+
import * as Context from "effect/Context";
|
|
22
|
+
import * as MutableHashSet from "effect/MutableHashSet";
|
|
23
|
+
import * as Ref from "effect/Ref";
|
|
11
24
|
import { SyntaxKind } from "typescript/unstable/ast";
|
|
25
|
+
//#region src/Checker.schema.ts
|
|
26
|
+
/**
|
|
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.
|
|
31
|
+
*/
|
|
32
|
+
var CheckMutantsCommand = class extends S.TaggedClass()("CheckMutantsCommand", { mutants: S.Array(Mutant) }) {};
|
|
33
|
+
/**
|
|
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.
|
|
37
|
+
*/
|
|
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
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/Checker.workflow.ts
|
|
58
|
+
/**
|
|
59
|
+
* Checker — pure check decision.
|
|
60
|
+
*
|
|
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.
|
|
64
|
+
*/
|
|
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 = {};
|
|
95
|
+
if (diagnostics.length > 0 && mutants.length === 1) {
|
|
96
|
+
const only = mutants[0];
|
|
97
|
+
if (only !== void 0) {
|
|
98
|
+
definitive[only.id] = [...diagnostics];
|
|
99
|
+
return Result$1.succeed({
|
|
100
|
+
definitive,
|
|
101
|
+
needsRetest: []
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const diagnostic of diagnostics) {
|
|
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({
|
|
110
|
+
text: diagnostic.text,
|
|
111
|
+
fileName
|
|
112
|
+
}));
|
|
113
|
+
const related = getMutantsWithReferenceToChildrenOrSelf(node, [...mutants]);
|
|
114
|
+
if (related.length === 0) for (const m of mutants) needsRetest[m.id] = m;
|
|
115
|
+
else if (related.length === 1) {
|
|
116
|
+
const only = related[0];
|
|
117
|
+
if (only !== void 0) {
|
|
118
|
+
const existing = definitive[only.id];
|
|
119
|
+
if (existing !== void 0) existing.push(diagnostic);
|
|
120
|
+
else definitive[only.id] = [diagnostic];
|
|
121
|
+
}
|
|
122
|
+
} else for (const m of related) needsRetest[m.id] = m;
|
|
123
|
+
}
|
|
124
|
+
const filteredRetest = Object.values(needsRetest).filter((m) => definitive[m.id] === void 0);
|
|
125
|
+
return Result$1.succeed({
|
|
126
|
+
definitive,
|
|
127
|
+
needsRetest: filteredRetest
|
|
128
|
+
});
|
|
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
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return Result$1.succeed(result);
|
|
150
|
+
};
|
|
151
|
+
const checkMutants = Workflow.make(CheckMutantsInput, (input) => buildResult(input));
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/Compiler.schema.ts
|
|
154
|
+
/**
|
|
155
|
+
* Compiler — declarations for the TypeScript compiler and version guard.
|
|
156
|
+
*/
|
|
157
|
+
/** The installed TypeScript version is below the supported floor. */
|
|
158
|
+
var UnsupportedTypeScriptVersionError = class extends Schema.TaggedError()("UnsupportedTypeScriptVersionError", { version: Schema.String }) {
|
|
159
|
+
get message() {
|
|
160
|
+
return `@systemfsoftware/stryker-js-typescript-checker only supports typescript@7.0.0 or higher. Found typescript@${this.version}`;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
/** Requested file is not present in the hybrid in-memory file map. */
|
|
164
|
+
var HybridFileNotFoundError = class extends Schema.TaggedError()("HybridFileNotFoundError", { fileName: Schema.String }) {};
|
|
165
|
+
//#endregion
|
|
12
166
|
//#region ../../../../../node_modules/.pnpm/@jsr+std__jsonc@1.0.2/node_modules/@jsr/std__jsonc/parse.js
|
|
13
167
|
/**
|
|
14
168
|
* Converts a JSON with Comments (JSONC) string into an object.
|
|
@@ -229,26 +383,43 @@ function buildErrorMessage({ type, sourceText, position }) {
|
|
|
229
383
|
return `Cannot parse JSONC: unexpected token "${token}" in JSONC at position ${position}`;
|
|
230
384
|
}
|
|
231
385
|
//#endregion
|
|
232
|
-
//#region src/
|
|
386
|
+
//#region src/Tsconfig.schema.ts
|
|
233
387
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
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.
|
|
236
392
|
*/
|
|
393
|
+
/** The configured tsconfig failed to parse or is not a shape this package can consume. */
|
|
237
394
|
var TsConfigParseError = class extends Schema.TaggedError()("TsConfigParseError", {
|
|
238
395
|
file: Schema.String,
|
|
239
396
|
reason: Schema.String
|
|
240
397
|
}) {};
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
398
|
+
/** The configured tsconfig file could not be read. */
|
|
399
|
+
var TsConfigNotFoundError = class extends Schema.TaggedError()("TsConfigNotFoundError", { file: Schema.String }) {
|
|
400
|
+
get message() {
|
|
401
|
+
return `The tsconfig file does not exist at: "${this.file}". Please configure the tsconfig file in your stryker.conf file using "tsconfigFile"`;
|
|
402
|
+
}
|
|
403
|
+
};
|
|
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
|
+
});
|
|
246
408
|
//#endregion
|
|
247
|
-
//#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, "/");
|
|
248
418
|
const COMPILER_OPTIONS_OVERRIDES = Object.freeze({
|
|
249
419
|
allowUnreachableCode: true,
|
|
250
420
|
noUnusedLocals: false,
|
|
251
|
-
noUnusedParameters: false
|
|
421
|
+
noUnusedParameters: false,
|
|
422
|
+
skipLibCheck: true
|
|
252
423
|
});
|
|
253
424
|
const NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT = Object.freeze({
|
|
254
425
|
noEmit: true,
|
|
@@ -260,19 +431,9 @@ const LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES = Object.freeze({
|
|
|
260
431
|
emitDeclarationOnly: true,
|
|
261
432
|
noEmit: false,
|
|
262
433
|
declarationMap: true,
|
|
263
|
-
declaration: true
|
|
434
|
+
declaration: true,
|
|
435
|
+
composite: true
|
|
264
436
|
});
|
|
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
437
|
/**
|
|
277
438
|
* Parses the raw text of a tsconfig file into a typed config, rejecting shapes this package cannot consume.
|
|
278
439
|
* @param fileName The tsconfig file name, used for error reporting
|
|
@@ -286,33 +447,40 @@ function parseTsConfig(fileName, jsonText) {
|
|
|
286
447
|
reason: error.message
|
|
287
448
|
}));
|
|
288
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
|
+
}
|
|
289
458
|
return Result.fail(new TsConfigParseError({
|
|
290
459
|
file: fileName,
|
|
291
|
-
reason
|
|
460
|
+
reason
|
|
292
461
|
}));
|
|
293
462
|
}
|
|
294
463
|
}
|
|
295
|
-
/**
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
*/
|
|
299
|
-
function determineBuildModeEnabled(tsconfigFileName) {
|
|
300
|
-
const parsed = parseTsConfig(tsconfigFileName, readFileSync(tsconfigFileName, "utf-8"));
|
|
464
|
+
/** Whether `--build` mode should be enabled based on `references` in the tsconfig. */
|
|
465
|
+
const determineBuildModeEnabled = (tsconfigFileName, fsService) => Effect.gen(function* () {
|
|
466
|
+
const parsed = parseTsConfig(tsconfigFileName, yield* fsService.readFileString(tsconfigFileName));
|
|
301
467
|
return Result.match(parsed, {
|
|
302
468
|
onFailure: () => false,
|
|
303
469
|
onSuccess: (config) => config.references !== void 0
|
|
304
470
|
});
|
|
305
|
-
}
|
|
471
|
+
});
|
|
306
472
|
/**
|
|
307
|
-
* Overrides
|
|
308
|
-
*
|
|
309
|
-
* @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.
|
|
310
475
|
*/
|
|
311
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;
|
|
312
480
|
const compilerOptions = {
|
|
313
481
|
...config.compilerOptions,
|
|
314
482
|
...COMPILER_OPTIONS_OVERRIDES,
|
|
315
|
-
...
|
|
483
|
+
...extraOptions
|
|
316
484
|
};
|
|
317
485
|
if (!useBuildMode && compilerOptions["declarationDir"] !== void 0 && compilerOptions["declarationDir"] !== null) delete compilerOptions["declarationDir"];
|
|
318
486
|
if (useBuildMode) {
|
|
@@ -322,99 +490,247 @@ function overrideOptions(config, useBuildMode) {
|
|
|
322
490
|
delete compilerOptions["sourceRoot"];
|
|
323
491
|
delete compilerOptions["outFile"];
|
|
324
492
|
}
|
|
325
|
-
return JSON.stringify({
|
|
493
|
+
if (useBuildMode) return JSON.stringify({
|
|
326
494
|
...config,
|
|
327
495
|
compilerOptions
|
|
328
496
|
});
|
|
497
|
+
const { references: _references, ...withoutReferences } = config;
|
|
498
|
+
return JSON.stringify({
|
|
499
|
+
...withoutReferences,
|
|
500
|
+
compilerOptions
|
|
501
|
+
});
|
|
329
502
|
}
|
|
330
503
|
/**
|
|
331
|
-
* Retrieves the referenced config files based on parsed configuration
|
|
332
|
-
* @param config The parsed config file
|
|
333
|
-
* @param fromDirName The directory where to resolve from
|
|
504
|
+
* Retrieves the referenced config files based on parsed configuration.
|
|
334
505
|
*/
|
|
335
|
-
function retrieveReferencedProjects(config, fromDirName) {
|
|
506
|
+
function retrieveReferencedProjects(config, fromDirName, pathService) {
|
|
336
507
|
return (config.references ?? []).map((reference) => {
|
|
337
|
-
let resolved =
|
|
338
|
-
if (!
|
|
339
|
-
return
|
|
508
|
+
let resolved = pathService.resolve(fromDirName, reference.path);
|
|
509
|
+
if (!pathService.basename(resolved).endsWith(".json")) resolved = pathService.join(resolved, "tsconfig.json");
|
|
510
|
+
return normalizeFileName$2(resolved);
|
|
340
511
|
});
|
|
341
512
|
}
|
|
513
|
+
//#endregion
|
|
514
|
+
//#region src/Compiler.ts
|
|
342
515
|
/**
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
|
|
347
|
-
|
|
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
|
|
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.
|
|
354
521
|
*/
|
|
522
|
+
const normalizeFileName$1 = (fileName) => fileName.replace(/\\/g, "/");
|
|
355
523
|
const findSourceMapRegex = /\/\/# sourceMappingURL=(.+)$/m;
|
|
356
524
|
function getSourceMappingURL(content) {
|
|
357
|
-
findSourceMapRegex.lastIndex = 0;
|
|
358
525
|
return findSourceMapRegex.exec(content)?.[1];
|
|
359
526
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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];
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
//#endregion
|
|
394
|
-
//#region src/grouping/create-groups.ts
|
|
527
|
+
let cachedTSVersion;
|
|
528
|
+
const getTSVersion = (fsService) => Effect.gen(function* () {
|
|
529
|
+
if (cachedTSVersion !== void 0) return cachedTSVersion;
|
|
530
|
+
const pkgPath = createRequire(import.meta.url).resolve("typescript/package.json");
|
|
531
|
+
const text = yield* fsService.readFileString(pkgPath);
|
|
532
|
+
const raw = JSON.parse(text);
|
|
533
|
+
let version = "";
|
|
534
|
+
if (Predicate.hasProperty(raw, "version") && typeof raw.version === "string") version = raw.version;
|
|
535
|
+
cachedTSVersion = version;
|
|
536
|
+
return version;
|
|
537
|
+
});
|
|
395
538
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
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.
|
|
400
|
-
*
|
|
401
|
-
* @param mutants All the mutants of the test project.
|
|
402
|
-
* @param nodes A graph representation of the test project.
|
|
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`.
|
|
403
541
|
*/
|
|
542
|
+
function isSupportedTypescriptVersion(version) {
|
|
543
|
+
const dashBase = version.split("-")[0] ?? version;
|
|
544
|
+
const parts = (dashBase.split("+")[0] ?? dashBase).split(".").map((p) => Number.parseInt(p, 10));
|
|
545
|
+
const major = parts[0] ?? 0;
|
|
546
|
+
const minor = parts[1] ?? 0;
|
|
547
|
+
const patch = parts[2] ?? 0;
|
|
548
|
+
if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch)) return false;
|
|
549
|
+
if (major !== 7) return major > 7;
|
|
550
|
+
if (minor !== 0) return minor > 0;
|
|
551
|
+
return patch >= 0;
|
|
552
|
+
}
|
|
553
|
+
const guardTSVersion = (fsService) => Effect.gen(function* () {
|
|
554
|
+
const version = yield* getTSVersion(fsService);
|
|
555
|
+
if (!isSupportedTypescriptVersion(version)) return yield* new UnsupportedTypeScriptVersionError({ version });
|
|
556
|
+
});
|
|
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
|
+
}
|
|
404
720
|
function createGroups(mutants, nodes) {
|
|
405
721
|
const groups = [];
|
|
406
|
-
const mutantsToGroup =
|
|
407
|
-
while (
|
|
722
|
+
const mutantsToGroup = MutableHashSet.fromIterable(mutants);
|
|
723
|
+
while (MutableHashSet.size(mutantsToGroup) > 0) {
|
|
408
724
|
const group = [];
|
|
409
|
-
const groupNodes =
|
|
410
|
-
const nodesToIgnore =
|
|
725
|
+
const groupNodes = MutableHashSet.empty();
|
|
726
|
+
const nodesToIgnore = MutableHashSet.empty();
|
|
411
727
|
for (const currentMutant of mutantsToGroup) {
|
|
412
728
|
const currentNode = findNode(currentMutant.fileName, nodes);
|
|
413
|
-
if (!
|
|
729
|
+
if (!MutableHashSet.has(nodesToIgnore, currentNode) && !parentsHaveOverlapWith(currentNode, groupNodes)) {
|
|
414
730
|
group.push(currentMutant.id);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
addRangeOfNodesToSet(nodesToIgnore,
|
|
731
|
+
MutableHashSet.add(groupNodes, currentNode);
|
|
732
|
+
MutableHashSet.remove(mutantsToGroup, currentMutant);
|
|
733
|
+
addRangeOfNodesToSet(nodesToIgnore, getAllParentReferencesIncludingSelf(currentNode));
|
|
418
734
|
}
|
|
419
735
|
}
|
|
420
736
|
groups.push(group);
|
|
@@ -422,264 +738,111 @@ function createGroups(mutants, nodes) {
|
|
|
422
738
|
return groups;
|
|
423
739
|
}
|
|
424
740
|
function addRangeOfNodesToSet(nodes, nodesToAdd) {
|
|
425
|
-
for (const parent of nodesToAdd)
|
|
741
|
+
for (const parent of nodesToAdd) MutableHashSet.add(nodes, parent);
|
|
426
742
|
}
|
|
427
743
|
function findNode(fileName, nodes) {
|
|
428
|
-
const
|
|
429
|
-
if (
|
|
430
|
-
|
|
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}`);
|
|
431
749
|
}
|
|
432
750
|
function parentsHaveOverlapWith(currentNode, groupNodes) {
|
|
433
|
-
for (const parentNode of
|
|
751
|
+
for (const parentNode of getAllParentReferencesIncludingSelf(currentNode)) if (MutableHashSet.has(groupNodes, parentNode)) return true;
|
|
434
752
|
return false;
|
|
435
753
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
};
|
|
480
|
-
//#endregion
|
|
481
|
-
//#region src/project/hybrid-file-system.ts
|
|
482
|
-
/**
|
|
483
|
-
* A very simple hybrid file system.
|
|
484
|
-
* * Readonly from disk
|
|
485
|
-
* * Writes in-memory
|
|
486
|
-
* * Hard caching
|
|
487
|
-
* * Ability to mutate one file
|
|
488
|
-
*/
|
|
489
|
-
var HybridFileSystem = class {
|
|
490
|
-
files = /* @__PURE__ */ new Map();
|
|
491
|
-
/**
|
|
492
|
-
* Map of absolute tsconfig file paths to their adjusted JSON content.
|
|
493
|
-
* This allows the TS7 API to read overridden compiler options.
|
|
494
|
-
*/
|
|
495
|
-
tsConfigOverrides = /* @__PURE__ */ new Map();
|
|
496
|
-
readFile = (fileName) => {
|
|
497
|
-
const normalized = toPosixFileName(fileName);
|
|
498
|
-
if (this.fileNameIsBuildInfo(normalized)) return null;
|
|
499
|
-
const override = this.tsConfigOverrides.get(normalized);
|
|
500
|
-
if (override !== void 0) return override;
|
|
501
|
-
const file = this.files.get(normalized);
|
|
502
|
-
if (file) return file.content;
|
|
503
|
-
if (file === void 0 && this.files.has(normalized)) return null;
|
|
754
|
+
var TypeScriptCompiler = class extends Context.Service()("@systemfsoftware/stryker-js-typescript-checker/TypeScriptCompiler") {};
|
|
755
|
+
const makeDummy = Effect.gen(function* () {
|
|
756
|
+
const stateRef = yield* Ref.make({
|
|
757
|
+
api: void 0,
|
|
758
|
+
snapshot: void 0,
|
|
759
|
+
sourceFiles: MutableHashMap.empty(),
|
|
760
|
+
nodes: MutableHashMap.empty(),
|
|
761
|
+
lastMutants: [],
|
|
762
|
+
lastMutatedFileNames: [],
|
|
763
|
+
allTSConfigFiles: MutableHashSet.fromIterable(["tsconfig.json"]),
|
|
764
|
+
tsconfigFile: "tsconfig.json"
|
|
765
|
+
});
|
|
766
|
+
yield* Effect.addFinalizer(() => Effect.gen(function* () {
|
|
767
|
+
const s = yield* Ref.get(stateRef);
|
|
768
|
+
yield* Effect.sync(() => s.snapshot?.dispose());
|
|
769
|
+
yield* Effect.sync(() => s.api?.close());
|
|
770
|
+
}));
|
|
771
|
+
return {
|
|
772
|
+
init: Effect.succeed([]),
|
|
773
|
+
check: () => Effect.succeed([]),
|
|
774
|
+
nodes: Ref.get(stateRef).pipe(Effect.map((s) => s.nodes)),
|
|
775
|
+
close: Effect.gen(function* () {
|
|
776
|
+
const s = yield* Ref.get(stateRef);
|
|
777
|
+
yield* Effect.sync(() => s.snapshot?.dispose());
|
|
778
|
+
yield* Effect.sync(() => s.api?.close());
|
|
779
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
780
|
+
...prev,
|
|
781
|
+
snapshot: void 0,
|
|
782
|
+
api: void 0
|
|
783
|
+
}));
|
|
784
|
+
}),
|
|
785
|
+
getLineAndCharacterOfPosition: () => Effect.succeed(void 0)
|
|
504
786
|
};
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
787
|
+
});
|
|
788
|
+
Layer.effect(TypeScriptCompiler)(makeDummy);
|
|
789
|
+
function makeTypescriptCompiler(options, fs, fsService, pathService) {
|
|
790
|
+
if (!S.is(StrykerOptionsSchema)(options)) throw new Error("Invalid StrykerOptions");
|
|
791
|
+
const rawTsconfigFile = normalizeFileName$1(options.tsconfigFile);
|
|
792
|
+
const initialState = {
|
|
793
|
+
api: void 0,
|
|
794
|
+
snapshot: void 0,
|
|
795
|
+
sourceFiles: MutableHashMap.empty(),
|
|
796
|
+
nodes: MutableHashMap.empty(),
|
|
797
|
+
lastMutants: [],
|
|
798
|
+
lastMutatedFileNames: [],
|
|
799
|
+
allTSConfigFiles: MutableHashSet.fromIterable([rawTsconfigFile]),
|
|
800
|
+
tsconfigFile: rawTsconfigFile
|
|
510
801
|
};
|
|
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}`);
|
|
802
|
+
const stateRef = Ref.makeUnsafe(initialState);
|
|
803
|
+
const getProgramsEffect = () => Effect.gen(function* () {
|
|
804
|
+
const s = yield* Ref.get(stateRef);
|
|
805
|
+
if (!s.snapshot) return yield* new CompilerFailed({ reason: "not-initialized" });
|
|
806
|
+
const projects = s.snapshot.getProjects();
|
|
807
|
+
if (projects.length === 0) return yield* new CompilerFailed({
|
|
808
|
+
reason: "no-projects",
|
|
809
|
+
subject: s.tsconfigFile
|
|
810
|
+
});
|
|
635
811
|
return projects.map((project) => project.program);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
812
|
+
});
|
|
813
|
+
const guardTSConfigFileExistsEffect = Effect.gen(function* () {
|
|
814
|
+
const s = yield* Ref.get(stateRef);
|
|
815
|
+
yield* fsService.readFileString(s.tsconfigFile).pipe(Effect.mapError(() => new TsConfigNotFoundError({ file: s.tsconfigFile })));
|
|
816
|
+
});
|
|
817
|
+
const collectAllTSConfigFiles = (buildModeEnabled) => Effect.gen(function* () {
|
|
818
|
+
const s = yield* Ref.get(stateRef);
|
|
819
|
+
const tsConfigOverrides = MutableHashMap.empty();
|
|
820
|
+
const toProcess = [s.tsconfigFile];
|
|
821
|
+
const processed = MutableHashSet.empty();
|
|
641
822
|
while (toProcess.length > 0) {
|
|
642
823
|
const current = toProcess.pop();
|
|
643
|
-
if (
|
|
644
|
-
|
|
645
|
-
const content =
|
|
824
|
+
if (current === void 0 || current === "" || MutableHashSet.has(processed, current)) continue;
|
|
825
|
+
MutableHashSet.add(processed, current);
|
|
826
|
+
const content = yield* fsService.readFileString(current);
|
|
646
827
|
const parsed = parseTsConfig(current, content);
|
|
647
828
|
if (Result.isFailure(parsed)) {
|
|
648
|
-
|
|
649
|
-
tsConfigOverrides.set(current, content);
|
|
829
|
+
MutableHashMap.set(tsConfigOverrides, current, content);
|
|
650
830
|
continue;
|
|
651
831
|
}
|
|
652
|
-
|
|
653
|
-
for (const referenced of retrieveReferencedProjects(parsed.success,
|
|
654
|
-
|
|
832
|
+
MutableHashMap.set(tsConfigOverrides, current, overrideOptions(parsed.success, buildModeEnabled));
|
|
833
|
+
for (const referenced of retrieveReferencedProjects(parsed.success, pathService.dirname(current), pathService)) {
|
|
834
|
+
const normalized = normalizeFileName$1(referenced);
|
|
835
|
+
MutableHashSet.add(s.allTSConfigFiles, normalized);
|
|
655
836
|
toProcess.push(referenced);
|
|
656
837
|
}
|
|
657
838
|
}
|
|
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) {
|
|
839
|
+
yield* fs.setTsConfigOverrides(tsConfigOverrides);
|
|
840
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
841
|
+
...prev,
|
|
842
|
+
allTSConfigFiles: MutableHashSet.fromIterable(s.allTSConfigFiles)
|
|
843
|
+
}));
|
|
844
|
+
});
|
|
845
|
+
const extractImports = (sourceFile) => {
|
|
683
846
|
const result = [];
|
|
684
847
|
for (const statement of sourceFile.statements) if (statement.kind === SyntaxKind.ImportDeclaration) {
|
|
685
848
|
let spec;
|
|
@@ -695,17 +858,9 @@ var TypescriptCompiler = class {
|
|
|
695
858
|
for (const ref of sourceFile.referencedFiles) result.push(ref.fileName);
|
|
696
859
|
for (const ref of sourceFile.typeReferenceDirectives) result.push(ref.fileName);
|
|
697
860
|
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);
|
|
861
|
+
};
|
|
862
|
+
const getResolutionCandidates = (resolved, pathService) => {
|
|
863
|
+
const extension = pathService.extname(resolved);
|
|
709
864
|
if (extension) {
|
|
710
865
|
const withoutExt = resolved.slice(0, -extension.length);
|
|
711
866
|
return [
|
|
@@ -736,124 +891,332 @@ var TypescriptCompiler = class {
|
|
|
736
891
|
`${resolved}/index.mjs`,
|
|
737
892
|
`${resolved}/index.cjs`
|
|
738
893
|
];
|
|
739
|
-
}
|
|
740
|
-
|
|
894
|
+
};
|
|
895
|
+
const resolveModuleSpecifier = (sourceFileName, specifier, sourceFiles, pathService) => {
|
|
896
|
+
const cleaned = specifier.replace(/^['"]|['"]$/g, "");
|
|
897
|
+
if (!cleaned.startsWith("./") && !cleaned.startsWith("../")) return;
|
|
898
|
+
const baseDir = pathService.dirname(sourceFileName);
|
|
899
|
+
const resolved = normalizeFileName$1(pathService.resolve(baseDir, cleaned));
|
|
900
|
+
const candidates = getResolutionCandidates(resolved, pathService);
|
|
901
|
+
for (const candidate of candidates) if (MutableHashMap.has(sourceFiles, candidate)) return candidate;
|
|
902
|
+
};
|
|
903
|
+
const resolveTSInputFile = (dependencyFileName, pathService) => {
|
|
741
904
|
if (!dependencyFileName.endsWith(".d.ts")) return dependencyFileName;
|
|
742
|
-
const
|
|
743
|
-
if (
|
|
744
|
-
const sourceMappingURL = getSourceMappingURL(
|
|
745
|
-
if (
|
|
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;
|
|
905
|
+
const content = fs.fileSystem.readFile?.(dependencyFileName);
|
|
906
|
+
if (typeof content !== "string") return dependencyFileName;
|
|
907
|
+
const sourceMappingURL = getSourceMappingURL(content);
|
|
908
|
+
if (sourceMappingURL === void 0 || sourceMappingURL === "") return dependencyFileName;
|
|
909
|
+
const sourceMapFileName = normalizeFileName$1(pathService.resolve(pathService.dirname(dependencyFileName), sourceMappingURL));
|
|
910
|
+
const sourceMapContent = fs.fileSystem.readFile?.(sourceMapFileName);
|
|
911
|
+
if (typeof sourceMapContent !== "string") return dependencyFileName;
|
|
912
|
+
const rawMap = JSON.parse(sourceMapContent);
|
|
913
|
+
let sources;
|
|
914
|
+
if (Predicate.hasProperty(rawMap, "sources") && Array.isArray(rawMap.sources)) sources = rawMap.sources.filter((s) => typeof s === "string");
|
|
753
915
|
if (sources?.length === 1) {
|
|
754
916
|
const sourcePath = sources[0];
|
|
755
917
|
if (sourcePath === void 0) return dependencyFileName;
|
|
756
|
-
return
|
|
918
|
+
return normalizeFileName$1(pathService.resolve(pathService.dirname(sourceMapFileName), sourcePath));
|
|
757
919
|
}
|
|
758
920
|
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;
|
|
921
|
+
};
|
|
922
|
+
const buildDependencyGraph = (programs) => Effect.gen(function* () {
|
|
923
|
+
const s = yield* Ref.get(stateRef);
|
|
924
|
+
for (const program of programs) for (const fileName of program.getSourceFileNames()) {
|
|
925
|
+
if (fileName.endsWith(".d.ts") || fileName.includes("node_modules")) continue;
|
|
926
|
+
const normalized = normalizeFileName$1(fileName);
|
|
927
|
+
MutableHashMap.set(s.sourceFiles, normalized, {
|
|
928
|
+
fileName: normalized,
|
|
929
|
+
imports: MutableHashSet.empty()
|
|
930
|
+
});
|
|
815
931
|
}
|
|
816
|
-
for (const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
932
|
+
for (const [fileName] of s.sourceFiles) {
|
|
933
|
+
const sourceFile = programs.map((p) => p.getSourceFile(fileName)).find((sf) => sf != null);
|
|
934
|
+
if (!sourceFile) continue;
|
|
935
|
+
const imports = extractImports(sourceFile);
|
|
936
|
+
for (const specifier of imports) {
|
|
937
|
+
const resolved = resolveModuleSpecifier(fileName, specifier, s.sourceFiles, pathService);
|
|
938
|
+
if (resolved !== void 0 && resolved !== "") {
|
|
939
|
+
const sourceFileName = resolveTSInputFile(resolved, pathService);
|
|
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
|
+
}
|
|
828
944
|
}
|
|
829
|
-
}
|
|
945
|
+
}
|
|
830
946
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
947
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
948
|
+
...prev,
|
|
949
|
+
sourceFiles: MutableHashMap.fromIterable(s.sourceFiles)
|
|
950
|
+
}));
|
|
951
|
+
});
|
|
952
|
+
const getNodesEffect = Effect.gen(function* () {
|
|
953
|
+
const s = yield* Ref.get(stateRef);
|
|
954
|
+
if (MutableHashMap.size(s.nodes) > 0) return s.nodes;
|
|
955
|
+
for (const [fileName] of s.sourceFiles) {
|
|
956
|
+
const node = makeTSFileNode(fileName);
|
|
957
|
+
MutableHashMap.set(s.nodes, fileName, node);
|
|
835
958
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
959
|
+
const withChildren = MutableHashMap.empty();
|
|
960
|
+
for (const [fileName, file] of s.sourceFiles) {
|
|
961
|
+
const nodeOpt = MutableHashMap.get(s.nodes, fileName);
|
|
962
|
+
if (Option.isNone(nodeOpt)) return yield* new CompilerFailed({
|
|
963
|
+
reason: "unknown-file-node",
|
|
964
|
+
subject: fileName
|
|
965
|
+
});
|
|
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, {
|
|
969
|
+
...node,
|
|
970
|
+
children,
|
|
971
|
+
parents: []
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
MutableHashMap.clear(s.nodes);
|
|
975
|
+
for (const [k, v] of withChildren) MutableHashMap.set(s.nodes, k, v);
|
|
976
|
+
const withParents = MutableHashMap.empty();
|
|
977
|
+
for (const [fileName, node] of s.nodes) {
|
|
978
|
+
const parents = [];
|
|
979
|
+
for (const [, n] of s.nodes) if (n.children.includes(node)) parents.push(n);
|
|
980
|
+
MutableHashMap.set(withParents, fileName, {
|
|
981
|
+
...node,
|
|
982
|
+
parents
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
MutableHashMap.clear(s.nodes);
|
|
986
|
+
for (const [k, v] of withParents) MutableHashMap.set(s.nodes, k, v);
|
|
987
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
988
|
+
...prev,
|
|
989
|
+
nodes: MutableHashMap.fromIterable(s.nodes)
|
|
990
|
+
}));
|
|
991
|
+
return s.nodes;
|
|
992
|
+
});
|
|
993
|
+
const check = (mutants) => Effect.gen(function* () {
|
|
994
|
+
const state = yield* Ref.get(stateRef);
|
|
995
|
+
for (const mutant of state.lastMutants) yield* fs.resetFile(mutant.fileName);
|
|
996
|
+
for (const mutant of mutants) {
|
|
997
|
+
if (!(yield* fs.getFile(mutant.fileName))) return yield* new CompilerFailed({
|
|
998
|
+
reason: "file-not-in-project",
|
|
999
|
+
subject: mutant.fileName
|
|
1000
|
+
});
|
|
1001
|
+
yield* fs.mutateFile(mutant.fileName, mutant);
|
|
1002
|
+
}
|
|
1003
|
+
const mutatedFileNames = Array.from(MutableHashSet.fromIterable(mutants.map((m) => normalizeFileName$1(m.fileName))));
|
|
1004
|
+
const changedFiles = Array.from(MutableHashSet.fromIterable([...state.lastMutatedFileNames, ...mutatedFileNames]));
|
|
1005
|
+
const current = yield* Ref.get(stateRef);
|
|
1006
|
+
if (current.api && current.snapshot) {
|
|
1007
|
+
const oldSnapshot = current.snapshot;
|
|
1008
|
+
const nextSnapshot = current.api.updateSnapshot({
|
|
1009
|
+
openProjects: Array.from(current.allTSConfigFiles),
|
|
1010
|
+
fileChanges: { changed: changedFiles }
|
|
1011
|
+
});
|
|
1012
|
+
yield* Effect.sync(() => oldSnapshot.dispose());
|
|
1013
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
1014
|
+
...prev,
|
|
1015
|
+
snapshot: nextSnapshot
|
|
1016
|
+
}));
|
|
1017
|
+
}
|
|
1018
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
1019
|
+
...prev,
|
|
1020
|
+
lastMutants: [...mutants],
|
|
1021
|
+
lastMutatedFileNames: mutatedFileNames
|
|
1022
|
+
}));
|
|
1023
|
+
return (yield* getProgramsEffect()).flatMap((program) => [
|
|
1024
|
+
...program.getConfigFileParsingDiagnostics(),
|
|
1025
|
+
...program.getSemanticDiagnostics(),
|
|
1026
|
+
...program.getProgramDiagnostics()
|
|
1027
|
+
]).filter((diagnostic) => diagnostic.category === DiagnosticCategory.Error);
|
|
1028
|
+
});
|
|
1029
|
+
const init = Effect.gen(function* () {
|
|
1030
|
+
yield* guardTSVersion(fsService);
|
|
1031
|
+
const absoluteTsconfigFile = normalizeFileName$1(pathService.resolve(rawTsconfigFile));
|
|
1032
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
1033
|
+
...prev,
|
|
1034
|
+
tsconfigFile: absoluteTsconfigFile,
|
|
1035
|
+
allTSConfigFiles: MutableHashSet.fromIterable([absoluteTsconfigFile])
|
|
1036
|
+
}));
|
|
1037
|
+
yield* guardTSConfigFileExistsEffect;
|
|
1038
|
+
const buildModeEnabled = yield* determineBuildModeEnabled(absoluteTsconfigFile, fsService);
|
|
1039
|
+
yield* collectAllTSConfigFiles(buildModeEnabled);
|
|
1040
|
+
const s = yield* Ref.get(stateRef);
|
|
1041
|
+
const api = new API({ fs: fs.fileSystem });
|
|
1042
|
+
const snapshot = api.updateSnapshot({ openProjects: Array.from(s.allTSConfigFiles) });
|
|
1043
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
1044
|
+
...prev,
|
|
1045
|
+
api,
|
|
1046
|
+
snapshot
|
|
1047
|
+
}));
|
|
1048
|
+
const programs = yield* getProgramsEffect();
|
|
1049
|
+
yield* buildDependencyGraph(programs);
|
|
1050
|
+
return yield* check([]);
|
|
1051
|
+
});
|
|
1052
|
+
const close = Effect.gen(function* () {
|
|
1053
|
+
const s = yield* Ref.get(stateRef);
|
|
1054
|
+
yield* Effect.sync(() => s.snapshot?.dispose());
|
|
1055
|
+
yield* Effect.sync(() => s.api?.close());
|
|
1056
|
+
yield* Ref.update(stateRef, (prev) => ({
|
|
1057
|
+
...prev,
|
|
1058
|
+
snapshot: void 0,
|
|
1059
|
+
api: void 0
|
|
1060
|
+
}));
|
|
1061
|
+
});
|
|
1062
|
+
const getLineAndCharacterOfPosition = (fileName, position) => Effect.gen(function* () {
|
|
1063
|
+
const programs = yield* getProgramsEffect();
|
|
1064
|
+
for (const program of programs) {
|
|
1065
|
+
const sourceFile = program.getSourceFile(fileName);
|
|
1066
|
+
if (sourceFile) return sourceFile.getLineAndCharacterOfPosition(position);
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
return {
|
|
1070
|
+
init,
|
|
1071
|
+
check,
|
|
1072
|
+
nodes: getNodesEffect,
|
|
1073
|
+
close,
|
|
1074
|
+
getLineAndCharacterOfPosition
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
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);
|
|
840
1146
|
}
|
|
841
|
-
|
|
842
|
-
|
|
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";
|
|
843
1155
|
let location = "";
|
|
844
|
-
|
|
845
|
-
|
|
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));
|
|
846
1163
|
const line = (lineAndCharacter?.line ?? 0) + 1;
|
|
847
1164
|
const character = (lineAndCharacter?.character ?? 0) + 1;
|
|
848
1165
|
location = `${error.fileName}(${line},${character}): `;
|
|
849
1166
|
}
|
|
850
1167
|
return `${location}${severity} TS${error.code}: ${error.text}`;
|
|
851
|
-
}
|
|
852
|
-
|
|
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
|
+
}
|
|
853
1207
|
//#endregion
|
|
854
1208
|
//#region src/index.ts
|
|
855
|
-
const strykerPlugins = [
|
|
856
|
-
const
|
|
857
|
-
const
|
|
1209
|
+
const strykerPlugins = [declarePlugin("Checker", "typescript", Layer.effect(Checker, Effect.gen(function* () {
|
|
1210
|
+
const options = yield* RunConfiguration;
|
|
1211
|
+
const fsService = yield* FileSystem.FileSystem;
|
|
1212
|
+
const pathService = yield* Path.Path;
|
|
1213
|
+
return makeCheckerService({
|
|
1214
|
+
options,
|
|
1215
|
+
compiler: makeTypescriptCompiler(options, yield* makeHybridFileSystem(fsService), fsService, pathService)
|
|
1216
|
+
});
|
|
1217
|
+
})))];
|
|
1218
|
+
const rawSchema = JSON.parse(readFileSync(new URL("../schema/typescript-checker-options.json", import.meta.url), "utf-8"));
|
|
1219
|
+
if (!S.is(S.Record(S.String, S.Unknown))(rawSchema)) throw new Error("Invalid typescript-checker schema file");
|
|
1220
|
+
const strykerValidationSchema = rawSchema;
|
|
858
1221
|
//#endregion
|
|
859
|
-
export {
|
|
1222
|
+
export { strykerPlugins, strykerValidationSchema };
|