@systemfsoftware/stryker-js-instrumenter 1.0.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/dist/index.mjs CHANGED
@@ -1,15 +1,88 @@
1
+ import babel, { File } from "@babel/core";
2
+ import generator from "@babel/generator";
3
+ import { Cell, Wire, Workflow } from "@systemfsoftware/effect-cell-types";
4
+ import { INSTRUMENTER_CONSTANTS, Mutant } from "@systemfsoftware/stryker-js/Mutant";
5
+ import * as Effect from "effect/Effect";
6
+ import { pipe } from "effect/Function";
1
7
  import * as Predicate from "effect/Predicate";
2
- import { INSTRUMENTER_CONSTANTS, propertyPath, strykerReportBugUrl } from "@systemfsoftware/stryker-js-plugin-api/core";
3
- import path from "path";
8
+ import * as Result from "effect/Result";
4
9
  import * as S from "effect/Schema";
5
- import babel, { File, types } from "@babel/core";
6
10
  import { createRequire } from "module";
11
+ import path from "path";
12
+ import "@systemfsoftware/stryker-js/Ignorer";
13
+ import { propertyPath, strykerReportBugUrl } from "@systemfsoftware/stryker-js/Schema";
7
14
  import * as Option from "effect/Option";
8
- import "@systemfsoftware/stryker-js-plugin-api/ignore";
9
- import generator from "@babel/generator";
15
+ import path$1 from "node:path";
10
16
  import { RegExpParser, visitRegExpAST } from "@eslint-community/regexpp";
11
- import * as Effect from "effect/Effect";
12
- //#region src/parsers/parse-error.schema.ts
17
+ //#region src/Instrument.workflow.ts
18
+ /**
19
+ * Instrument.workflow — pure decision for the instrument capability.
20
+ *
21
+ * The heavy lifting (Babel traversal, mutant collection, code generation)
22
+ * happens in `Instrument.ts`'s read phase, which is allowed to be stateful.
23
+ * This decision receives the already-collected mutants as data and wraps them
24
+ * into the decision type. All helpers and schemas live in this file so the
25
+ * decision body only reaches parameters, const locals, same-file declarations,
26
+ * and the sealed pure effect surface.
27
+ */
28
+ var InstrumentError = class extends S.TaggedError("@systemfsoftware/stryker-js-instrumenter/InstrumentError")("InstrumentError", {
29
+ message: S.String,
30
+ cause: S.Defect()
31
+ }) {
32
+ get message() {
33
+ if (this.message.length === 0) return "Instrumenter failure";
34
+ return this.message;
35
+ }
36
+ };
37
+ const PositionSchema = S.Struct({
38
+ line: S.Finite,
39
+ column: S.Finite
40
+ });
41
+ const RangeSchema = S.Struct({
42
+ start: PositionSchema,
43
+ end: PositionSchema
44
+ });
45
+ const MutateDescriptionSchema = S.Union([S.Boolean, S.Array(RangeSchema)]);
46
+ const FileSchema = S.Struct({
47
+ name: S.String,
48
+ content: S.String,
49
+ mutate: MutateDescriptionSchema
50
+ });
51
+ const PluginSchema = Wire.mint(S.Unknown);
52
+ const IgnorerSchema = Wire.mint(S.Unknown);
53
+ const AstSchema = Wire.mint(S.Unknown);
54
+ const InstrumenterOptionsSchema = Wire.wire({
55
+ plugins: Wire.mint(S.NullOr(Wire.mint(S.Array(PluginSchema)))),
56
+ excludedMutations: Wire.mint(S.Array(Wire.mint(S.String))),
57
+ ignorers: Wire.mint(S.Array(IgnorerSchema)),
58
+ noHeader: Wire.mint(S.optional(Wire.mint(S.Boolean)))
59
+ });
60
+ var InstrumentCommand = class extends S.TaggedClass()("InstrumentCommand", {
61
+ files: S.Array(FileSchema),
62
+ options: InstrumenterOptionsSchema
63
+ }) {};
64
+ var InstrumentDecoded = class extends S.TaggedClass()("InstrumentDecoded", {
65
+ files: S.Array(FileSchema),
66
+ options: InstrumenterOptionsSchema,
67
+ asts: S.Array(AstSchema),
68
+ mutants: S.Array(Mutant)
69
+ }) {};
70
+ var InstrumentDecision = class extends S.TaggedClass()("InstrumentDecision", {
71
+ files: S.Array(FileSchema),
72
+ mutants: S.Array(Mutant),
73
+ asts: S.Array(AstSchema)
74
+ }) {};
75
+ var InstrumentResult = class extends S.TaggedClass()("InstrumentResult", {
76
+ files: S.Array(FileSchema),
77
+ mutants: S.Array(Mutant)
78
+ }) {};
79
+ const instrumentWorkflow = Workflow.make(InstrumentDecoded, (decoded) => Result.succeed(InstrumentDecision.make({
80
+ files: decoded.files,
81
+ mutants: decoded.mutants,
82
+ asts: decoded.asts
83
+ })));
84
+ //#endregion
85
+ //#region src/Parser.schema.ts
13
86
  var ParseFailed = class extends S.TaggedError("@systemfsoftware/stryker-js-instrumenter/ParseFailed")("ParseFailed", {
14
87
  fileName: S.String,
15
88
  message: S.String,
@@ -28,178 +101,54 @@ S.TaggedError("@systemfsoftware/stryker-js-instrumenter/ParserNotFound")("Parser
28
101
  extension: S.String,
29
102
  cause: S.Defect()
30
103
  });
31
- //#endregion
32
- //#region src/parsers/html-parser.ts
33
- const TSX_SCRIPT_TYPES = Object.freeze(["tsx", "text/tsx"]);
34
- const TS_SCRIPT_TYPES = Object.freeze([
35
- "ts",
36
- "text/typescript",
37
- "typescript"
38
- ]);
39
- const JS_SCRIPT_TYPES = Object.freeze([
40
- "js",
41
- "text/javascript",
42
- "javascript",
43
- "module"
44
- ]);
45
- async function parse$2(text, originFileName, context) {
46
- return {
47
- originFileName,
48
- rawContent: text,
49
- format: "html",
50
- root: await ngHtmlParser(text, originFileName, context)
51
- };
52
- }
53
- async function ngHtmlParser(text, fileName, parserContext) {
54
- const ngParser = await import("angular-html-parser");
55
- const { rootNodes, errors } = ngParser.parse(text, {
56
- canSelfClose: true,
57
- allowHtmComponentClosingTags: true,
58
- isTagNameCaseSensitive: true
59
- });
60
- if (errors.length !== 0) {
61
- const firstError = errors[0];
62
- if (firstError === void 0) throw new ParseFailed({
63
- fileName,
64
- message: "HTML parser reported errors but first error is missing",
65
- location: {
66
- line: 0,
67
- column: 0
68
- },
69
- cause: errors
70
- });
71
- throw new ParseFailed({
72
- fileName,
73
- message: firstError.msg,
74
- location: toSourceLocation(firstError.span.start),
75
- cause: firstError
76
- });
77
- }
78
- const scriptsAsPromised = [];
79
- const scriptCollector = {
80
- visitElement: (el, context) => {
81
- const scriptFormat = getScriptType(el);
82
- if (scriptFormat) scriptsAsPromised.push(parseScript(el, scriptFormat));
83
- ngParser.visitAll(scriptCollector, el.children, context);
84
- },
85
- visitAttribute: () => void 0,
86
- visitText: () => void 0,
87
- visitComment: () => void 0,
88
- visitDocType: () => void 0,
89
- visitExpansion: () => void 0,
90
- visitExpansionCase: () => void 0,
91
- visitBlock: () => void 0,
92
- visitBlockParameter: () => void 0,
93
- visitLetDeclaration: () => void 0,
94
- visitCdata: () => void 0,
95
- visitComponent: () => void 0,
96
- visitDirective: () => void 0
97
- };
98
- ngParser.visitAll(scriptCollector, rootNodes);
99
- return { scripts: await Promise.all(scriptsAsPromised) };
100
- async function parseScript(el, scriptFormat) {
101
- const endSourceSpan = el.endSourceSpan;
102
- if (endSourceSpan === null || endSourceSpan === void 0) throw new Error("HTML element without an end source span");
103
- const content = text.substring(el.startSourceSpan.end.offset, endSourceSpan.start.offset);
104
- const ast = await parserContext.parse(content, fileName, scriptFormat);
105
- if (ast) {
106
- const offset = el.startSourceSpan.end;
107
- const rootStart = ast.root.start;
108
- if (rootStart === null || rootStart === void 0) throw new Error("Babel File node without a start offset");
109
- const rootEnd = ast.root.end;
110
- if (rootEnd === null || rootEnd === void 0) throw new Error("Babel File node without an end offset");
111
- ast.root.start = rootStart + offset.offset;
112
- ast.root.end = rootEnd + offset.offset;
113
- return {
114
- ...ast,
115
- offset: {
116
- column: offset.offset,
117
- line: offset.line
118
- }
119
- };
120
- }
121
- return ast;
104
+ var SvelteVersionNotSupported = class extends S.TaggedError("@systemfsoftware/stryker-js-instrumenter/SvelteVersionNotSupported")("SvelteVersionNotSupported", {
105
+ version: S.String,
106
+ fileName: S.String,
107
+ cause: S.Defect()
108
+ }) {
109
+ get message() {
110
+ return `Svelte version ${this.version} is not supported for ${this.fileName} (expected >=3.30)`;
122
111
  }
123
- }
124
- function toSourceLocation({ line, col }) {
125
- return {
126
- line: line + 1,
127
- column: col
128
- };
129
- }
130
- function getScriptType(element) {
131
- if (element.name === "script") {
132
- if (!element.attrs.some((attr) => attr.name === "src")) {
133
- const type = element.attrs.find((attr) => attr.name === "type") ?? element.attrs.find((attr) => attr.name === "lang");
134
- if (type) {
135
- const typeToLower = type.value.toLowerCase();
136
- if (TSX_SCRIPT_TYPES.includes(typeToLower)) return "tsx";
137
- if (TS_SCRIPT_TYPES.includes(typeToLower)) return "ts";
138
- if (JS_SCRIPT_TYPES.includes(typeToLower)) return "js";
139
- } else return "js";
140
- }
112
+ };
113
+ var SvelteWalkerNotFound = class extends S.TaggedError("@systemfsoftware/stryker-js-instrumenter/SvelteWalkerNotFound")("SvelteWalkerNotFound", {
114
+ fileName: S.String,
115
+ cause: S.Defect()
116
+ }) {
117
+ get message() {
118
+ return `Svelte walker not found for ${this.fileName}`;
141
119
  }
142
- }
120
+ };
121
+ S.TaggedError("@systemfsoftware/stryker-js-instrumenter/SvelteParseFailed")("SvelteParseFailed", {
122
+ fileName: S.String,
123
+ cause: S.Defect()
124
+ });
125
+ S.Literals([
126
+ "html",
127
+ "js",
128
+ "ts",
129
+ "tsx",
130
+ "svelte"
131
+ ]);
143
132
  //#endregion
144
- //#region src/parsers/js-parser.ts
145
- const { types: types$20, parseAsync: parseAsync$1 } = babel;
146
- function isParserPlugin(value) {
147
- return typeof value === "string" || Array.isArray(value);
148
- }
149
- function isParserPluginArray(value) {
150
- return value.every(isParserPlugin);
151
- }
152
- function getParserPlugins(override) {
153
- if (override === null || override === void 0) return defaultPlugins;
154
- if (isParserPluginArray(override)) return override;
155
- throw new Error("Invalid parser plugins: expected ParserPlugin[]");
133
+ //#region src/Syntax.ts
134
+ /**
135
+ * Determines if a location (needle) is included in an other location (haystack)
136
+ * @param haystack The range to look in
137
+ * @param needle the range to search for
138
+ */
139
+ function locationIncluded(haystack, needle) {
140
+ const startIncluded = haystack.start.line < needle.start.line || haystack.start.line === needle.start.line && haystack.start.column <= needle.start.column;
141
+ const endIncluded = haystack.end.line > needle.end.line || haystack.end.line === needle.end.line && haystack.end.column >= needle.end.column;
142
+ return startIncluded && endIncluded;
156
143
  }
157
- const defaultPlugins = [
158
- "doExpressions",
159
- "objectRestSpread",
160
- "classProperties",
161
- "exportDefaultFrom",
162
- "exportNamespaceFrom",
163
- "asyncGenerators",
164
- "functionBind",
165
- "functionSent",
166
- "dynamicImport",
167
- "numericSeparator",
168
- "importMeta",
169
- "optionalCatchBinding",
170
- "optionalChaining",
171
- "classPrivateProperties",
172
- ["pipelineOperator", { proposal: "minimal" }],
173
- "nullishCoalescingOperator",
174
- "bigInt",
175
- "throwExpressions",
176
- "logicalAssignment",
177
- "classPrivateMethods",
178
- "v8intrinsic",
179
- "partialApplication",
180
- ["decorators", { decoratorsBeforeExport: false }],
181
- "jsx"
182
- ];
183
- function createParser$1({ plugins: pluginsOverride }) {
184
- return async function parse(text, fileName) {
185
- const plugins = getParserPlugins(pluginsOverride);
186
- const ast = await parseAsync$1(text, {
187
- parserOpts: { plugins: [...plugins] },
188
- filename: fileName,
189
- sourceType: "module"
190
- });
191
- if (ast === null || ast === void 0) throw new Error(`Expected ${fileName} to contain a babel.types.file, but it yielded null`);
192
- if (types$20.isProgram(ast)) throw new Error(`Expected ${fileName} to contain a babel.types.file, but was a program`);
193
- return {
194
- originFileName: fileName,
195
- rawContent: text,
196
- format: "js",
197
- root: ast
198
- };
199
- };
144
+ /**
145
+ * Determines if two locations overlap with each other
146
+ */
147
+ function locationOverlaps(a, b) {
148
+ const startIncluded = a.start.line < b.end.line || a.start.line === b.end.line && a.start.column <= b.end.column;
149
+ const endIncluded = a.end.line > b.start.line || a.end.line === b.start.line && a.end.column >= b.start.column;
150
+ return startIncluded && endIncluded;
200
151
  }
201
- //#endregion
202
- //#region src/syntax/position-converter.ts
203
152
  function computeLineStarts(text) {
204
153
  const result = [];
205
154
  let pos = 0;
@@ -262,7 +211,9 @@ function binarySearch(array, value) {
262
211
  return ~low;
263
212
  }
264
213
  function compare(a, b) {
265
- return a < b ? -1 : a > b ? 1 : 0;
214
+ if (a < b) return -1;
215
+ if (a > b) return 1;
216
+ return 0;
266
217
  }
267
218
  const CharacterCodes = {
268
219
  lineFeed: 10,
@@ -275,36 +226,256 @@ function isLineBreak(ch) {
275
226
  return ch === CharacterCodes.lineFeed || ch === CharacterCodes.carriageReturn || ch === CharacterCodes.lineSeparator || ch === CharacterCodes.paragraphSeparator;
276
227
  }
277
228
  //#endregion
278
- //#region src/parsers/svelte-parser.schema.ts
279
- var SvelteVersionNotSupported = class extends S.TaggedError("@systemfsoftware/stryker-js-instrumenter/SvelteVersionNotSupported")("SvelteVersionNotSupported", {
280
- version: S.String,
281
- fileName: S.String,
282
- cause: S.Defect()
283
- }) {
284
- get message() {
285
- return `Svelte version ${this.version} is not supported for ${this.fileName} (expected >=3.30)`;
286
- }
287
- };
288
- var SvelteWalkerNotFound = class extends S.TaggedError("@systemfsoftware/stryker-js-instrumenter/SvelteWalkerNotFound")("SvelteWalkerNotFound", {
289
- fileName: S.String,
290
- cause: S.Defect()
291
- }) {
292
- get message() {
293
- return `Svelte walker not found for ${this.fileName}`;
229
+ //#region src/Parser.ts
230
+ /**
231
+ * Parser — all parsers that turn source text into the instrumenter's ASTs.
232
+ */
233
+ const { types: types$1, parseAsync } = babel;
234
+ const require = createRequire(import.meta.url);
235
+ function createParser(parserOptions) {
236
+ const jsParse = createJSParser(parserOptions);
237
+ async function parse(code, fileName, formatOverride) {
238
+ const format = getFormat(fileName, formatOverride);
239
+ if (!format) {
240
+ const ext = path.extname(fileName).toLowerCase();
241
+ throw new Error(`Unable to parse ${fileName}. No parser registered for ${ext}!`);
242
+ }
243
+ switch (format) {
244
+ case "js": return jsParse(code, fileName);
245
+ case "tsx": return parseTsx(code, fileName);
246
+ case "ts": return parseTS(code, fileName);
247
+ case "html": return parseHtml(code, fileName, { parse });
248
+ case "svelte": return parseSvelte(code, fileName, { parse });
249
+ }
250
+ throw new Error(`Unsupported format: ${String(format)}`);
294
251
  }
295
- };
296
- S.TaggedError("@systemfsoftware/stryker-js-instrumenter/SvelteParseFailed")("SvelteParseFailed", {
297
- fileName: S.String,
298
- cause: S.Defect()
299
- });
300
- //#endregion
301
- //#region src/parsers/svelte-parser.ts
302
- function parseVersion(version) {
303
- const match = /^(\d+)\.(\d+)(?:\.\d+)?/.exec(version);
304
- if (!match) return;
305
- const major = Number.parseInt(match[1] ?? "", 10);
306
- const minor = Number.parseInt(match[2] ?? "", 10);
307
- if (Number.isNaN(major) || Number.isNaN(minor)) return;
252
+ return parse;
253
+ }
254
+ function getFormat(fileName, override) {
255
+ if (override) return override;
256
+ else switch (path.extname(fileName).toLowerCase()) {
257
+ case ".js":
258
+ case ".jsx":
259
+ case ".mjs":
260
+ case ".cjs": return "js";
261
+ case ".mts":
262
+ case ".cts":
263
+ case ".ts": return "ts";
264
+ case ".tsx": return "tsx";
265
+ case ".vue":
266
+ case ".html":
267
+ case ".htm": return "html";
268
+ case ".svelte": return "svelte";
269
+ default: return;
270
+ }
271
+ }
272
+ function isParserPlugin(value) {
273
+ return typeof value === "string" || Array.isArray(value);
274
+ }
275
+ function isParserPluginArray(value) {
276
+ return value.every(isParserPlugin);
277
+ }
278
+ function getParserPlugins(override) {
279
+ if (override === null) return defaultPlugins;
280
+ if (isParserPluginArray(override)) return override;
281
+ throw new Error("Invalid parser plugins: expected ParserPlugin[]");
282
+ }
283
+ const defaultPlugins = [
284
+ "doExpressions",
285
+ "objectRestSpread",
286
+ "classProperties",
287
+ "exportDefaultFrom",
288
+ "exportNamespaceFrom",
289
+ "asyncGenerators",
290
+ "functionBind",
291
+ "functionSent",
292
+ "dynamicImport",
293
+ "numericSeparator",
294
+ "importMeta",
295
+ "optionalCatchBinding",
296
+ "optionalChaining",
297
+ "classPrivateProperties",
298
+ ["pipelineOperator", { proposal: "minimal" }],
299
+ "nullishCoalescingOperator",
300
+ "bigInt",
301
+ "throwExpressions",
302
+ "logicalAssignment",
303
+ "classPrivateMethods",
304
+ "v8intrinsic",
305
+ "partialApplication",
306
+ ["decorators", { decoratorsBeforeExport: false }],
307
+ "jsx"
308
+ ];
309
+ function createJSParser({ plugins: pluginsOverride }) {
310
+ return async function parse(text, fileName) {
311
+ const plugins = getParserPlugins(pluginsOverride);
312
+ const ast = await parseAsync(text, {
313
+ parserOpts: { plugins: [...plugins] },
314
+ filename: fileName,
315
+ sourceType: "module"
316
+ });
317
+ if (ast === null) throw new Error(`Expected ${fileName} to contain a babel.types.file, but it yielded null`);
318
+ return {
319
+ originFileName: fileName,
320
+ rawContent: text,
321
+ format: "js",
322
+ root: ast
323
+ };
324
+ };
325
+ }
326
+ /**
327
+ * See https://babeljs.io/docs/en/babel-preset-typescript
328
+ * @param text The text to parse
329
+ * @param fileName The name of the file
330
+ */
331
+ async function parseTS(text, fileName) {
332
+ return {
333
+ originFileName: fileName,
334
+ rawContent: text,
335
+ format: "ts",
336
+ root: await parseTSInternal(text, fileName, false)
337
+ };
338
+ }
339
+ async function parseTsx(text, fileName) {
340
+ return {
341
+ root: await parseTSInternal(text, fileName, true),
342
+ format: "tsx",
343
+ originFileName: fileName,
344
+ rawContent: text
345
+ };
346
+ }
347
+ async function parseTSInternal(text, fileName, isTSX) {
348
+ const ast = await parseAsync(text, {
349
+ filename: fileName,
350
+ parserOpts: { ranges: true },
351
+ configFile: false,
352
+ babelrc: false,
353
+ presets: [[require.resolve("@babel/preset-typescript"), {
354
+ isTSX,
355
+ allExtensions: true
356
+ }]],
357
+ plugins: [[require.resolve("@babel/plugin-proposal-decorators"), { legacy: true }], [require.resolve("@babel/plugin-transform-explicit-resource-management")]]
358
+ });
359
+ if (ast === null) throw new Error(`Expected ${fileName} to contain a babel.types.file, but it yielded null`);
360
+ if (types$1.isProgram(ast)) throw new Error(`Expected ${fileName} to contain a babel.types.file, but was a program`);
361
+ return ast;
362
+ }
363
+ const TSX_SCRIPT_TYPES = Object.freeze(["tsx", "text/tsx"]);
364
+ const TS_SCRIPT_TYPES = Object.freeze([
365
+ "ts",
366
+ "text/typescript",
367
+ "typescript"
368
+ ]);
369
+ const JS_SCRIPT_TYPES = Object.freeze([
370
+ "js",
371
+ "text/javascript",
372
+ "javascript",
373
+ "module"
374
+ ]);
375
+ async function parseHtml(text, originFileName, context) {
376
+ return {
377
+ originFileName,
378
+ rawContent: text,
379
+ format: "html",
380
+ root: await ngHtmlParser(text, originFileName, context)
381
+ };
382
+ }
383
+ async function ngHtmlParser(text, fileName, parserContext) {
384
+ const ngParser = await import("angular-html-parser");
385
+ const { rootNodes, errors } = ngParser.parse(text, {
386
+ canSelfClose: true,
387
+ allowHtmComponentClosingTags: true,
388
+ isTagNameCaseSensitive: true
389
+ });
390
+ if (errors.length !== 0) {
391
+ const firstError = errors[0];
392
+ if (firstError === void 0) throw new ParseFailed({
393
+ fileName,
394
+ message: "HTML parser reported errors but first error is missing",
395
+ location: {
396
+ line: 0,
397
+ column: 0
398
+ },
399
+ cause: errors
400
+ });
401
+ throw new ParseFailed({
402
+ fileName,
403
+ message: firstError.msg,
404
+ location: toSourceLocation(firstError.span.start),
405
+ cause: firstError
406
+ });
407
+ }
408
+ const scriptsAsPromised = [];
409
+ const scriptCollector = {
410
+ visitElement: (el, context) => {
411
+ const scriptFormat = getScriptType(el);
412
+ if (scriptFormat) scriptsAsPromised.push(parseScript(el, scriptFormat));
413
+ ngParser.visitAll(scriptCollector, el.children, context);
414
+ },
415
+ visitAttribute: () => void 0,
416
+ visitText: () => void 0,
417
+ visitComment: () => void 0,
418
+ visitDocType: () => void 0,
419
+ visitExpansion: () => void 0,
420
+ visitExpansionCase: () => void 0,
421
+ visitBlock: () => void 0,
422
+ visitBlockParameter: () => void 0,
423
+ visitLetDeclaration: () => void 0,
424
+ visitCdata: () => void 0,
425
+ visitComponent: () => void 0,
426
+ visitDirective: () => void 0
427
+ };
428
+ ngParser.visitAll(scriptCollector, rootNodes);
429
+ return { scripts: await Promise.all(scriptsAsPromised) };
430
+ async function parseScript(el, scriptFormat) {
431
+ const endSourceSpan = el.endSourceSpan;
432
+ if (endSourceSpan == null) throw new Error("HTML element without an end source span");
433
+ const content = text.substring(el.startSourceSpan.end.offset, endSourceSpan.start.offset);
434
+ const ast = await parserContext.parse(content, fileName, scriptFormat);
435
+ if (ast !== null && ast !== void 0) {
436
+ const offset = el.startSourceSpan.end;
437
+ const rootStart = ast.root.start;
438
+ if (rootStart == null) throw new Error("Babel File node without a start offset");
439
+ const rootEnd = ast.root.end;
440
+ if (rootEnd == null) throw new Error("Babel File node without an end offset");
441
+ ast.root.start = rootStart + offset.offset;
442
+ ast.root.end = rootEnd + offset.offset;
443
+ return {
444
+ ...ast,
445
+ offset: {
446
+ column: offset.offset,
447
+ line: offset.line
448
+ }
449
+ };
450
+ }
451
+ return ast;
452
+ }
453
+ }
454
+ function toSourceLocation({ line, col }) {
455
+ return {
456
+ line: line + 1,
457
+ column: col
458
+ };
459
+ }
460
+ function getScriptType(element) {
461
+ if (element.name === "script") {
462
+ if (!element.attrs.some((attr) => attr.name === "src")) {
463
+ const type = element.attrs.find((attr) => attr.name === "type") ?? element.attrs.find((attr) => attr.name === "lang");
464
+ if (type) {
465
+ const typeToLower = type.value.toLowerCase();
466
+ if (TSX_SCRIPT_TYPES.includes(typeToLower)) return "tsx";
467
+ if (TS_SCRIPT_TYPES.includes(typeToLower)) return "ts";
468
+ if (JS_SCRIPT_TYPES.includes(typeToLower)) return "js";
469
+ } else return "js";
470
+ }
471
+ }
472
+ }
473
+ function parseVersion(version) {
474
+ const match = /^(\d+)\.(\d+)(?:\.\d+)?/.exec(version);
475
+ if (!match) return;
476
+ const major = Number.parseInt(match[1] ?? "", 10);
477
+ const minor = Number.parseInt(match[2] ?? "", 10);
478
+ if (Number.isNaN(major) || Number.isNaN(minor)) return;
308
479
  return {
309
480
  major,
310
481
  minor
@@ -350,7 +521,7 @@ function tryGetScriptRangeFromElement(node) {
350
521
  isExpression: false
351
522
  };
352
523
  }
353
- async function parse$1(text, fileName, context) {
524
+ async function parseSvelte(text, fileName, context) {
354
525
  const { parse: svelteParse, preprocess, VERSION } = await import("svelte/compiler");
355
526
  let walk;
356
527
  if (!isSupportedSvelteVersion(VERSION)) throw new SvelteVersionNotSupported({
@@ -359,7 +530,7 @@ async function parse$1(text, fileName, context) {
359
530
  cause: `Expected >=3.30`
360
531
  });
361
532
  if (isSvelteV5OrLater(VERSION)) {
362
- const walkerModule = await import(import.meta.resolve("estree-walker", import.meta.resolve("svelte")));
533
+ const walkerModule = await import(import.meta.resolve("estree-walker"));
363
534
  if (!isPlainRecord(walkerModule) || !isWalkFunction(walkerModule["walk"])) throw new SvelteWalkerNotFound({
364
535
  fileName,
365
536
  cause: "estree-walker module without walk export"
@@ -378,14 +549,17 @@ async function parse$1(text, fileName, context) {
378
549
  const svelteAst = svelteParse(replacedCode, { filename: fileName });
379
550
  const { remappedModuleScriptRange, remappedScriptRanges } = remapScriptLocations(replacedCode, scriptMap, getModuleScriptRange(svelteAst), getTemplateScriptRanges(svelteAst, walk));
380
551
  const [moduleScript, ...additionalScripts] = await Promise.all([parseTemplateScriptIfDefined(remappedModuleScriptRange), ...remappedScriptRanges.map(parseTemplateScript)]);
552
+ let root;
553
+ if (moduleScript === void 0) root = { additionalScripts };
554
+ else root = {
555
+ moduleScript,
556
+ additionalScripts
557
+ };
381
558
  return {
382
559
  originFileName: fileName,
383
560
  rawContent: text,
384
561
  format: "svelte",
385
- root: {
386
- ...moduleScript === void 0 ? {} : { moduleScript },
387
- additionalScripts
388
- }
562
+ root
389
563
  };
390
564
  /**
391
565
  * Replaces script tags with placeholders.
@@ -476,11 +650,13 @@ function remapScriptLocations(code, scriptMap, moduleScriptRange, templateRanges
476
650
  const actualScript = scriptMap.get(script);
477
651
  const start = range.start + offset;
478
652
  if (actualScript) {
653
+ let langFormat = "js";
654
+ if (actualScript.attributes["lang"] === "ts") langFormat = "ts";
479
655
  const scriptRange = {
480
656
  start,
481
657
  end: start + actualScript.content.length,
482
658
  isExpression: range.isExpression,
483
- format: actualScript.attributes["lang"] === "ts" ? "ts" : "js"
659
+ format: langFormat
484
660
  };
485
661
  offset += actualScript.content.length - script.length;
486
662
  if (range === moduleScriptRange) newModuleScriptRange = scriptRange;
@@ -505,140 +681,44 @@ function collectTemplateExpression(node) {
505
681
  if (isRangedBaseNode(expression)) return expression;
506
682
  }
507
683
  //#endregion
508
- //#region src/parsers/ts-parser.ts
509
- const { types: types$19, parseAsync } = babel;
510
- const require = createRequire(import.meta.url);
684
+ //#region src/Printer.ts
511
685
  /**
512
- * See https://babeljs.io/docs/en/babel-preset-typescript
513
- * @param text The text to parse
514
- * @param fileName The name of the file
686
+ * Printer — turns the instrumenter's ASTs back into source text.
515
687
  */
516
- async function parseTS(text, fileName) {
517
- return {
518
- originFileName: fileName,
519
- rawContent: text,
520
- format: "ts",
521
- root: await parse(text, fileName, false)
522
- };
688
+ /**
689
+ * `@babel/generator` is CommonJS. Under Node's own ESM interop a default import
690
+ * of it is the module's `exports` object, so the code generator sits behind
691
+ * `.default` — the shape upstream reaches for, because upstream ships one
692
+ * emitted file per source file. This package ships a bundle, where the default
693
+ * import is already the function and `.default` is `undefined`, which fails at
694
+ * the first mutant with `generator is not a function` rather than at build
695
+ * time. Resolving both shapes once keeps the printers and the mutant's
696
+ * replacement code identical under either layout.
697
+ */
698
+ function resolveGenerate$1() {
699
+ if (typeof generator === "function") return generator;
700
+ return generator.default;
523
701
  }
524
- async function parseTsx(text, fileName) {
525
- return {
526
- root: await parse(text, fileName, true),
527
- format: "tsx",
528
- originFileName: fileName,
529
- rawContent: text
530
- };
531
- }
532
- async function parse(text, fileName, isTSX) {
533
- const ast = await parseAsync(text, {
534
- filename: fileName,
535
- parserOpts: { ranges: true },
536
- configFile: false,
537
- babelrc: false,
538
- presets: [[require.resolve("@babel/preset-typescript"), {
539
- isTSX,
540
- allExtensions: true
541
- }]],
542
- plugins: [[require.resolve("@babel/plugin-proposal-decorators"), { legacy: true }], [require.resolve("@babel/plugin-transform-explicit-resource-management")]]
543
- });
544
- if (ast === null) throw new Error(`Expected ${fileName} to contain a babel.types.file, but it yielded null`);
545
- if (types$19.isProgram(ast)) throw new Error(`Expected ${fileName} to contain a babel.types.file, but was a program`);
546
- return ast;
547
- }
548
- //#endregion
549
- //#region src/parsers/create-parser.ts
550
- function createParser(parserOptions) {
551
- const jsParse = createParser$1(parserOptions);
552
- async function parse(code, fileName, formatOverride) {
553
- const format = getFormat(fileName, formatOverride);
554
- if (!format) {
555
- const ext = path.extname(fileName).toLowerCase();
556
- throw new Error(`Unable to parse ${fileName}. No parser registered for ${ext}!`);
557
- }
558
- switch (format) {
559
- case "js": return jsParse(code, fileName);
560
- case "tsx": return parseTsx(code, fileName);
561
- case "ts": return parseTS(code, fileName);
562
- case "html": return parse$2(code, fileName, { parse });
563
- case "svelte": return parse$1(code, fileName, { parse });
564
- default: throw new Error(`Unsupported format: ${String(format)}`);
565
- }
566
- }
567
- return parse;
568
- }
569
- function getFormat(fileName, override) {
570
- if (override) return override;
571
- else switch (path.extname(fileName).toLowerCase()) {
572
- case ".js":
573
- case ".jsx":
574
- case ".mjs":
575
- case ".cjs": return "js";
576
- case ".mts":
577
- case ".cts":
578
- case ".ts": return "ts";
579
- case ".tsx": return "tsx";
580
- case ".vue":
581
- case ".html":
582
- case ".htm": return "html";
583
- case ".svelte": return "svelte";
584
- default: return;
585
- }
586
- }
587
- //#endregion
588
- //#region src/disable-type-checks.ts
589
- const commentDirectiveRegEx = /^(\s*)@(ts-[a-z-]+).*$/;
590
- const tsDirectiveLikeRegEx = /@(ts-[a-z-]+)/;
591
- const startingCommentRegex = /(^\s*\/\*.*?\*\/)/gs;
592
- /**
593
- * Disables TypeScript type checking for a single file by inserting `// @ts-nocheck` commands.
594
- * It also does this for *.js files, as they can be type checked by typescript as well.
595
- * Other file types are silently ignored
596
- *
597
- * @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#-ts-nocheck-in-typescript-files
598
- */
599
- async function disableTypeChecks(file, options) {
600
- const format = getFormat(file.name);
601
- if (!format) return file;
602
- if (isJSFileWithoutTSDirectives(file, format)) return {
603
- ...file,
604
- content: prefixWithNoCheck(file.content)
605
- };
606
- const ast = await createParser(options)(file.content, file.name);
607
- switch (ast.format) {
608
- case "js":
609
- case "ts":
610
- case "tsx": return {
611
- ...file,
612
- content: disableTypeCheckingInBabelAst(ast)
613
- };
614
- case "html": return {
615
- ...file,
616
- content: disableTypeCheckingInHtml(ast)
617
- };
618
- case "svelte": return {
619
- ...file,
620
- content: disableTypeCheckingInSvelte(ast)
621
- };
622
- }
623
- }
624
- function isJSFileWithoutTSDirectives(file, format) {
625
- return (format === "ts" || format === "js") && !tsDirectiveLikeRegEx.test(file.content);
626
- }
627
- function disableTypeCheckingInBabelAst(ast) {
628
- return prefixWithNoCheck(removeTSDirectives(ast.rawContent, ast.root.comments));
629
- }
630
- function prefixWithNoCheck(code) {
631
- if (code.startsWith("#")) {
632
- const newLineIndex = code.indexOf("\n");
633
- if (newLineIndex > 0) return `${code.substring(0, newLineIndex)}\n// @ts-nocheck\n${code.substring(newLineIndex + 1)}`;
634
- else return code;
635
- } else {
636
- startingCommentRegex.lastIndex = 0;
637
- const leadingComment = startingCommentRegex.exec(code)?.[1];
638
- if (leadingComment === void 0) return `// @ts-nocheck\n${code}`;
639
- return `${leadingComment.concat("\n")}// @ts-nocheck\n${code.substring(leadingComment.length)}`;
640
- }
702
+ const generate$2 = resolveGenerate$1();
703
+ function print(file) {
704
+ const context = { print };
705
+ switch (file.format) {
706
+ case "js": return jsPrint(file, context);
707
+ case "ts": return tsPrint(file, context);
708
+ case "tsx": return tsPrint(file, context);
709
+ case "html": return htmlPrint(file, context);
710
+ case "svelte": return sveltePrint(file, context);
711
+ }
641
712
  }
713
+ const jsPrint = (file) => {
714
+ return generate$2(file.root, { sourceMaps: false }).code;
715
+ };
716
+ const tsPrint = (file) => {
717
+ return generate$2(file.root, {
718
+ decoratorsBeforeExport: true,
719
+ sourceMaps: false
720
+ }).code;
721
+ };
642
722
  function getScriptStart$1(script) {
643
723
  const start = script.root.start;
644
724
  if (start === void 0 || start === null) throw new Error("Script AST root without start");
@@ -649,158 +729,62 @@ function getScriptEnd$1(script) {
649
729
  if (end === void 0 || end === null) throw new Error("Script AST root without end");
650
730
  return end;
651
731
  }
652
- function disableTypeCheckingInHtml(ast) {
732
+ const htmlPrint = (ast, context) => {
653
733
  const sortedScripts = [...ast.root.scripts].sort((a, b) => getScriptStart$1(a) - getScriptStart$1(b));
654
734
  let currentIndex = 0;
655
735
  let html = "";
656
736
  for (const script of sortedScripts) {
657
737
  html += ast.rawContent.substring(currentIndex, getScriptStart$1(script));
658
738
  html += "\n";
659
- html += prefixWithNoCheck(removeTSDirectives(script.rawContent, script.root.comments));
739
+ html += context.print(script, context);
660
740
  html += "\n";
661
741
  currentIndex = getScriptEnd$1(script);
662
742
  }
663
- html += ast.rawContent.substring(currentIndex);
743
+ html += ast.rawContent.substr(currentIndex);
664
744
  return html;
665
- }
666
- function disableTypeCheckingInSvelte(ast) {
667
- const sortedScripts = [ast.root.moduleScript, ...ast.root.additionalScripts].filter(Predicate.isNotNullish).sort((a, b) => a.range.start - b.range.start);
745
+ };
746
+ const sveltePrint = ({ root, rawContent }, context) => {
668
747
  let currentIndex = 0;
669
- let html = "";
670
- for (const script of sortedScripts) {
671
- html += ast.rawContent.substring(currentIndex, script.range.start);
672
- html += "\n";
673
- html += prefixWithNoCheck(removeTSDirectives(script.ast.rawContent, script.ast.root.comments));
674
- html += "\n";
748
+ let outputText = "";
749
+ const sortedScripts = [root.moduleScript, ...root.additionalScripts].filter(Predicate.isNotNullish).sort((a, b) => a.range.start - b.range.start);
750
+ for (const script of sortedScripts) if (script.isExpression) {
751
+ const codeWithoutSemicolon = context.print(script.ast, context).slice(0, -1);
752
+ outputText += rawContent.substring(currentIndex, script.range.start) + codeWithoutSemicolon;
753
+ currentIndex = script.range.end;
754
+ } else {
755
+ outputText += rawContent.substring(currentIndex, script.range.start);
756
+ outputText += "\n";
757
+ outputText += context.print(script.ast, context);
758
+ outputText += "\n";
675
759
  currentIndex = script.range.end;
676
760
  }
677
- html += ast.rawContent.substring(currentIndex);
678
- return html;
679
- }
680
- function removeTSDirectives(text, comments) {
681
- const directiveRanges = comments?.map(tryParseTSDirective).filter(Predicate.isNotNullish).sort((a, b) => a.startPos - b.startPos);
682
- if (directiveRanges) {
683
- let currentIndex = 0;
684
- let pruned = "";
685
- for (const directiveRange of directiveRanges) {
686
- pruned += text.substring(currentIndex, directiveRange.startPos);
687
- currentIndex = directiveRange.endPos;
688
- }
689
- pruned += text.substring(currentIndex);
690
- return pruned;
691
- } else return text;
692
- }
693
- function tryParseTSDirective(comment) {
694
- const match = commentDirectiveRegEx.exec(comment.value);
695
- if (match) {
696
- const start = comment.start;
697
- if (start === void 0 || start === null) throw new Error("Comment without start");
698
- const directivePrefix = match[1];
699
- if (directivePrefix === void 0) throw new Error("TS directive match without prefix");
700
- const directiveName = match[2];
701
- if (directiveName === void 0) throw new Error("TS directive match without directive name");
702
- const directiveStartPos = start + directivePrefix.length + 2;
703
- return {
704
- startPos: directiveStartPos,
705
- endPos: directiveStartPos + directiveName.length + 1
706
- };
707
- }
708
- }
709
- //#endregion
710
- //#region src/frameworks/angular-ignorer.ts
711
- const ANGULAR_SIGNAL_IO_FUNCTIONS = Object.freeze([
712
- "input",
713
- "model",
714
- "output"
715
- ]);
716
- const ANGULAR_SIGNAL_QUERY_FUNCTIONS = Object.freeze([
717
- "contentChild",
718
- "contentChildren",
719
- "viewChild",
720
- "viewChildren"
721
- ]);
722
- const INPUT_MODEL_OUTPUT_CONFIG_MSG = "Angular signal based input, model and output functions configuration object cannot be mutated as that causes issues with the Angular compiler.";
723
- const SIGNAL_QUERY_OPTIONS_MSG = "Angular signal query options object cannot be mutated as that causes issues with the Angular compiler.";
724
- function shouldIgnore(path) {
725
- if (isInputModelOrOutputConfigurationObject(path)) return Option.some(INPUT_MODEL_OUTPUT_CONFIG_MSG);
726
- if (isSignalQueryOptionsObject(path)) return Option.some(SIGNAL_QUERY_OPTIONS_MSG);
727
- return Option.none();
728
- }
729
- const angularIgnorer = { shouldIgnore };
730
- function isClassFieldLike(path) {
731
- return path.isClassProperty() || path.isClassPrivateProperty() || path.isClassAccessorProperty();
732
- }
733
- function isInputModelOrOutputConfigurationObject(path) {
734
- const parent = path.parentPath;
735
- const grandParent = parent?.parentPath;
736
- if (!path.isObjectExpression() || parent === null || parent === void 0 || !parent.isCallExpression() || grandParent === null || grandParent === void 0 || !grandParent.isClassProperty()) return false;
737
- const callExpression = parent;
738
- const objectExpression = path;
739
- const callNode = callExpression.node;
740
- if (!Predicate.hasProperty(callNode, "callee") || !Predicate.hasProperty(callNode, "arguments")) return false;
741
- const callee = callNode["callee"];
742
- const args = callNode["arguments"];
743
- if (!Array.isArray(args)) return false;
744
- const isRequiredSignalIOFunction = isMemberExpressionWithIdentifier(callee, ANGULAR_SIGNAL_IO_FUNCTIONS, "required");
745
- const isSignalIOFunction = isIdentifierIn(callee, ANGULAR_SIGNAL_IO_FUNCTIONS);
746
- const isOutput = isIdentifierWithName(callee, "output");
747
- if (isRequiredSignalIOFunction || isOutput) return args.length >= 1 && args[0] === objectExpression.node;
748
- if (isSignalIOFunction) return args.length >= 2 && args[1] === objectExpression.node;
749
- return false;
750
- }
751
- function isSignalQueryOptionsObject(path) {
752
- const parent = path.parentPath;
753
- const grandParent = parent?.parentPath;
754
- if (!path.isObjectExpression() || parent === null || parent === void 0 || !parent.isCallExpression() || grandParent === null || grandParent === void 0 || !isClassFieldLike(grandParent)) return false;
755
- const callExpression = parent;
756
- const objectExpression = path;
757
- const callNode = callExpression.node;
758
- if (!Predicate.hasProperty(callNode, "callee") || !Predicate.hasProperty(callNode, "arguments")) return false;
759
- const callee = callNode["callee"];
760
- const args = callNode["arguments"];
761
- if (!Array.isArray(args)) return false;
762
- const isQueryFn = isIdentifierIn(callee, ANGULAR_SIGNAL_QUERY_FUNCTIONS);
763
- const isRequiredQueryFn = isMemberExpressionWithIdentifier(callee, ANGULAR_SIGNAL_QUERY_FUNCTIONS, "required");
764
- if (!isQueryFn && !isRequiredQueryFn) return false;
765
- return args.length >= 2 && args[1] === objectExpression.node;
766
- }
767
- function isIdentifierWithName(node, name) {
768
- return Predicate.hasProperty(node, "type") && node["type"] === "Identifier" && Predicate.hasProperty(node, "name") && node["name"] === name;
769
- }
770
- function isIdentifierIn(node, names) {
771
- return Predicate.hasProperty(node, "type") && node["type"] === "Identifier" && Predicate.hasProperty(node, "name") && typeof node["name"] === "string" && names.includes(node["name"]);
772
- }
773
- function isMemberExpressionWithIdentifier(node, objectNames, propertyName) {
774
- return Predicate.hasProperty(node, "type") && node["type"] === "MemberExpression" && Predicate.hasProperty(node, "object") && Predicate.hasProperty(node, "property") && isIdentifierIn(node["object"], objectNames) && isIdentifierWithName(node["property"], propertyName);
775
- }
776
- //#endregion
777
- //#region src/frameworks/index.ts
778
- const strykerPlugins = [];
779
- const frameworkPluginsFileUrl = import.meta.url;
780
- //#endregion
781
- //#region src/babel/babel-generator.ts
782
- /**
783
- * `@babel/generator` is CommonJS. Under Node's own ESM interop a default import
784
- * of it is the module's `exports` object, so the code generator sits behind
785
- * `.default` — the shape upstream reaches for, because upstream ships one
786
- * emitted file per source file. This package ships a bundle, where the default
787
- * import is already the function and `.default` is `undefined`, which fails at
788
- * the first mutant with `generator is not a function` rather than at build
789
- * time. Resolving both shapes once keeps the printers and the mutant's
790
- * replacement code identical under either layout.
791
- */
792
- const generate = typeof generator === "function" ? generator : generator.default;
761
+ outputText += rawContent.substring(currentIndex);
762
+ return outputText;
763
+ };
793
764
  //#endregion
794
- //#region src/babel/clone.ts
795
- const { types: types$18 } = babel;
765
+ //#region src/Mutator.ts
796
766
  /**
797
- * Helper for `types.cloneNode(node, deep: true, withoutLocations: false);`
767
+ * Mutator — every mutation operator and its registry.
798
768
  */
769
+ const { traverse: babelTraverse, types: babelTypes$2 } = babel;
770
+ const t$1 = babelTypes$2;
771
+ function isGeneratorFn(value) {
772
+ return typeof value === "function";
773
+ }
774
+ function hasDefault(value) {
775
+ if (typeof value !== "object" || value === null) return false;
776
+ if (!("default" in value)) return false;
777
+ return typeof Reflect.get(value, "default") === "function";
778
+ }
779
+ let generateExport;
780
+ const candidate = generator;
781
+ if (isGeneratorFn(candidate)) generateExport = candidate;
782
+ else if (hasDefault(candidate)) generateExport = candidate.default;
783
+ else throw new Error("Missing generator");
784
+ const generate$1 = generateExport;
799
785
  function deepCloneNode(node) {
800
- return types$18.cloneNode(node, true, false);
786
+ return babelTypes$2.cloneNode(node, true, false);
801
787
  }
802
- //#endregion
803
- //#region src/babel/equality.ts
804
788
  function eqPosition(a, b) {
805
789
  return a.line === b.line && a.column === b.column;
806
790
  }
@@ -810,9 +794,6 @@ function eqLocation(a, b) {
810
794
  function eqNode(a, b) {
811
795
  return a.type === b.type && !!a.loc && !!b.loc && eqLocation(a.loc, b.loc);
812
796
  }
813
- //#endregion
814
- //#region src/mutant.ts
815
- const { traverse: traverse$1 } = babel;
816
797
  function createMutant(id, fileName, original, specs, offset = {
817
798
  column: 0,
818
799
  line: 0
@@ -825,40 +806,19 @@ function createMutant(id, fileName, original, specs, offset = {
825
806
  replacement: specs.replacement,
826
807
  mutatorName: specs.mutatorName,
827
808
  ignoreReason: specs.ignoreReason,
828
- replacementCode: generate(specs.replacement).code
829
- };
830
- }
831
- function toApiMutant(mutant) {
832
- const loc = mutant.original.loc;
833
- if (loc === void 0 || loc === null) throw new Error("Babel node without a source location");
834
- return {
835
- fileName: mutant.fileName,
836
- id: mutant.id,
837
- location: toApiLocation(loc, mutant.offset),
838
- mutatorName: mutant.mutatorName,
839
- replacement: mutant.replacementCode,
840
- ...mutant.ignoreReason === void 0 ? {} : { statusReason: mutant.ignoreReason },
841
- ...mutant.ignoreReason === void 0 ? {} : { status: "Ignored" }
809
+ replacementCode: generate$1(specs.replacement, { sourceMaps: false }).code
842
810
  };
843
811
  }
844
- /**
845
- * Applies the mutant in (a copy of) the AST, without changing provided AST.
846
- * Can the tree itself (in which case the replacement is returned),
847
- * or can be nested in the given tree.
848
- *
849
- * Returns a plain node rather than the argument's own type: whether the
850
- * replacement fits a given position is the placer's claim, and the placer
851
- * checks it with a Babel predicate. A generic return would have to assert it
852
- * here, where nothing can check it.
853
- * @param mutant the mutant to apply
854
- * @param originalTree The original node, which will be treated as readonly
855
- */
856
812
  function applyMutant(mutant, originalTree) {
857
813
  if (originalTree === mutant.original) return mutant.replacement;
858
814
  const mutatedAst = deepCloneNode(originalTree);
859
- let applied = false;
860
815
  const { original, replacement } = mutant;
861
- traverse$1(mutatedAst, {
816
+ if (hasReplaced(mutatedAst, original, replacement) === false) throw new Error(`Could not apply mutant ${JSON.stringify(replacement)}.`);
817
+ return mutatedAst;
818
+ }
819
+ function hasReplaced(root, original, replacement) {
820
+ let applied = false;
821
+ babelTraverse(root, {
862
822
  noScope: true,
863
823
  enter(path) {
864
824
  if (eqNode(path.node, original)) {
@@ -868,54 +828,140 @@ function applyMutant(mutant, originalTree) {
868
828
  }
869
829
  }
870
830
  });
871
- if (!applied) throw new Error(`Could not apply mutant ${JSON.stringify(replacement)}.`);
872
- return mutatedAst;
873
- }
874
- function toApiLocation(source, offset) {
875
- return {
876
- start: toPosition(source.start, offset),
877
- end: toPosition(source.end, offset)
878
- };
879
- }
880
- function toPosition(source, offset) {
881
- return {
882
- column: source.column + (source.line === 1 ? offset.column : 0),
883
- line: source.line + offset.line - 1
884
- };
831
+ return applied;
885
832
  }
886
- //#endregion
887
- //#region src/transformers/mutant-collector.ts
888
- function createMutantCollector() {
889
- return [];
890
- }
891
- function collect(collector, fileName, original, mutable, offset = {
892
- line: 0,
893
- column: 0
894
- }) {
895
- const mutant = createMutant(collector.length.toString(), fileName, original, mutable, offset);
896
- collector.push(mutant);
897
- return mutant;
898
- }
899
- function hasPlacedMutants(collector, fileName) {
900
- return collector.some((mutant) => mutant.fileName === fileName && !mutant.ignoreReason);
901
- }
902
- //#endregion
903
- //#region src/babel/babel-file.ts
904
833
  /**
905
- * Wraps a parsed AST the way Babel's own pipeline does, so
906
- * `NodePath#buildCodeFrameError` can render a code frame
907
- * (https://github.com/babel/babel/issues/11889). Without the wrapper a
908
- * placement failure reports no source context.
834
+ * The mutations of a regular expression pattern.
835
+ *
836
+ * Pure: a pattern and its flags in, replacement patterns out. No I/O, no clock,
837
+ * no throwing — a pattern this cannot parse yields no mutants, which is the
838
+ * honest answer for a literal whose syntax the engine does not model.
839
+ *
840
+ * The transformation set is fixed and small, and each member changes exactly
841
+ * one thing about the pattern:
842
+ *
843
+ * | family | example |
844
+ * | ----------------------- | ------------------------ |
845
+ * | anchor removal | `^abc$` -> `abc$`, `^abc` |
846
+ * | character class negation| `[abc]` <-> `[^abc]` |
847
+ * | predefined class negation| `\d` <-> `\D`, `\p{L}` <-> `\P{L}` |
848
+ * | quantifier removal | `a+`, `a*`, `a{2,3}` -> `a` |
849
+ * | lookaround negation | `(?=a)` <-> `(?!a)`, `(?<=a)` <-> `(?<!a)` |
850
+ *
851
+ * Alternation and grouping are deliberately untouched: swapping a branch or
852
+ * dropping a group produces mutants that survive for reasons unrelated to the
853
+ * test suite's strength, which inflates a score rather than measuring one.
854
+ *
855
+ * The order is part of the contract, because a mutant's identity in a report is
856
+ * its position: anchors first, then each remaining position left to right with
857
+ * quantifier removal ahead of class negation.
909
858
  */
910
- function createBabelFile(filename, code, ast) {
911
- return new File({ filename }, {
912
- code,
913
- ast
914
- });
859
+ function mutateRegexPattern(pattern, flags) {
860
+ if (pattern.length === 0) return [];
861
+ try {
862
+ const f = flags ?? "";
863
+ const ast = new RegExpParser().parsePattern(pattern, void 0, void 0, {
864
+ unicode: f.includes("u"),
865
+ unicodeSets: f.includes("v")
866
+ });
867
+ const bol = [];
868
+ const eol = [];
869
+ const rest = [];
870
+ visitRegExpAST(ast, {
871
+ onAssertionEnter(node) {
872
+ if (node.kind === "start") {
873
+ const splice = {
874
+ start: node.start,
875
+ end: node.end,
876
+ text: ""
877
+ };
878
+ if ((pattern.slice(0, splice.start) + splice.text + pattern.slice(splice.end)).length === 0) return;
879
+ bol.push(splice);
880
+ } else if (node.kind === "end") {
881
+ const splice = {
882
+ start: node.start,
883
+ end: node.end,
884
+ text: ""
885
+ };
886
+ if ((pattern.slice(0, splice.start) + splice.text + pattern.slice(splice.end)).length === 0) return;
887
+ eol.push(splice);
888
+ } else if (node.kind === "lookahead" || node.kind === "lookbehind") {
889
+ let offset;
890
+ if (node.kind === "lookahead") offset = 2;
891
+ else offset = 3;
892
+ const pos = node.start + offset;
893
+ let text;
894
+ if (node.negate) text = "=";
895
+ else text = "!";
896
+ rest.push({
897
+ start: pos,
898
+ end: pos + 1,
899
+ text,
900
+ priority: 1
901
+ });
902
+ }
903
+ },
904
+ onCharacterClassEnter(node) {
905
+ const pos = node.start + 1;
906
+ if (node.negate) rest.push({
907
+ start: pos,
908
+ end: pos + 1,
909
+ text: "",
910
+ priority: 1
911
+ });
912
+ else rest.push({
913
+ start: pos,
914
+ end: pos,
915
+ text: "^",
916
+ priority: 1
917
+ });
918
+ },
919
+ onCharacterSetEnter(node) {
920
+ if (node.kind === "digit" || node.kind === "space" || node.kind === "word") {
921
+ const pos = node.start + 1;
922
+ const cur = node.raw[1] ?? "";
923
+ let toggled;
924
+ if (cur === cur.toUpperCase()) toggled = cur.toLowerCase();
925
+ else toggled = cur.toUpperCase();
926
+ rest.push({
927
+ start: pos,
928
+ end: pos + 1,
929
+ text: toggled,
930
+ priority: 2
931
+ });
932
+ } else if (node.kind === "property") {
933
+ const pos = node.start + 1;
934
+ const cur = node.raw[1] ?? "";
935
+ let toggled;
936
+ if (cur === "p") toggled = "P";
937
+ else toggled = "p";
938
+ rest.push({
939
+ start: pos,
940
+ end: pos + 1,
941
+ text: toggled,
942
+ priority: 2
943
+ });
944
+ }
945
+ },
946
+ onQuantifierEnter(node) {
947
+ rest.push({
948
+ start: node.start,
949
+ end: node.end,
950
+ text: node.element.raw,
951
+ priority: 0
952
+ });
953
+ }
954
+ });
955
+ rest.sort((a, b) => a.start - b.start || a.priority - b.priority);
956
+ return [
957
+ ...bol,
958
+ ...eol,
959
+ ...rest
960
+ ].map((s) => pattern.slice(0, s.start) + s.text + pattern.slice(s.end));
961
+ } catch {
962
+ return [];
963
+ }
915
964
  }
916
- //#endregion
917
- //#region src/mutators/arithmetic-operator-mutator.ts
918
- const { types: t$3 } = babel;
919
965
  const arithmeticOperatorReplacements = Object.freeze({
920
966
  "+": "-",
921
967
  "-": "+",
@@ -924,39 +970,42 @@ const arithmeticOperatorReplacements = Object.freeze({
924
970
  "%": "*"
925
971
  });
926
972
  const arithmeticOperatorMutator = function* (node, _context) {
927
- if (t$3.isBinaryExpression(node) && isSupported$3(node.operator, node)) {
973
+ if (t$1.isBinaryExpression(node) && isSupportedArithmeticOperator(node.operator, node)) {
928
974
  const mutatedOperator = arithmeticOperatorReplacements[node.operator];
929
975
  const replacement = deepCloneNode(node);
930
976
  replacement.operator = mutatedOperator;
931
977
  yield replacement;
932
978
  }
933
979
  };
934
- function isSupported$3(operator, node) {
980
+ function isSupportedArithmeticOperator(operator, node) {
935
981
  if (!Object.keys(arithmeticOperatorReplacements).includes(operator)) return false;
936
982
  const stringTypes = ["StringLiteral", "TemplateLiteral"];
937
- const leftType = t$3.isBinaryExpression(node.left) ? node.left.right.type : node.left.type;
983
+ let leftType;
984
+ if (t$1.isBinaryExpression(node.left)) leftType = node.left.right.type;
985
+ else leftType = node.left.type;
938
986
  if (stringTypes.includes(node.right.type) || stringTypes.includes(leftType)) return false;
939
987
  return true;
940
988
  }
941
- //#endregion
942
- //#region src/mutators/array-declaration-mutator.ts
943
- const { types: types$17 } = babel;
944
989
  const arrayDeclarationMutator = function* (node, _context) {
945
- if (types$17.isArrayExpression(node)) yield node.elements.length ? types$17.arrayExpression() : types$17.arrayExpression([types$17.stringLiteral("Stryker was here")]);
946
- if ((types$17.isCallExpression(node) || types$17.isNewExpression(node)) && types$17.isIdentifier(node.callee) && node.callee.name === "Array") {
947
- const mutatedCallArgs = node.arguments.length ? [] : [types$17.arrayExpression()];
948
- yield types$17.isNewExpression(node) ? types$17.newExpression(deepCloneNode(node.callee), mutatedCallArgs) : types$17.callExpression(deepCloneNode(node.callee), mutatedCallArgs);
990
+ if (babelTypes$2.isArrayExpression(node)) {
991
+ let replacement;
992
+ if (node.elements.length > 0) replacement = babelTypes$2.arrayExpression();
993
+ else replacement = babelTypes$2.arrayExpression([babelTypes$2.stringLiteral("Stryker was here")]);
994
+ yield replacement;
995
+ }
996
+ if ((babelTypes$2.isCallExpression(node) || babelTypes$2.isNewExpression(node)) && babelTypes$2.isIdentifier(node.callee) && node.callee.name === "Array") {
997
+ let mutatedCallArgs;
998
+ if (node.arguments.length > 0) mutatedCallArgs = [];
999
+ else mutatedCallArgs = [babelTypes$2.arrayExpression()];
1000
+ let replacement;
1001
+ if (babelTypes$2.isNewExpression(node)) replacement = babelTypes$2.newExpression(deepCloneNode(node.callee), mutatedCallArgs);
1002
+ else replacement = babelTypes$2.callExpression(deepCloneNode(node.callee), mutatedCallArgs);
1003
+ yield replacement;
949
1004
  }
950
1005
  };
951
- //#endregion
952
- //#region src/mutators/arrow-function-mutator.ts
953
- const { types: types$16 } = babel;
954
1006
  const arrowFunctionMutator = function* (node, _context) {
955
- if (types$16.isArrowFunctionExpression(node) && !types$16.isBlockStatement(node.body) && !(types$16.isIdentifier(node.body) && node.body.name === "undefined")) yield types$16.arrowFunctionExpression([], types$16.identifier("undefined"));
1007
+ if (babelTypes$2.isArrowFunctionExpression(node) && !babelTypes$2.isBlockStatement(node.body) && !(babelTypes$2.isIdentifier(node.body) && node.body.name === "undefined")) yield babelTypes$2.arrowFunctionExpression([], babelTypes$2.identifier("undefined"));
956
1008
  };
957
- //#endregion
958
- //#region src/mutators/assignment-operator-mutator.ts
959
- const { types: types$15 } = babel;
960
1009
  const assignmentOperatorReplacements = Object.freeze({
961
1010
  "+=": "-=",
962
1011
  "-=": "+=",
@@ -978,7 +1027,7 @@ const stringAssignmentTypes = Object.freeze([
978
1027
  "??="
979
1028
  ]);
980
1029
  const assignmentOperatorMutator = function* (node, _context) {
981
- if (types$15.isAssignmentExpression(node) && isSupportedAssignmentOperator(node.operator) && isSupported$2(node)) {
1030
+ if (babelTypes$2.isAssignmentExpression(node) && isSupportedAssignmentOperator(node.operator) && isSupportedAssignmentExpression(node)) {
982
1031
  const mutatedOperator = assignmentOperatorReplacements[node.operator];
983
1032
  const replacement = deepCloneNode(node);
984
1033
  replacement.operator = mutatedOperator;
@@ -988,15 +1037,12 @@ const assignmentOperatorMutator = function* (node, _context) {
988
1037
  function isSupportedAssignmentOperator(operator) {
989
1038
  return Object.keys(assignmentOperatorReplacements).includes(operator);
990
1039
  }
991
- function isSupported$2(node) {
1040
+ function isSupportedAssignmentExpression(node) {
992
1041
  if (stringTypes.includes(node.right.type) && !stringAssignmentTypes.includes(node.operator)) return false;
993
1042
  return true;
994
1043
  }
995
- //#endregion
996
- //#region src/mutators/block-statement-mutator.ts
997
- const { types: types$14 } = babel;
998
1044
  const blockStatementMutator = function* (node, context) {
999
- if (types$14.isBlockStatement(node) && isValid(node, context)) yield types$14.blockStatement([]);
1045
+ if (babelTypes$2.isBlockStatement(node) && isValid(node, context)) yield babelTypes$2.blockStatement([]);
1000
1046
  };
1001
1047
  function isValid(node, context) {
1002
1048
  return !isEmpty(node) && !isInvalidConstructorBody(node, context);
@@ -1006,16 +1052,16 @@ function isEmpty(node) {
1006
1052
  }
1007
1053
  function isInvalidConstructorBody(node, context) {
1008
1054
  const parent = context.parent;
1009
- if (parent === void 0 || !types$14.isClassMethod(parent) || parent.kind !== "constructor") return false;
1010
- const hasParamProps = parent.params.some((param) => types$14.isTSParameterProperty(param));
1055
+ if (parent === void 0 || !babelTypes$2.isClassMethod(parent) || parent.kind !== "constructor") return false;
1056
+ const hasParamProps = parent.params.some((param) => babelTypes$2.isTSParameterProperty(param));
1011
1057
  const hasInitProps = containsInitializedClassProperties(parent, context);
1012
1058
  const hasSuper = hasSuperExpression(node);
1013
1059
  return (hasParamProps || hasInitProps) && hasSuper;
1014
1060
  }
1015
1061
  function containsInitializedClassProperties(constructor, context) {
1016
1062
  const grandParent = context.grandParent;
1017
- if (grandParent === void 0 || !types$14.isClassBody(grandParent)) return false;
1018
- return grandParent.body.some((classMember) => types$14.isClassProperty(classMember) && classMember.value !== null && classMember.value !== void 0);
1063
+ if (grandParent === void 0 || !babelTypes$2.isClassBody(grandParent)) return false;
1064
+ return grandParent.body.some((classMember) => babelTypes$2.isClassProperty(classMember) && classMember.value !== null && classMember.value !== void 0);
1019
1065
  }
1020
1066
  function hasSuperExpression(block) {
1021
1067
  return containsSuperCall(block);
@@ -1041,15 +1087,10 @@ function hasSuperInChildren(node) {
1041
1087
  }
1042
1088
  return false;
1043
1089
  }
1044
- //#endregion
1045
- //#region src/mutators/boolean-literal-mutator.ts
1046
- const { types: types$13 } = babel;
1047
1090
  const booleanLiteralMutator = function* (node, _context) {
1048
- if (types$13.isBooleanLiteral(node)) yield types$13.booleanLiteral(!node.value);
1049
- if (types$13.isUnaryExpression(node) && node.operator === "!" && node.prefix) yield deepCloneNode(node.argument);
1091
+ if (babelTypes$2.isBooleanLiteral(node)) yield babelTypes$2.booleanLiteral(!node.value);
1092
+ if (babelTypes$2.isUnaryExpression(node) && node.operator === "!" && node.prefix) yield deepCloneNode(node.argument);
1050
1093
  };
1051
- //#endregion
1052
- //#region src/mutators/conditional-expression-mutator.ts
1053
1094
  const booleanOperators = Object.freeze([
1054
1095
  "!=",
1055
1096
  "!==",
@@ -1062,31 +1103,30 @@ const booleanOperators = Object.freeze([
1062
1103
  ">=",
1063
1104
  "||"
1064
1105
  ]);
1065
- const { types: types$12 } = babel;
1066
1106
  const conditionalExpressionMutator = function* (node, context) {
1067
- if (isTestOfLoop(node, context)) yield types$12.booleanLiteral(false);
1107
+ if (isTestOfLoop(node, context)) yield babelTypes$2.booleanLiteral(false);
1068
1108
  else if (isTestOfCondition(node, context)) {
1069
- yield types$12.booleanLiteral(true);
1070
- yield types$12.booleanLiteral(false);
1109
+ yield babelTypes$2.booleanLiteral(true);
1110
+ yield babelTypes$2.booleanLiteral(false);
1071
1111
  } else if (isBooleanExpression(node)) {
1072
1112
  const parent = context.parent;
1073
- if (parent !== void 0 && types$12.isLogicalExpression(parent)) {
1113
+ if (parent !== void 0 && babelTypes$2.isLogicalExpression(parent)) {
1074
1114
  if (parent.operator === "||") {
1075
- yield types$12.booleanLiteral(false);
1115
+ yield babelTypes$2.booleanLiteral(false);
1076
1116
  return;
1077
1117
  }
1078
1118
  if (parent.operator === "&&") {
1079
- yield types$12.booleanLiteral(true);
1119
+ yield babelTypes$2.booleanLiteral(true);
1080
1120
  return;
1081
1121
  }
1082
1122
  }
1083
- yield types$12.booleanLiteral(true);
1084
- yield types$12.booleanLiteral(false);
1085
- } else if (types$12.isForStatement(node) && node.test === null) {
1123
+ yield babelTypes$2.booleanLiteral(true);
1124
+ yield babelTypes$2.booleanLiteral(false);
1125
+ } else if (babelTypes$2.isForStatement(node) && node.test === null) {
1086
1126
  const replacement = deepCloneNode(node);
1087
- replacement.test = types$12.booleanLiteral(false);
1127
+ replacement.test = babelTypes$2.booleanLiteral(false);
1088
1128
  yield replacement;
1089
- } else if (types$12.isSwitchCase(node) && node.consequent.length > 0) {
1129
+ } else if (babelTypes$2.isSwitchCase(node) && node.consequent.length > 0) {
1090
1130
  const replacement = deepCloneNode(node);
1091
1131
  replacement.consequent = [];
1092
1132
  yield replacement;
@@ -1095,24 +1135,21 @@ const conditionalExpressionMutator = function* (node, context) {
1095
1135
  function isTestOfLoop(node, context) {
1096
1136
  const parent = context.parent;
1097
1137
  if (parent === void 0) return false;
1098
- if (types$12.isForStatement(parent) && parent.test === node) return true;
1099
- if (types$12.isWhileStatement(parent) && parent.test === node) return true;
1100
- if (types$12.isDoWhileStatement(parent) && parent.test === node) return true;
1138
+ if (babelTypes$2.isForStatement(parent) && parent.test === node) return true;
1139
+ if (babelTypes$2.isWhileStatement(parent) && parent.test === node) return true;
1140
+ if (babelTypes$2.isDoWhileStatement(parent) && parent.test === node) return true;
1101
1141
  return false;
1102
1142
  }
1103
1143
  function isTestOfCondition(node, context) {
1104
1144
  const parent = context.parent;
1105
1145
  if (parent === void 0) return false;
1106
- return types$12.isIfStatement(parent) && parent.test === node;
1146
+ return babelTypes$2.isIfStatement(parent) && parent.test === node;
1107
1147
  }
1108
1148
  function isBooleanExpression(node) {
1109
- if (types$12.isBinaryExpression(node)) return booleanOperators.includes(node.operator);
1110
- if (types$12.isLogicalExpression(node)) return booleanOperators.includes(node.operator);
1149
+ if (babelTypes$2.isBinaryExpression(node)) return booleanOperators.includes(node.operator);
1150
+ if (babelTypes$2.isLogicalExpression(node)) return booleanOperators.includes(node.operator);
1111
1151
  return false;
1112
1152
  }
1113
- //#endregion
1114
- //#region src/mutators/equality-operator-mutator.ts
1115
- const { types: t$2 } = babel;
1116
1153
  const operators = {
1117
1154
  "<": ["<=", ">="],
1118
1155
  "<=": ["<", ">"],
@@ -1127,34 +1164,28 @@ function isEqualityOperator(operator) {
1127
1164
  return Object.keys(operators).includes(operator);
1128
1165
  }
1129
1166
  const equalityOperatorMutator = function* (node, _context) {
1130
- if (t$2.isBinaryExpression(node) && isEqualityOperator(node.operator)) for (const mutableOperator of operators[node.operator]) {
1131
- const replacement = t$2.cloneNode(node, true);
1167
+ if (t$1.isBinaryExpression(node) && isEqualityOperator(node.operator)) for (const mutableOperator of operators[node.operator]) {
1168
+ const replacement = t$1.cloneNode(node, true);
1132
1169
  replacement.operator = mutableOperator;
1133
1170
  yield replacement;
1134
1171
  }
1135
1172
  };
1136
- //#endregion
1137
- //#region src/mutators/logical-operator-mutator.ts
1138
- const { types: types$11 } = babel;
1139
1173
  const logicalOperatorReplacements = Object.freeze({
1140
1174
  "&&": "||",
1141
1175
  "||": "&&",
1142
1176
  "??": "&&"
1143
1177
  });
1144
1178
  const logicalOperatorMutator = function* (node, _context) {
1145
- if (types$11.isLogicalExpression(node) && isSupported$1(node.operator)) {
1179
+ if (babelTypes$2.isLogicalExpression(node) && isSupportedLogicalOperator(node.operator)) {
1146
1180
  const mutatedOperator = logicalOperatorReplacements[node.operator];
1147
1181
  const replacement = deepCloneNode(node);
1148
1182
  replacement.operator = mutatedOperator;
1149
1183
  yield replacement;
1150
1184
  }
1151
1185
  };
1152
- function isSupported$1(operator) {
1186
+ function isSupportedLogicalOperator(operator) {
1153
1187
  return Object.keys(logicalOperatorReplacements).includes(operator);
1154
1188
  }
1155
- //#endregion
1156
- //#region src/mutators/method-expression-mutator.ts
1157
- const { types: types$10 } = babel;
1158
1189
  const baseReplacements = {
1159
1190
  charAt: null,
1160
1191
  endsWith: "startsWith",
@@ -1180,270 +1211,135 @@ const baseReplacements = {
1180
1211
  setUTCSeconds: "setUTCMilliseconds"
1181
1212
  };
1182
1213
  const noReverseReplacements = ["getUTCDate", "setUTCDate"];
1183
- const replacements = { ...baseReplacements };
1184
- for (const key of Object.keys(baseReplacements)) {
1185
- const value = baseReplacements[key];
1186
- if (value !== null && value !== void 0 && !noReverseReplacements.includes(key)) replacements[value] = key;
1187
- }
1214
+ const replacements = new Map(Object.entries(baseReplacements));
1215
+ for (const [key, value] of Object.entries(baseReplacements)) if (value !== null && !noReverseReplacements.includes(key)) replacements.set(value, key);
1188
1216
  const methodExpressionMutator = function* (node, _context) {
1189
- if (!(types$10.isCallExpression(node) || types$10.isOptionalCallExpression(node))) return;
1217
+ if (!(babelTypes$2.isCallExpression(node) || babelTypes$2.isOptionalCallExpression(node))) return;
1190
1218
  const { callee } = node;
1191
- if (!(types$10.isMemberExpression(callee) || types$10.isOptionalMemberExpression(callee)) || !types$10.isIdentifier(callee.property)) return;
1192
- const newName = replacements[callee.property.name];
1219
+ if (!(babelTypes$2.isMemberExpression(callee) || babelTypes$2.isOptionalMemberExpression(callee)) || !babelTypes$2.isIdentifier(callee.property)) return;
1220
+ const newName = replacements.get(callee.property.name);
1193
1221
  if (newName === void 0) return;
1194
1222
  if (newName === null) {
1195
1223
  yield deepCloneNode(callee.object);
1196
1224
  return;
1197
1225
  }
1198
1226
  const nodeArguments = node.arguments.map((argumentNode) => deepCloneNode(argumentNode));
1199
- const mutatedCallee = types$10.isMemberExpression(callee) ? types$10.memberExpression(deepCloneNode(callee.object), types$10.identifier(newName), false, callee.optional) : types$10.optionalMemberExpression(deepCloneNode(callee.object), types$10.identifier(newName), false, callee.optional);
1200
- yield types$10.isCallExpression(node) ? types$10.callExpression(mutatedCallee, nodeArguments) : types$10.optionalCallExpression(mutatedCallee, nodeArguments, node.optional);
1227
+ let mutatedCallee;
1228
+ if (babelTypes$2.isMemberExpression(callee)) mutatedCallee = babelTypes$2.memberExpression(deepCloneNode(callee.object), babelTypes$2.identifier(newName), false, callee.optional);
1229
+ else mutatedCallee = babelTypes$2.optionalMemberExpression(deepCloneNode(callee.object), babelTypes$2.identifier(newName), false, callee.optional);
1230
+ if (babelTypes$2.isCallExpression(node)) yield babelTypes$2.callExpression(mutatedCallee, nodeArguments);
1231
+ else yield babelTypes$2.optionalCallExpression(mutatedCallee, nodeArguments, node.optional);
1201
1232
  };
1202
- //#endregion
1203
- //#region src/mutators/object-literal-mutator.ts
1204
- const { types: types$9 } = babel;
1205
1233
  const objectLiteralMutator = function* (node, _context) {
1206
- if (types$9.isObjectExpression(node) && node.properties.length > 0) yield types$9.objectExpression([]);
1234
+ if (babelTypes$2.isObjectExpression(node) && node.properties.length > 0) yield babelTypes$2.objectExpression([]);
1207
1235
  };
1208
- //#endregion
1209
- //#region src/mutators/optional-chaining-mutator.ts
1210
- const { types: t$1 } = babel;
1211
1236
  const optionalChainingMutator = function* (node, _context) {
1212
1237
  if (t$1.isOptionalMemberExpression(node) && node.optional) yield t$1.optionalMemberExpression(t$1.cloneNode(node.object, true), t$1.cloneNode(node.property, true), node.computed, false);
1213
1238
  if (t$1.isOptionalCallExpression(node) && node.optional) yield t$1.optionalCallExpression(t$1.cloneNode(node.callee, true), node.arguments.map((arg) => t$1.cloneNode(arg, true)), false);
1214
1239
  };
1215
- //#endregion
1216
- //#region src/mutators/regex-mutation.ts
1240
+ const regexMutator = function* (node, context) {
1241
+ if (babelTypes$2.isRegExpLiteral(node)) for (const replacementPattern of mutateRegexPattern(node.pattern, node.flags)) yield babelTypes$2.regExpLiteral(replacementPattern, node.flags);
1242
+ else if (babelTypes$2.isStringLiteral(node) && isObviousRegexString(node, context)) {
1243
+ const parent = context.parent;
1244
+ if (parent !== void 0 && babelTypes$2.isNewExpression(parent)) {
1245
+ const flags = getFlags(parent);
1246
+ for (const replacementPattern of mutateRegexPattern(node.value, flags)) yield babelTypes$2.stringLiteral(replacementPattern);
1247
+ }
1248
+ }
1249
+ };
1250
+ function isObviousRegexString(node, context) {
1251
+ const parent = context.parent;
1252
+ if (parent === void 0 || !babelTypes$2.isNewExpression(parent) || !babelTypes$2.isIdentifier(parent.callee) || parent.callee.name !== RegExp.name) return false;
1253
+ return parent.arguments[0] === node;
1254
+ }
1255
+ function getFlags(node) {
1256
+ const secondArg = node.arguments[1];
1257
+ if (secondArg !== void 0 && babelTypes$2.isStringLiteral(secondArg)) return secondArg.value;
1258
+ }
1259
+ const stringLiteralMutator = function* (node, context) {
1260
+ if (babelTypes$2.isTemplateLiteral(node)) {
1261
+ const firstQuasi = node.quasis[0];
1262
+ if (firstQuasi === void 0) return;
1263
+ let replacement;
1264
+ if (node.quasis.length === 1 && firstQuasi.value.raw.length === 0) replacement = "Stryker was here!";
1265
+ else replacement = "";
1266
+ yield babelTypes$2.templateLiteral([babelTypes$2.templateElement({ raw: replacement })], []);
1267
+ }
1268
+ if (babelTypes$2.isStringLiteral(node) && isValidParent(node, context)) {
1269
+ let replacement;
1270
+ if (node.value.length === 0) replacement = "Stryker was here!";
1271
+ else replacement = "";
1272
+ yield babelTypes$2.stringLiteral(replacement);
1273
+ }
1274
+ };
1275
+ function isValidParent(child, context) {
1276
+ const parent = context.parent;
1277
+ if (parent === void 0) return true;
1278
+ return !isImportExportRelated(parent) && !isJsxOrExpressionRelated(parent) && !isObjectOrClassPropertyKey(parent, child) && !isDisallowedCallExpression(parent);
1279
+ }
1280
+ function isImportExportRelated(parent) {
1281
+ return babelTypes$2.isImportDeclaration(parent) || babelTypes$2.isExportDeclaration(parent) || babelTypes$2.isImportOrExportDeclaration(parent) || babelTypes$2.isTSExternalModuleReference(parent);
1282
+ }
1283
+ function isJsxOrExpressionRelated(parent) {
1284
+ return babelTypes$2.isJSXAttribute(parent) || babelTypes$2.isExpressionStatement(parent) || babelTypes$2.isTSLiteralType(parent) || babelTypes$2.isObjectMethod(parent);
1285
+ }
1286
+ function isObjectOrClassPropertyKey(parent, child) {
1287
+ return babelTypes$2.isObjectProperty(parent) && parent.key === child || babelTypes$2.isClassProperty(parent) && parent.key === child;
1288
+ }
1289
+ function isDisallowedCallExpression(parent) {
1290
+ return isRequireCall(parent) || isSymbolCall(parent) || isImportCall(parent);
1291
+ }
1292
+ function isRequireCall(parent) {
1293
+ return babelTypes$2.isCallExpression(parent) && babelTypes$2.isIdentifier(parent.callee, { name: "require" });
1294
+ }
1295
+ function isSymbolCall(parent) {
1296
+ return babelTypes$2.isCallExpression(parent) && babelTypes$2.isIdentifier(parent.callee, { name: "Symbol" });
1297
+ }
1298
+ function isImportCall(parent) {
1299
+ return babelTypes$2.isCallExpression(parent) && babelTypes$2.isImport(parent.callee);
1300
+ }
1301
+ const UnaryOperator = {
1302
+ "+": "-",
1303
+ "-": "+",
1304
+ "~": ""
1305
+ };
1306
+ const unaryOperatorMutator = function* (node, _context) {
1307
+ if (babelTypes$2.isUnaryExpression(node) && isSupportedUnaryOperator(node.operator) && node.prefix) {
1308
+ const mutatedOperator = UnaryOperator[node.operator];
1309
+ let replacement;
1310
+ if (isPlusOrMinus(mutatedOperator)) replacement = babelTypes$2.unaryExpression(mutatedOperator, deepCloneNode(node.argument));
1311
+ else replacement = deepCloneNode(node.argument);
1312
+ yield replacement;
1313
+ }
1314
+ };
1315
+ function isSupportedUnaryOperator(operator) {
1316
+ return Object.keys(UnaryOperator).includes(operator);
1317
+ }
1318
+ function isPlusOrMinus(operator) {
1319
+ return operator === "-" || operator === "+";
1320
+ }
1321
+ const UpdateOperators = {
1322
+ "++": "--",
1323
+ "--": "++"
1324
+ };
1325
+ const updateOperatorMutator = function* (node, _context) {
1326
+ if (babelTypes$2.isUpdateExpression(node)) yield babelTypes$2.updateExpression(UpdateOperators[node.operator], deepCloneNode(node.argument), node.prefix);
1327
+ };
1217
1328
  /**
1218
- * The mutations of a regular expression pattern.
1219
- *
1220
- * Pure: a pattern and its flags in, replacement patterns out. No I/O, no clock,
1221
- * no throwing — a pattern this cannot parse yields no mutants, which is the
1222
- * honest answer for a literal whose syntax the engine does not model.
1223
- *
1224
- * The transformation set is fixed and small, and each member changes exactly
1225
- * one thing about the pattern:
1226
- *
1227
- * | family | example |
1228
- * | ----------------------- | ------------------------ |
1229
- * | anchor removal | `^abc$` -> `abc$`, `^abc` |
1230
- * | character class negation| `[abc]` <-> `[^abc]` |
1231
- * | predefined class negation| `\d` <-> `\D`, `\p{L}` <-> `\P{L}` |
1232
- * | quantifier removal | `a+`, `a*`, `a{2,3}` -> `a` |
1233
- * | lookaround negation | `(?=a)` <-> `(?!a)`, `(?<=a)` <-> `(?<!a)` |
1329
+ * Every mutator this instrumenter can apply, named explicitly.
1234
1330
  *
1235
- * Alternation and grouping are deliberately untouched: swapping a branch or
1236
- * dropping a group produces mutants that survive for reasons unrelated to the
1237
- * test suite's strength, which inflates a score rather than measuring one.
1331
+ * This list is deliberately hand-written rather than self-registering. A
1332
+ * registry populated by import side effects — each mutator module calling
1333
+ * `registerMutator(self)` at module scope — makes the mutant population depend
1334
+ * on which imports were evaluated: import order decides the order, a bundler
1335
+ * that judges a side-effect-only import unused drops a mutator entirely, and
1336
+ * anything reading the array before the last import finished sees a short list.
1337
+ * Every one of those failures REMOVES mutants, which RAISES the mutation score,
1338
+ * so the tool reports a better number for doing less work and nothing anywhere
1339
+ * says so.
1238
1340
  *
1239
- * The order is part of the contract, because a mutant's identity in a report is
1240
- * its position: anchors first, then each remaining position left to right with
1241
- * quantifier removal ahead of class negation.
1242
- */
1243
- function mutateRegexPattern(pattern, flags) {
1244
- if (pattern.length === 0) return [];
1245
- try {
1246
- const f = flags ?? "";
1247
- const ast = new RegExpParser().parsePattern(pattern, void 0, void 0, {
1248
- unicode: f.includes("u"),
1249
- unicodeSets: f.includes("v")
1250
- });
1251
- const bol = [];
1252
- const eol = [];
1253
- const rest = [];
1254
- visitRegExpAST(ast, {
1255
- onAssertionEnter(node) {
1256
- if (node.kind === "start") {
1257
- const splice = {
1258
- start: node.start,
1259
- end: node.end,
1260
- text: ""
1261
- };
1262
- if ((pattern.slice(0, splice.start) + splice.text + pattern.slice(splice.end)).length === 0) return;
1263
- bol.push(splice);
1264
- } else if (node.kind === "end") {
1265
- const splice = {
1266
- start: node.start,
1267
- end: node.end,
1268
- text: ""
1269
- };
1270
- if ((pattern.slice(0, splice.start) + splice.text + pattern.slice(splice.end)).length === 0) return;
1271
- eol.push(splice);
1272
- } else if (node.kind === "lookahead" || node.kind === "lookbehind") {
1273
- const offset = node.kind === "lookahead" ? 2 : 3;
1274
- const pos = node.start + offset;
1275
- const text = node.negate ? "=" : "!";
1276
- rest.push({
1277
- start: pos,
1278
- end: pos + 1,
1279
- text,
1280
- priority: 1
1281
- });
1282
- }
1283
- },
1284
- onCharacterClassEnter(node) {
1285
- const pos = node.start + 1;
1286
- if (node.negate) rest.push({
1287
- start: pos,
1288
- end: pos + 1,
1289
- text: "",
1290
- priority: 1
1291
- });
1292
- else rest.push({
1293
- start: pos,
1294
- end: pos,
1295
- text: "^",
1296
- priority: 1
1297
- });
1298
- },
1299
- onCharacterSetEnter(node) {
1300
- if (node.kind === "digit" || node.kind === "space" || node.kind === "word") {
1301
- const pos = node.start + 1;
1302
- const cur = node.raw[1] ?? "";
1303
- const toggled = cur === cur.toUpperCase() ? cur.toLowerCase() : cur.toUpperCase();
1304
- rest.push({
1305
- start: pos,
1306
- end: pos + 1,
1307
- text: toggled,
1308
- priority: 2
1309
- });
1310
- } else if (node.kind === "property") {
1311
- const pos = node.start + 1;
1312
- const toggled = (node.raw[1] ?? "") === "p" ? "P" : "p";
1313
- rest.push({
1314
- start: pos,
1315
- end: pos + 1,
1316
- text: toggled,
1317
- priority: 2
1318
- });
1319
- }
1320
- },
1321
- onQuantifierEnter(node) {
1322
- rest.push({
1323
- start: node.start,
1324
- end: node.end,
1325
- text: node.element.raw,
1326
- priority: 0
1327
- });
1328
- }
1329
- });
1330
- rest.sort((a, b) => a.start - b.start || a.priority - b.priority);
1331
- return [
1332
- ...bol,
1333
- ...eol,
1334
- ...rest
1335
- ].map((s) => pattern.slice(0, s.start) + s.text + pattern.slice(s.end));
1336
- } catch {
1337
- return [];
1338
- }
1339
- }
1340
- //#endregion
1341
- //#region src/mutators/regex-mutator.ts
1342
- const { types: types$8 } = babel;
1343
- const regexMutator = function* (node, context) {
1344
- if (types$8.isRegExpLiteral(node)) for (const replacementPattern of mutateRegexPattern(node.pattern, node.flags)) yield types$8.regExpLiteral(replacementPattern, node.flags);
1345
- else if (types$8.isStringLiteral(node) && isObviousRegexString(node, context)) {
1346
- const parent = context.parent;
1347
- if (parent !== void 0 && types$8.isNewExpression(parent)) {
1348
- const flags = getFlags(parent);
1349
- for (const replacementPattern of mutateRegexPattern(node.value, flags)) yield types$8.stringLiteral(replacementPattern);
1350
- }
1351
- }
1352
- };
1353
- function isObviousRegexString(node, context) {
1354
- const parent = context.parent;
1355
- if (parent === void 0 || !types$8.isNewExpression(parent) || !types$8.isIdentifier(parent.callee) || parent.callee.name !== RegExp.name) return false;
1356
- return parent.arguments[0] === node;
1357
- }
1358
- function getFlags(node) {
1359
- const secondArg = node.arguments[1];
1360
- if (secondArg !== void 0 && types$8.isStringLiteral(secondArg)) return secondArg.value;
1361
- }
1362
- //#endregion
1363
- //#region src/mutators/string-literal-mutator.ts
1364
- const { types: types$7 } = babel;
1365
- const stringLiteralMutator = function* (node, context) {
1366
- if (types$7.isTemplateLiteral(node)) {
1367
- const firstQuasi = node.quasis[0];
1368
- if (firstQuasi === void 0) return;
1369
- const replacement = node.quasis.length === 1 && firstQuasi.value.raw.length === 0 ? "Stryker was here!" : "";
1370
- yield types$7.templateLiteral([types$7.templateElement({ raw: replacement })], []);
1371
- }
1372
- if (types$7.isStringLiteral(node) && isValidParent(node, context)) yield types$7.stringLiteral(node.value.length === 0 ? "Stryker was here!" : "");
1373
- };
1374
- function isValidParent(child, context) {
1375
- const parent = context.parent;
1376
- if (parent === void 0) return true;
1377
- return !isImportExportRelated(parent) && !isJsxOrExpressionRelated(parent) && !isObjectOrClassPropertyKey(parent, child) && !isDisallowedCallExpression(parent);
1378
- }
1379
- function isImportExportRelated(parent) {
1380
- return types$7.isImportDeclaration(parent) || types$7.isExportDeclaration(parent) || types$7.isImportOrExportDeclaration(parent) || types$7.isTSExternalModuleReference(parent);
1381
- }
1382
- function isJsxOrExpressionRelated(parent) {
1383
- return types$7.isJSXAttribute(parent) || types$7.isExpressionStatement(parent) || types$7.isTSLiteralType(parent) || types$7.isObjectMethod(parent);
1384
- }
1385
- function isObjectOrClassPropertyKey(parent, child) {
1386
- return types$7.isObjectProperty(parent) && parent.key === child || types$7.isClassProperty(parent) && parent.key === child;
1387
- }
1388
- function isDisallowedCallExpression(parent) {
1389
- return isRequireCall(parent) || isSymbolCall(parent) || isImportCall(parent);
1390
- }
1391
- function isRequireCall(parent) {
1392
- return types$7.isCallExpression(parent) && types$7.isIdentifier(parent.callee, { name: "require" });
1393
- }
1394
- function isSymbolCall(parent) {
1395
- return types$7.isCallExpression(parent) && types$7.isIdentifier(parent.callee, { name: "Symbol" });
1396
- }
1397
- function isImportCall(parent) {
1398
- return types$7.isCallExpression(parent) && types$7.isImport(parent.callee);
1399
- }
1400
- //#endregion
1401
- //#region src/mutators/unary-operator-mutator.ts
1402
- const { types: types$6 } = babel;
1403
- const UnaryOperator = {
1404
- "+": "-",
1405
- "-": "+",
1406
- "~": ""
1407
- };
1408
- const unaryOperatorMutator = function* (node, _context) {
1409
- if (types$6.isUnaryExpression(node) && isSupported(node.operator) && node.prefix) {
1410
- const mutatedOperator = UnaryOperator[node.operator];
1411
- yield isPlusOrMinus(mutatedOperator) ? types$6.unaryExpression(mutatedOperator, deepCloneNode(node.argument)) : deepCloneNode(node.argument);
1412
- }
1413
- };
1414
- function isSupported(operator) {
1415
- return Object.keys(UnaryOperator).includes(operator);
1416
- }
1417
- function isPlusOrMinus(operator) {
1418
- return operator === "-" || operator === "+";
1419
- }
1420
- //#endregion
1421
- //#region src/mutators/update-operator-mutator.ts
1422
- const { types: types$5 } = babel;
1423
- const UpdateOperators = {
1424
- "++": "--",
1425
- "--": "++"
1426
- };
1427
- const updateOperatorMutator = function* (node, _context) {
1428
- if (types$5.isUpdateExpression(node)) yield types$5.updateExpression(UpdateOperators[node.operator], deepCloneNode(node.argument), node.prefix);
1429
- };
1430
- //#endregion
1431
- //#region src/mutators/registry.ts
1432
- /**
1433
- * Every mutator this instrumenter can apply, named explicitly.
1434
- *
1435
- * This list is deliberately hand-written rather than self-registering. A
1436
- * registry populated by import side effects — each mutator module calling
1437
- * `registerMutator(self)` at module scope — makes the mutant population depend
1438
- * on which imports were evaluated: import order decides the order, a bundler
1439
- * that judges a side-effect-only import unused drops a mutator entirely, and
1440
- * anything reading the array before the last import finished sees a short list.
1441
- * Every one of those failures REMOVES mutants, which RAISES the mutation score,
1442
- * so the tool reports a better number for doing less work and nothing anywhere
1443
- * says so.
1444
- *
1445
- * Naming each mutator here costs one line when a mutator is added and makes
1446
- * that line a compile-checked import instead of a runtime effect.
1341
+ * Naming each mutator here costs one line when a mutator is added and makes
1342
+ * that line a compile-checked import instead of a runtime effect.
1447
1343
  */
1448
1344
  const allMutators = Object.freeze({
1449
1345
  ArithmeticOperator: arithmeticOperatorMutator,
@@ -1463,107 +1359,184 @@ const allMutators = Object.freeze({
1463
1359
  UnaryOperator: unaryOperatorMutator,
1464
1360
  UpdateOperator: updateOperatorMutator
1465
1361
  });
1362
+ S.TaggedError("@systemfsoftware/stryker-js-instrumenter/PlacementFailed")("PlacementFailed", {
1363
+ fileName: S.String,
1364
+ placerName: S.String,
1365
+ mutantIds: S.Array(S.String),
1366
+ cause: S.Defect()
1367
+ });
1368
+ S.TaggedError("@systemfsoftware/stryker-js-instrumenter/TransformFailed")("TransformFailed", {
1369
+ fileName: S.String,
1370
+ cause: S.Defect()
1371
+ });
1466
1372
  //#endregion
1467
- //#region src/babel/instrumentation-header.ts
1468
- const STRYKER_NAMESPACE_HELPER = "stryNS_9fa48";
1469
- const COVER_MUTANT_HELPER$1 = "stryCov_9fa48";
1470
- const IS_MUTANT_ACTIVE_HELPER$1 = "stryMutAct_9fa48";
1471
- const { types: types$4 } = babel;
1373
+ //#region src/Transformer.ts
1472
1374
  /**
1473
- * Returns syntax for the header if JS/TS files
1375
+ * Transformer — traverses ASTs, asks mutators for mutants and placers to instrument them.
1474
1376
  */
1475
- const parsedInstrumentationHeader = babel.parse(`function ${STRYKER_NAMESPACE_HELPER}(){
1476
- var g = typeof globalThis === 'object' && globalThis && globalThis.Math === Math && globalThis || new Function("return this")();
1477
- var ns = g.${INSTRUMENTER_CONSTANTS.NAMESPACE} || (g.${INSTRUMENTER_CONSTANTS.NAMESPACE} = {});
1478
- if (ns.${INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT} === undefined && g.process && g.process.env && g.process.env.${INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT_ENV_VARIABLE}) {
1479
- ns.${INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT} = g.process.env.${INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT_ENV_VARIABLE};
1480
- }
1481
- function retrieveNS(){
1482
- return ns;
1483
- }
1484
- ${STRYKER_NAMESPACE_HELPER} = retrieveNS;
1485
- return retrieveNS();
1486
- }
1487
- ${STRYKER_NAMESPACE_HELPER}();
1488
-
1489
- function ${COVER_MUTANT_HELPER$1}() {
1490
- var ns = ${STRYKER_NAMESPACE_HELPER}();
1491
- var cov = ns.${INSTRUMENTER_CONSTANTS.MUTATION_COVERAGE_OBJECT} || (ns.${INSTRUMENTER_CONSTANTS.MUTATION_COVERAGE_OBJECT} = { static: {}, perTest: {} });
1492
- function cover() {
1493
- var c = cov.static;
1494
- if (ns.${INSTRUMENTER_CONSTANTS.CURRENT_TEST_ID}) {
1495
- c = cov.perTest[ns.${INSTRUMENTER_CONSTANTS.CURRENT_TEST_ID}] = cov.perTest[ns.${INSTRUMENTER_CONSTANTS.CURRENT_TEST_ID}] || {};
1496
- }
1497
- var a = arguments;
1498
- for(var i=0; i < a.length; i++){
1499
- c[a[i]] = (c[a[i]] || 0) + 1;
1500
- }
1501
- }
1502
- ${COVER_MUTANT_HELPER$1} = cover;
1503
- cover.apply(null, arguments);
1377
+ const { types: babelTypes$1 } = babel;
1378
+ const t = babelTypes$1;
1379
+ const STRYKER_NAMESPACE_HELPER = "stryNS_9fa48";
1380
+ const COVER_MUTANT_HELPER = "stryCov_9fa48";
1381
+ const IS_MUTANT_ACTIVE_HELPER = "stryMutAct_9fa48";
1382
+ function createMutantCollector() {
1383
+ return [];
1504
1384
  }
1505
- function ${IS_MUTANT_ACTIVE_HELPER$1}(id) {
1506
- var ns = ${STRYKER_NAMESPACE_HELPER}();
1507
- function isActive(id) {
1508
- if (ns.${INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT} === id) {
1509
- if (ns.${INSTRUMENTER_CONSTANTS.HIT_COUNT} !== void 0 && ++ns.${INSTRUMENTER_CONSTANTS.HIT_COUNT} > ns.${INSTRUMENTER_CONSTANTS.HIT_LIMIT}) {
1510
- throw new Error('Stryker: Hit count limit reached (' + ns.${INSTRUMENTER_CONSTANTS.HIT_COUNT} + ')');
1511
- }
1512
- return true;
1513
- }
1514
- return false;
1515
- }
1516
- ${IS_MUTANT_ACTIVE_HELPER$1} = isActive;
1517
- return isActive(id);
1518
- }`, {
1519
- configFile: false,
1520
- browserslistConfigFile: false,
1521
- env: { targets: {} }
1522
- });
1523
- if (!types$4.isFile(parsedInstrumentationHeader)) throw new Error("Instrumentation header parsed as non-File");
1524
- const instrumentationBabelHeader = parsedInstrumentationHeader.program.body;
1525
- deepFreeze(instrumentationBabelHeader);
1526
- function placeHeaderIfNeeded(mutantCollector, originFileName, options, root) {
1527
- if (hasPlacedMutants(mutantCollector, originFileName) && !options.noHeader) placeHeader(root);
1385
+ function collect(collector, fileName, original, mutable, offset = {
1386
+ line: 0,
1387
+ column: 0
1388
+ }) {
1389
+ const mutant = createMutant(collector.length.toString(), fileName, original, mutable, offset);
1390
+ collector.push(mutant);
1391
+ return mutant;
1528
1392
  }
1529
- function placeHeader(root) {
1530
- let header = instrumentationBabelHeader;
1531
- const leadingComments = root.program.body[0]?.leadingComments;
1532
- if (Array.isArray(leadingComments)) {
1533
- const firstHeader = instrumentationBabelHeader[0];
1534
- if (firstHeader === void 0) throw new Error("Instrumentation header is empty");
1535
- const cloned = types$4.cloneNode(firstHeader, true, false);
1536
- cloned.leadingComments = leadingComments;
1537
- header = [cloned, ...instrumentationBabelHeader.slice(1)];
1538
- }
1539
- root.program.body.unshift(...header);
1393
+ function hasPlacedMutants(collector, fileName) {
1394
+ return collector.some((mutant) => mutant.fileName === fileName && mutant.ignoreReason === void 0);
1540
1395
  }
1541
- function deepFreeze(value) {
1542
- if (value !== null && typeof value === "object") {
1543
- if (Array.isArray(value)) {
1544
- for (const item of value) deepFreeze(item);
1545
- return Object.freeze(value);
1546
- }
1547
- if (value instanceof RegExp) return Object.freeze(value);
1548
- if (value instanceof Map) {
1549
- for (const [k, v] of value.entries()) {
1550
- deepFreeze(k);
1551
- deepFreeze(v);
1552
- }
1553
- return Object.freeze(value);
1554
- }
1555
- if (value instanceof Set) {
1556
- for (const v of value.values()) deepFreeze(v);
1557
- return Object.freeze(value);
1396
+ const WILDCARD = "all";
1397
+ const DEFAULT_REASON = "Ignored using a comment";
1398
+ const strykerCommentDirectiveRegex = /^\s?Stryker (disable|restore)(?: (next-line))? ([a-zA-Z, ]+)(?::(.+)?)?/;
1399
+ const rootRule = { kind: "Root" };
1400
+ function findIgnoreReason(rule, mutatorName, line) {
1401
+ const lower = mutatorName.toLowerCase();
1402
+ let current = rule;
1403
+ for (;;) switch (current.kind) {
1404
+ case "Root": return;
1405
+ case "Ignore": {
1406
+ const lineMatches = current.line === void 0 || current.line === line;
1407
+ const mutatorMatches = current.mutatorNames.includes(lower) || current.mutatorNames.includes(WILDCARD);
1408
+ if (lineMatches && mutatorMatches) return current.ignoreReason;
1409
+ current = current.previous;
1410
+ break;
1411
+ }
1412
+ case "Restore": {
1413
+ const lineMatches = current.line === void 0 || current.line === line;
1414
+ const mutatorMatches = current.mutatorNames.includes(lower) || current.mutatorNames.includes(WILDCARD);
1415
+ if (lineMatches && mutatorMatches) return;
1416
+ current = current.previous;
1417
+ break;
1558
1418
  }
1559
- for (const v of Object.values(value)) deepFreeze(v);
1560
- return Object.freeze(value);
1561
1419
  }
1562
- return value;
1563
1420
  }
1564
- //#endregion
1565
- //#region src/babel/type-guards.ts
1566
- const { types: types$3 } = babel;
1421
+ function processStrykerDirectives(rule, node, allMutatorNames, originFileName) {
1422
+ const leadingComments = node.leadingComments;
1423
+ if (!leadingComments) return {
1424
+ rule,
1425
+ warnings: []
1426
+ };
1427
+ let current = rule;
1428
+ const warnings = [];
1429
+ for (const comment of leadingComments) {
1430
+ const matchResult = strykerCommentDirectiveRegex.exec(comment.value);
1431
+ if (!matchResult) continue;
1432
+ const directiveType = matchResult[1];
1433
+ const scope = matchResult[2];
1434
+ const mutators = matchResult[3];
1435
+ const optionalReason = matchResult[4];
1436
+ if (directiveType === void 0 || mutators === void 0) throw new Error("Stryker directive without directive type or mutators");
1437
+ let mutatorNames = mutators.split(",").map((mutator) => mutator.trim());
1438
+ for (const mutator of mutatorNames) {
1439
+ if (mutator === WILDCARD) continue;
1440
+ if (!allMutatorNames.includes(mutator.toLowerCase())) {
1441
+ const commentLoc = comment.loc;
1442
+ if (commentLoc == null) throw new Error("Comment without location");
1443
+ let directiveLabel;
1444
+ if (scope !== void 0 && scope !== "") directiveLabel = `${directiveType} ${scope}`;
1445
+ else directiveLabel = directiveType;
1446
+ warnings.push(`Unused 'Stryker ${directiveLabel}' directive. Mutator with name '${mutator}' not found. Directive found at: ${originFileName}:${commentLoc.start.line}:${commentLoc.start.column}.`);
1447
+ }
1448
+ }
1449
+ mutatorNames = mutatorNames.map((mutator) => mutator.toLowerCase());
1450
+ const reason = (optionalReason ?? DEFAULT_REASON).trim();
1451
+ current = applyDirective(current, directiveType, scope, mutatorNames, reason, node.loc);
1452
+ }
1453
+ return {
1454
+ rule: current,
1455
+ warnings
1456
+ };
1457
+ }
1458
+ function applyDirective(rule, directiveType, scope, mutatorNames, reason, loc) {
1459
+ switch (directiveType) {
1460
+ case "disable": return applyDisable(rule, scope, mutatorNames, reason, loc);
1461
+ case "restore": return applyRestore(rule, scope, mutatorNames, loc);
1462
+ default: return rule;
1463
+ }
1464
+ }
1465
+ function applyDisable(rule, scope, mutatorNames, reason, loc) {
1466
+ switch (scope) {
1467
+ case "next-line": return {
1468
+ kind: "Ignore",
1469
+ mutatorNames,
1470
+ line: getLine(loc),
1471
+ ignoreReason: reason,
1472
+ previous: rule
1473
+ };
1474
+ case void 0:
1475
+ default: return {
1476
+ kind: "Ignore",
1477
+ mutatorNames,
1478
+ line: void 0,
1479
+ ignoreReason: reason,
1480
+ previous: rule
1481
+ };
1482
+ }
1483
+ }
1484
+ function applyRestore(rule, scope, mutatorNames, loc) {
1485
+ switch (scope) {
1486
+ case "next-line": return {
1487
+ kind: "Restore",
1488
+ mutatorNames,
1489
+ line: getLine(loc),
1490
+ previous: rule
1491
+ };
1492
+ case void 0:
1493
+ default: return {
1494
+ kind: "Restore",
1495
+ mutatorNames,
1496
+ line: void 0,
1497
+ previous: rule
1498
+ };
1499
+ }
1500
+ }
1501
+ function getLine(loc) {
1502
+ if (loc == null) throw new Error("Babel node without location");
1503
+ return loc.start.line;
1504
+ }
1505
+ function toIgnorerPath(path) {
1506
+ let parentResult = null;
1507
+ if (path.parentPath !== null) parentResult = toIgnorerPath(path.parentPath);
1508
+ return {
1509
+ node: path.node,
1510
+ parentPath: parentResult,
1511
+ isObjectExpression() {
1512
+ return path.isObjectExpression();
1513
+ },
1514
+ isCallExpression() {
1515
+ return path.isCallExpression() || path.isOptionalCallExpression();
1516
+ },
1517
+ isClassProperty() {
1518
+ return path.isClassProperty();
1519
+ },
1520
+ isClassPrivateProperty() {
1521
+ return path.isClassPrivateProperty();
1522
+ },
1523
+ isClassAccessorProperty() {
1524
+ return path.isClassAccessorProperty();
1525
+ }
1526
+ };
1527
+ }
1528
+ /**
1529
+ * Wraps a parsed AST the way Babel's own pipeline does, so
1530
+ * `NodePath#buildCodeFrameError` can render a code frame
1531
+ * (https://github.com/babel/babel/issues/11889). Without the wrapper a
1532
+ * placement failure reports no source context.
1533
+ */
1534
+ function createBabelFile(filename, code, ast) {
1535
+ return new File({ filename }, {
1536
+ code,
1537
+ ast
1538
+ });
1539
+ }
1567
1540
  function isTypeNode(path) {
1568
1541
  return path.isTypeAnnotation() || flowTypeAnnotationNodeTypes.includes(path.node.type) || tsTypeAnnotationNodeTypes.includes(path.node.type) || isDeclareVariableStatement(path) || isDeclareModule(path);
1569
1542
  }
@@ -1610,19 +1583,14 @@ const flowTypeAnnotationNodeTypes = Object.freeze([
1610
1583
  "InterfaceDeclaration"
1611
1584
  ]);
1612
1585
  function isImportDeclaration(path) {
1613
- return types$3.isTSImportEqualsDeclaration(path.node) || path.isImportDeclaration();
1586
+ return babelTypes$1.isTSImportEqualsDeclaration(path.node) || path.isImportDeclaration();
1614
1587
  }
1615
- //#endregion
1616
- //#region src/mutant-placers/mutant-expression.ts
1617
- const { types: types$2 } = babel;
1618
- const COVER_MUTANT_HELPER = "stryCov_9fa48";
1619
- const IS_MUTANT_ACTIVE_HELPER = "stryMutAct_9fa48";
1620
1588
  /**
1621
1589
  * returns syntax for `global.activeMutant === $mutantId`
1622
1590
  * @param mutantId The id of the mutant to switch
1623
1591
  */
1624
1592
  function mutantTestExpression(mutantId) {
1625
- return types$2.callExpression(types$2.identifier(IS_MUTANT_ACTIVE_HELPER), [types$2.stringLiteral(mutantId)]);
1593
+ return babelTypes$1.callExpression(babelTypes$1.identifier(IS_MUTANT_ACTIVE_HELPER), [babelTypes$1.stringLiteral(mutantId)]);
1626
1594
  }
1627
1595
  /**
1628
1596
  * Returns a sequence of mutation coverage counters with an optional last expression.
@@ -1632,13 +1600,11 @@ function mutantTestExpression(mutantId) {
1632
1600
  * @param targetExpression The original expression
1633
1601
  */
1634
1602
  function mutationCoverageSequenceExpression(mutants, targetExpression) {
1635
- const mutantIds = [...mutants].map((mutant) => types$2.stringLiteral(mutant.id));
1636
- const sequence = [types$2.callExpression(types$2.identifier(COVER_MUTANT_HELPER), mutantIds)];
1603
+ const mutantIds = [...mutants].map((mutant) => babelTypes$1.stringLiteral(mutant.id));
1604
+ const sequence = [babelTypes$1.callExpression(babelTypes$1.identifier(COVER_MUTANT_HELPER), mutantIds)];
1637
1605
  if (targetExpression) sequence.push(targetExpression);
1638
- return types$2.sequenceExpression(sequence);
1606
+ return babelTypes$1.sequenceExpression(sequence);
1639
1607
  }
1640
- //#endregion
1641
- //#region src/mutant-placers/mutant-placer.ts
1642
1608
  /**
1643
1609
  * Narrows an applied mutant to the node kind a placer emits. `applied()` hands
1644
1610
  * back a plain node — whether it fits this position is the placer's claim, and
@@ -1649,9 +1615,16 @@ function nodeOfKind(mutant, node, isKind, kind) {
1649
1615
  if (!isKind(node)) throw new Error(`Cannot place mutant ${mutant.id}: expected ${kind}, got ${node.type}`);
1650
1616
  return node;
1651
1617
  }
1652
- //#endregion
1653
- //#region src/mutant-placers/expression-mutant-placer.ts
1654
- const { types: types$1 } = babel;
1618
+ function throwPlacementError(error, nodePath, placer, mutants, fileName) {
1619
+ const location = `${path$1.relative(process.cwd(), fileName)}:${nodePath.node.loc?.start.line}:${nodePath.node.loc?.start.column}`;
1620
+ const message = `${placer.name} could not place mutants with type(s): "${new Intl.ListFormat("en").format(mutants.map((mutant) => mutant.mutatorName))}"`;
1621
+ const errorMessage = `${location} ${message}. Either remove this file from the list of files to be mutated, or exclude the mutator (using ${propertyPath()("mutator", "excludedMutations")}). Please report this issue at ${strykerReportBugUrl(message)}. Original error: ${error.stack}`;
1622
+ let builtError = new Error(errorMessage);
1623
+ try {
1624
+ builtError = nodePath.buildCodeFrameError(errorMessage);
1625
+ } catch {}
1626
+ throw builtError;
1627
+ }
1655
1628
  /**
1656
1629
  * Will set the identifier of anonymous function expressions if is located in a variable declaration.
1657
1630
  * Will treat input as readonly. Returns undefined if not needed.
@@ -1662,13 +1635,10 @@ const { types: types$1 } = babel;
1662
1635
  */
1663
1636
  function classOrFunctionExpressionNamedIfNeeded(path) {
1664
1637
  if ((path.isFunctionExpression() || path.isClassExpression()) && !path.node.id) {
1665
- if (path.parentPath.isVariableDeclarator() && types$1.isIdentifier(path.parentPath.node.id)) {
1638
+ if (path.parentPath.isVariableDeclarator() && babelTypes$1.isIdentifier(path.parentPath.node.id)) {
1666
1639
  path.node.id = path.parentPath.node.id;
1667
1640
  return path.node;
1668
- } else if (path.parentPath.isObjectProperty() && types$1.isIdentifier(path.parentPath.node.key) && path.getStatementParent()?.isVariableDeclaration()) {
1669
- path.node.id = path.parentPath.node.key;
1670
- return path.node;
1671
- }
1641
+ } else if (path.parentPath.isObjectProperty() && babelTypes$1.isIdentifier(path.parentPath.node.key) && path.getStatementParent()?.isVariableDeclaration() === true) return path.node;
1672
1642
  }
1673
1643
  }
1674
1644
  /**
@@ -1680,7 +1650,7 @@ function classOrFunctionExpressionNamedIfNeeded(path) {
1680
1650
  * const a = (() => { const a = () => {}; return a; })()
1681
1651
  */
1682
1652
  function arrowFunctionExpressionNamedIfNeeded(path) {
1683
- if (path.isArrowFunctionExpression() && path.parentPath.isVariableDeclarator() && types$1.isIdentifier(path.parentPath.node.id)) return types$1.callExpression(types$1.arrowFunctionExpression([], types$1.blockStatement([types$1.variableDeclaration("const", [types$1.variableDeclarator(path.parentPath.node.id, path.node)]), types$1.returnStatement(path.parentPath.node.id)])), []);
1653
+ if (path.isArrowFunctionExpression() && path.parentPath.isVariableDeclarator() && babelTypes$1.isIdentifier(path.parentPath.node.id)) return babelTypes$1.callExpression(babelTypes$1.arrowFunctionExpression([], babelTypes$1.blockStatement([babelTypes$1.variableDeclaration("const", [babelTypes$1.variableDeclarator(path.parentPath.node.id, path.node)]), babelTypes$1.returnStatement(path.parentPath.node.id)])), []);
1684
1654
  }
1685
1655
  function nameIfAnonymous(path) {
1686
1656
  return classOrFunctionExpressionNamedIfNeeded(path) ?? arrowFunctionExpressionNamedIfNeeded(path) ?? path.node;
@@ -1746,285 +1716,285 @@ function isValidExpression(path) {
1746
1716
  return parent.isAssignmentExpression() && parent.node.left === path.node;
1747
1717
  }
1748
1718
  }
1749
- /**
1750
- * Places the mutants with a conditional expression: `global.activeMutant === 1? mutatedCode : originalCode`;
1751
- */
1752
- const expressionMutantPlacer = {
1753
- name: "expressionMutantPlacer",
1754
- canPlace(path) {
1755
- return path.isExpression() && isValidExpression(path);
1756
- },
1757
- place(path, appliedMutants) {
1758
- let expression = nameIfAnonymous(path);
1759
- expression = mutationCoverageSequenceExpression(appliedMutants.keys(), expression);
1760
- for (const [mutant, appliedMutant] of appliedMutants) expression = types$1.conditionalExpression(mutantTestExpression(mutant.id), nodeOfKind(mutant, appliedMutant, types$1.isExpression, "an expression"), expression);
1761
- path.replaceWith(expression);
1762
- }
1763
- };
1764
- //#endregion
1765
- //#region src/mutant-placers/statement-mutant-placer.ts
1766
- const { types: t } = babel;
1767
- /**
1768
- * Mutant placer that places mutants in statements that allow it.
1769
- * It uses an `if` statement to do so
1770
- */
1771
- const statementMutantPlacer = {
1772
- name: "statementMutantPlacer",
1773
- canPlace(path) {
1774
- return path.isStatement();
1719
+ const allMutantPlacers = Object.freeze([
1720
+ {
1721
+ name: "expressionMutantPlacer",
1722
+ canPlace(path) {
1723
+ return path.isExpression() && isValidExpression(path);
1724
+ },
1725
+ place(path, appliedMutants) {
1726
+ let expression = nameIfAnonymous(path);
1727
+ expression = mutationCoverageSequenceExpression(appliedMutants.keys(), expression);
1728
+ for (const [mutant, appliedMutant] of appliedMutants) expression = babelTypes$1.conditionalExpression(mutantTestExpression(mutant.id), nodeOfKind(mutant, appliedMutant, babelTypes$1.isExpression, "an expression"), expression);
1729
+ path.replaceWith(expression);
1730
+ }
1775
1731
  },
1776
- place(path, appliedMutants) {
1777
- let statement = t.blockStatement([t.expressionStatement(mutationCoverageSequenceExpression(appliedMutants.keys())), ...path.isBlockStatement() ? path.node.body : [path.node]]);
1778
- for (const [mutant, appliedMutant] of appliedMutants) statement = t.ifStatement(mutantTestExpression(mutant.id), t.blockStatement([nodeOfKind(mutant, appliedMutant, t.isStatement, "a statement")]), statement);
1779
- path.replaceWith(path.isBlockStatement() ? t.blockStatement([statement]) : statement);
1780
- }
1781
- };
1782
- //#endregion
1783
- //#region src/mutant-placers/switch-case-mutant-placer.ts
1784
- /**
1785
- * Places the mutants with consequent of a SwitchCase node. Uses an if-statement to do so.
1786
- * @example
1787
- * case 'foo':
1788
- * if (stryMutAct_9fa48(0)) {} else {
1789
- * stryCov_9fa48(0);
1790
- * console.log('bar');
1791
- * break;
1792
- * }
1793
- */
1794
- const switchCaseMutantPlacer = {
1795
- name: "switchCaseMutantPlacer",
1796
- canPlace(path) {
1797
- return path.isSwitchCase();
1732
+ {
1733
+ name: "statementMutantPlacer",
1734
+ canPlace(path) {
1735
+ return path.isStatement();
1736
+ },
1737
+ place(path, appliedMutants) {
1738
+ const bodyStatements = [t.expressionStatement(mutationCoverageSequenceExpression(appliedMutants.keys()))];
1739
+ if (path.isBlockStatement()) bodyStatements.push(...path.node.body);
1740
+ else bodyStatements.push(path.node);
1741
+ let statement = t.blockStatement(bodyStatements);
1742
+ for (const [mutant, appliedMutant] of appliedMutants) statement = t.ifStatement(mutantTestExpression(mutant.id), t.blockStatement([nodeOfKind(mutant, appliedMutant, t.isStatement, "a statement")]), statement);
1743
+ if (path.isBlockStatement()) path.replaceWith(t.blockStatement([statement]));
1744
+ else path.replaceWith(statement);
1745
+ }
1798
1746
  },
1799
- place(path, appliedMutants) {
1800
- let consequence = babel.types.blockStatement([babel.types.expressionStatement(mutationCoverageSequenceExpression(appliedMutants.keys())), ...path.node.consequent]);
1801
- for (const [mutant, appliedMutant] of appliedMutants) {
1802
- const switchCase = nodeOfKind(mutant, appliedMutant, babel.types.isSwitchCase, "a switch case");
1803
- consequence = babel.types.ifStatement(mutantTestExpression(mutant.id), babel.types.blockStatement(switchCase.consequent), consequence);
1747
+ {
1748
+ name: "switchCaseMutantPlacer",
1749
+ canPlace(path) {
1750
+ return path.isSwitchCase();
1751
+ },
1752
+ place(path, appliedMutants) {
1753
+ let consequence = babel.types.blockStatement([babel.types.expressionStatement(mutationCoverageSequenceExpression(appliedMutants.keys())), ...path.node.consequent]);
1754
+ for (const [mutant, appliedMutant] of appliedMutants) {
1755
+ const switchCase = nodeOfKind(mutant, appliedMutant, babel.types.isSwitchCase, "a switch case");
1756
+ consequence = babel.types.ifStatement(mutantTestExpression(mutant.id), babel.types.blockStatement(switchCase.consequent), consequence);
1757
+ }
1758
+ path.replaceWith(babel.types.switchCase(path.node.test, [consequence]));
1804
1759
  }
1805
- path.replaceWith(babel.types.switchCase(path.node.test, [consequence]));
1806
1760
  }
1807
- };
1808
- //#endregion
1809
- //#region src/mutant-placers/throw-placement-error.ts
1810
- function throwPlacementError(error, nodePath, placer, mutants, fileName) {
1811
- const location = `${path.relative(process.cwd(), fileName)}:${nodePath.node.loc?.start.line}:${nodePath.node.loc?.start.column}`;
1812
- const message = `${placer.name} could not place mutants with type(s): "${new Intl.ListFormat("en").format(mutants.map((mutant) => mutant.mutatorName))}"`;
1813
- const errorMessage = `${location} ${message}. Either remove this file from the list of files to be mutated, or exclude the mutator (using ${propertyPath()("mutator", "excludedMutations")}). Please report this issue at ${strykerReportBugUrl(message)}. Original error: ${error.stack}`;
1814
- let builtError = new Error(errorMessage);
1815
- try {
1816
- builtError = nodePath.buildCodeFrameError(errorMessage);
1817
- } catch {}
1818
- throw builtError;
1819
- }
1820
- //#endregion
1821
- //#region src/mutant-placers/index.ts
1822
- const allMutantPlacers = Object.freeze([
1823
- expressionMutantPlacer,
1824
- statementMutantPlacer,
1825
- switchCaseMutantPlacer
1826
1761
  ]);
1827
- //#endregion
1828
- //#region src/syntax/location.ts
1829
- /**
1830
- * Determines if a location (needle) is included in an other location (haystack)
1831
- * @param haystack The range to look in
1832
- * @param needle the range to search for
1833
- */
1834
- function locationIncluded(haystack, needle) {
1835
- const startIncluded = haystack.start.line < needle.start.line || haystack.start.line === needle.start.line && haystack.start.column <= needle.start.column;
1836
- const endIncluded = haystack.end.line > needle.end.line || haystack.end.line === needle.end.line && haystack.end.column >= needle.end.column;
1837
- return startIncluded && endIncluded;
1762
+ const ANGULAR_SIGNAL_IO_FUNCTIONS = Object.freeze([
1763
+ "input",
1764
+ "model",
1765
+ "output"
1766
+ ]);
1767
+ const ANGULAR_SIGNAL_QUERY_FUNCTIONS = Object.freeze([
1768
+ "contentChild",
1769
+ "contentChildren",
1770
+ "viewChild",
1771
+ "viewChildren"
1772
+ ]);
1773
+ const INPUT_MODEL_OUTPUT_CONFIG_MSG = "Angular signal based input, model and output functions configuration object cannot be mutated as that causes issues with the Angular compiler.";
1774
+ const SIGNAL_QUERY_OPTIONS_MSG = "Angular signal query options object cannot be mutated as that causes issues with the Angular compiler.";
1775
+ function shouldIgnore(path) {
1776
+ for (let current = path; current; current = current.parentPath) {
1777
+ if (isInputModelOrOutputConfigurationObject(current)) return Option.some(INPUT_MODEL_OUTPUT_CONFIG_MSG);
1778
+ if (isSignalQueryOptionsObject(current)) return Option.some(SIGNAL_QUERY_OPTIONS_MSG);
1779
+ }
1780
+ return Option.none();
1838
1781
  }
1839
- /**
1840
- * Determines if two locations overlap with each other
1841
- */
1842
- function locationOverlaps(a, b) {
1843
- const startIncluded = a.start.line < b.end.line || a.start.line === b.end.line && a.start.column <= b.end.column;
1844
- const endIncluded = a.end.line > b.start.line || a.end.line === b.start.line && a.end.column >= b.start.column;
1845
- return startIncluded && endIncluded;
1782
+ const angularIgnorer = { shouldIgnore };
1783
+ function isClassFieldLike(path) {
1784
+ return path.isClassProperty() || path.isClassPrivateProperty() || path.isClassAccessorProperty();
1846
1785
  }
1847
- //#endregion
1848
- //#region src/transformers/directive-bookkeeper.ts
1849
- const WILDCARD = "all";
1850
- const DEFAULT_REASON = "Ignored using a comment";
1851
- const strykerCommentDirectiveRegex = /^\s?Stryker (disable|restore)(?: (next-line))? ([a-zA-Z, ]+)(?::(.+)?)?/;
1852
- const rootRule = { kind: "Root" };
1853
- function findIgnoreReason(rule, mutatorName, line) {
1854
- const lower = mutatorName.toLowerCase();
1855
- let current = rule;
1856
- while (true) switch (current.kind) {
1857
- case "Root": return;
1858
- case "Ignore": {
1859
- const lineMatches = current.line === void 0 || current.line === line;
1860
- const mutatorMatches = current.mutatorNames.includes(lower) || current.mutatorNames.includes(WILDCARD);
1861
- if (lineMatches && mutatorMatches) return current.ignoreReason;
1862
- current = current.previous;
1863
- break;
1864
- }
1865
- case "Restore": {
1866
- const lineMatches = current.line === void 0 || current.line === line;
1867
- const mutatorMatches = current.mutatorNames.includes(lower) || current.mutatorNames.includes(WILDCARD);
1868
- if (lineMatches && mutatorMatches) return;
1869
- current = current.previous;
1870
- break;
1871
- }
1872
- }
1786
+ function isInputModelOrOutputConfigurationObject(path) {
1787
+ const parent = path.parentPath;
1788
+ const grandParent = parent?.parentPath;
1789
+ if (!path.isObjectExpression() || parent === null || parent === void 0 || !parent.isCallExpression() || grandParent === null || grandParent === void 0 || !grandParent.isClassProperty()) return false;
1790
+ const callExpression = parent;
1791
+ const objectExpression = path;
1792
+ const callNode = callExpression.node;
1793
+ if (!Predicate.hasProperty(callNode, "callee") || !Predicate.hasProperty(callNode, "arguments")) return false;
1794
+ const callee = callNode["callee"];
1795
+ const args = callNode["arguments"];
1796
+ if (!Array.isArray(args)) return false;
1797
+ const isRequiredSignalIOFunction = isMemberExpressionWithIdentifier(callee, ANGULAR_SIGNAL_IO_FUNCTIONS, "required");
1798
+ const isSignalIOFunction = isIdentifierIn(callee, ANGULAR_SIGNAL_IO_FUNCTIONS);
1799
+ const isOutput = isIdentifierWithName(callee, "output");
1800
+ if (isRequiredSignalIOFunction || isOutput) return args.length >= 1 && args[0] === objectExpression.node;
1801
+ if (isSignalIOFunction) return args.length >= 2 && args[1] === objectExpression.node;
1802
+ return false;
1873
1803
  }
1874
- function processStrykerDirectives(rule, node, allMutatorNames, originFileName) {
1875
- const leadingComments = node.leadingComments;
1876
- if (!leadingComments) return {
1877
- rule,
1878
- warnings: []
1879
- };
1880
- let current = rule;
1881
- const warnings = [];
1882
- for (const comment of leadingComments) {
1883
- const matchResult = strykerCommentDirectiveRegex.exec(comment.value);
1884
- if (!matchResult) continue;
1885
- const directiveType = matchResult[1];
1886
- const scope = matchResult[2];
1887
- const mutators = matchResult[3];
1888
- const optionalReason = matchResult[4];
1889
- if (directiveType === void 0 || mutators === void 0) throw new Error("Stryker directive without directive type or mutators");
1890
- let mutatorNames = mutators.split(",").map((mutator) => mutator.trim());
1891
- for (const mutator of mutatorNames) {
1892
- if (mutator === WILDCARD) continue;
1893
- if (!allMutatorNames.includes(mutator.toLowerCase())) {
1894
- const commentLoc = comment.loc;
1895
- if (commentLoc === void 0 || commentLoc === null || commentLoc.start === null || commentLoc.start === void 0) throw new Error("Comment without location");
1896
- warnings.push(`Unused 'Stryker ${scope ? directiveType + " " + scope : directiveType}' directive. Mutator with name '${mutator}' not found. Directive found at: ${originFileName}:${commentLoc.start.line}:${commentLoc.start.column}.`);
1897
- }
1898
- }
1899
- mutatorNames = mutatorNames.map((mutator) => mutator.toLowerCase());
1900
- const reason = (optionalReason ?? DEFAULT_REASON).trim();
1901
- current = applyDirective(current, directiveType, scope, mutatorNames, reason, node.loc);
1902
- }
1903
- return {
1904
- rule: current,
1905
- warnings
1906
- };
1804
+ function isSignalQueryOptionsObject(path) {
1805
+ const parent = path.parentPath;
1806
+ const grandParent = parent?.parentPath;
1807
+ if (!path.isObjectExpression() || parent === null || parent === void 0 || !parent.isCallExpression() || grandParent === null || grandParent === void 0 || !isClassFieldLike(grandParent)) return false;
1808
+ const callExpression = parent;
1809
+ const objectExpression = path;
1810
+ const callNode = callExpression.node;
1811
+ if (!Predicate.hasProperty(callNode, "callee") || !Predicate.hasProperty(callNode, "arguments")) return false;
1812
+ const callee = callNode["callee"];
1813
+ const args = callNode["arguments"];
1814
+ if (!Array.isArray(args)) return false;
1815
+ const isQueryFn = isIdentifierIn(callee, ANGULAR_SIGNAL_QUERY_FUNCTIONS);
1816
+ const isRequiredQueryFn = isMemberExpressionWithIdentifier(callee, ANGULAR_SIGNAL_QUERY_FUNCTIONS, "required");
1817
+ if (!isQueryFn && !isRequiredQueryFn) return false;
1818
+ return args.length >= 2 && args[1] === objectExpression.node;
1907
1819
  }
1908
- function applyDirective(rule, directiveType, scope, mutatorNames, reason, loc) {
1909
- switch (directiveType) {
1910
- case "disable": return applyDisable(rule, scope, mutatorNames, reason, loc);
1911
- case "restore": return applyRestore(rule, scope, mutatorNames, loc);
1912
- default: return rule;
1913
- }
1820
+ function isIdentifierWithName(node, name) {
1821
+ return Predicate.hasProperty(node, "type") && node["type"] === "Identifier" && Predicate.hasProperty(node, "name") && node["name"] === name;
1914
1822
  }
1915
- function applyDisable(rule, scope, mutatorNames, reason, loc) {
1916
- switch (scope) {
1917
- case "next-line": return {
1918
- kind: "Ignore",
1919
- mutatorNames,
1920
- line: getLine(loc),
1921
- ignoreReason: reason,
1922
- previous: rule
1923
- };
1924
- case void 0:
1925
- default: return {
1926
- kind: "Ignore",
1927
- mutatorNames,
1928
- line: void 0,
1929
- ignoreReason: reason,
1930
- previous: rule
1931
- };
1932
- }
1823
+ function isIdentifierIn(node, names) {
1824
+ return Predicate.hasProperty(node, "type") && node["type"] === "Identifier" && Predicate.hasProperty(node, "name") && typeof node["name"] === "string" && names.includes(node["name"]);
1933
1825
  }
1934
- function applyRestore(rule, scope, mutatorNames, loc) {
1935
- switch (scope) {
1936
- case "next-line": return {
1937
- kind: "Restore",
1938
- mutatorNames,
1939
- line: getLine(loc),
1940
- previous: rule
1941
- };
1942
- case void 0:
1943
- default: return {
1944
- kind: "Restore",
1945
- mutatorNames,
1946
- line: void 0,
1947
- previous: rule
1948
- };
1949
- }
1826
+ function isMemberExpressionWithIdentifier(node, objectNames, propertyName) {
1827
+ return Predicate.hasProperty(node, "type") && node["type"] === "MemberExpression" && Predicate.hasProperty(node, "object") && Predicate.hasProperty(node, "property") && isIdentifierIn(node["object"], objectNames) && isIdentifierWithName(node["property"], propertyName);
1950
1828
  }
1951
- function getLine(loc) {
1952
- if (loc === void 0 || loc === null || loc.start === null || loc.start === void 0) throw new Error("Babel node without location");
1953
- return loc.start.line;
1829
+ const strykerPlugins = [];
1830
+ const frameworkPluginsFileUrl = import.meta.url;
1831
+ /**
1832
+ * Returns syntax for the header if JS/TS files
1833
+ */
1834
+ const parsedInstrumentationHeader = babel.parse(`// @ts-nocheck
1835
+ var ${STRYKER_NAMESPACE_HELPER} = function(){
1836
+ var g = typeof globalThis === 'object' && globalThis && globalThis.Math === Math && globalThis || new Function("return this")();
1837
+ var ns = g.${INSTRUMENTER_CONSTANTS.NAMESPACE} || (g.${INSTRUMENTER_CONSTANTS.NAMESPACE} = {});
1838
+ if (ns.${INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT} === undefined && g.process && g.process.env && g.process.env.${INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT_ENV_VARIABLE}) {
1839
+ ns.${INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT} = g.process.env.${INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT_ENV_VARIABLE};
1840
+ }
1841
+ function retrieveNS(){
1842
+ return ns;
1843
+ }
1844
+ ${STRYKER_NAMESPACE_HELPER} = retrieveNS;
1845
+ return retrieveNS();
1846
+ };
1847
+ ${STRYKER_NAMESPACE_HELPER}();
1848
+
1849
+ var ${COVER_MUTANT_HELPER} = function() {
1850
+ var ns = ${STRYKER_NAMESPACE_HELPER}();
1851
+ var cov = ns.${INSTRUMENTER_CONSTANTS.MUTATION_COVERAGE_OBJECT} || (ns.${INSTRUMENTER_CONSTANTS.MUTATION_COVERAGE_OBJECT} = { static: {}, perTest: {} });
1852
+ function cover() {
1853
+ var c = cov.static;
1854
+ if (ns.${INSTRUMENTER_CONSTANTS.CURRENT_TEST_ID}) {
1855
+ c = cov.perTest[ns.${INSTRUMENTER_CONSTANTS.CURRENT_TEST_ID}] = cov.perTest[ns.${INSTRUMENTER_CONSTANTS.CURRENT_TEST_ID}] || {};
1856
+ }
1857
+ var a = arguments;
1858
+ for(var i=0; i < a.length; i++){
1859
+ c[a[i]] = (c[a[i]] || 0) + 1;
1860
+ }
1861
+ }
1862
+ ${COVER_MUTANT_HELPER} = cover;
1863
+ cover.apply(null, arguments);
1864
+ };
1865
+ var ${IS_MUTANT_ACTIVE_HELPER} = function(id) {
1866
+ var ns = ${STRYKER_NAMESPACE_HELPER}();
1867
+ function isActive(id) {
1868
+ if (ns.${INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT} === id) {
1869
+ if (ns.${INSTRUMENTER_CONSTANTS.HIT_COUNT} !== void 0 && ++ns.${INSTRUMENTER_CONSTANTS.HIT_COUNT} > ns.${INSTRUMENTER_CONSTANTS.HIT_LIMIT}) {
1870
+ throw new Error('Stryker: Hit count limit reached (' + ns.${INSTRUMENTER_CONSTANTS.HIT_COUNT} + ')');
1871
+ }
1872
+ return true;
1873
+ }
1874
+ return false;
1875
+ }
1876
+ ${IS_MUTANT_ACTIVE_HELPER} = isActive;
1877
+ return isActive(id);
1878
+ }`, {
1879
+ configFile: false,
1880
+ browserslistConfigFile: false,
1881
+ env: { targets: {} }
1882
+ });
1883
+ if (!babelTypes$1.isFile(parsedInstrumentationHeader)) throw new Error("Instrumentation header parsed as non-File");
1884
+ const instrumentationBabelHeader = parsedInstrumentationHeader.program.body;
1885
+ deepFreeze(instrumentationBabelHeader);
1886
+ function placeHeaderIfNeeded(mutantCollector, originFileName, options, root) {
1887
+ if (hasPlacedMutants(mutantCollector, originFileName) && options.noHeader !== true) placeHeader(root);
1954
1888
  }
1955
- //#endregion
1956
- //#region src/transformers/ignorer-bookkeeper.ts
1957
- function createIgnorerBookkeeper(ignorers) {
1958
- return {
1959
- ignorers,
1960
- activeIgnored: void 0
1961
- };
1889
+ function placeHeader(root) {
1890
+ let header = instrumentationBabelHeader;
1891
+ const leadingComments = root.program.body[0]?.leadingComments;
1892
+ if (Array.isArray(leadingComments)) {
1893
+ const firstHeader = instrumentationBabelHeader[0];
1894
+ if (firstHeader === void 0) throw new Error("Instrumentation header is empty");
1895
+ const cloned = babelTypes$1.cloneNode(firstHeader, true, false);
1896
+ cloned.leadingComments = leadingComments;
1897
+ header = [cloned, ...instrumentationBabelHeader.slice(1)];
1898
+ }
1899
+ root.program.body.unshift(...header);
1962
1900
  }
1963
- function currentIgnoreMessage(bookkeeper) {
1964
- return bookkeeper.activeIgnored?.message;
1965
- }
1966
- function enterNode(bookkeeper, path) {
1967
- if (bookkeeper.activeIgnored !== void 0) return bookkeeper;
1968
- const view = toIgnorerPath(path);
1969
- for (const ignorer of bookkeeper.ignorers) {
1970
- const result = ignorer.shouldIgnore(view);
1971
- if (Option.isSome(result)) return {
1972
- ...bookkeeper,
1973
- activeIgnored: {
1974
- node: path.node,
1975
- message: result.value
1901
+ function deepFreeze(value) {
1902
+ if (value !== null && typeof value === "object") {
1903
+ if (Array.isArray(value)) {
1904
+ for (const item of value) deepFreeze(item);
1905
+ return Object.freeze(value);
1906
+ }
1907
+ if (value instanceof RegExp) return Object.freeze(value);
1908
+ if (value instanceof Map) {
1909
+ for (const [k, v] of value.entries()) {
1910
+ deepFreeze(k);
1911
+ deepFreeze(v);
1976
1912
  }
1977
- };
1913
+ return Object.freeze(value);
1914
+ }
1915
+ if (value instanceof Set) {
1916
+ for (const v of value.values()) deepFreeze(v);
1917
+ return Object.freeze(value);
1918
+ }
1919
+ for (const v of Object.values(value)) deepFreeze(v);
1920
+ return Object.freeze(value);
1978
1921
  }
1979
- return bookkeeper;
1922
+ return value;
1980
1923
  }
1981
- function leaveNode(bookkeeper, path) {
1982
- if (bookkeeper.activeIgnored?.node === path.node) return {
1983
- ...bookkeeper,
1984
- activeIgnored: void 0
1924
+ function transform(ast, mutantCollector, transformerContext) {
1925
+ const context = {
1926
+ ...transformerContext,
1927
+ transform
1985
1928
  };
1986
- return bookkeeper;
1929
+ switch (ast.format) {
1930
+ case "html": return transformHtml(ast, mutantCollector, context);
1931
+ case "js":
1932
+ case "ts":
1933
+ case "tsx": return transformBabel(ast, mutantCollector, context);
1934
+ case "svelte": return transformSvelte(ast, mutantCollector, context);
1935
+ }
1987
1936
  }
1988
- function toIgnorerPath(path) {
1989
- return {
1990
- node: path.node,
1991
- parentPath: path.parentPath ? toIgnorerPath(path.parentPath) : null,
1992
- isObjectExpression() {
1993
- return path.isObjectExpression();
1994
- },
1995
- isCallExpression() {
1996
- return path.isCallExpression() || path.isOptionalCallExpression();
1997
- },
1998
- isClassProperty() {
1999
- return path.isClassProperty();
2000
- },
2001
- isClassPrivateProperty() {
2002
- return path.isClassPrivateProperty();
2003
- },
2004
- isClassAccessorProperty() {
2005
- return path.isClassAccessorProperty();
1937
+ const transformHtml = ({ root }, mutantCollector, context) => {
1938
+ const warnings = [];
1939
+ root.scripts.forEach((ast) => {
1940
+ warnings.push(...context.transform(ast, mutantCollector, context));
1941
+ });
1942
+ return warnings;
1943
+ };
1944
+ const moduleScript = `<script context="module">
1945
+ \n<\/script>\n`;
1946
+ const transformSvelte = (svelte, mutantCollector, context) => {
1947
+ const warnings = [];
1948
+ const { root, originFileName } = svelte;
1949
+ [root.moduleScript, ...root.additionalScripts].filter(Predicate.isNotNullish).forEach((script) => {
1950
+ warnings.push(...context.transform(script.ast, mutantCollector, {
1951
+ ...context,
1952
+ options: {
1953
+ ...context.options,
1954
+ noHeader: true
1955
+ }
1956
+ }));
1957
+ });
1958
+ if (hasPlacedMutants(mutantCollector, originFileName)) {
1959
+ if (!root.moduleScript) {
1960
+ root.moduleScript = {
1961
+ ast: {
1962
+ format: "js",
1963
+ root: babelTypes$1.file(babelTypes$1.program([])),
1964
+ rawContent: "",
1965
+ originFileName
1966
+ },
1967
+ range: {
1968
+ start: 26,
1969
+ end: 26
1970
+ },
1971
+ isExpression: false
1972
+ };
1973
+ svelte.rawContent = `${moduleScript}${svelte.rawContent}`;
1974
+ svelte.root.additionalScripts.forEach((script) => {
1975
+ script.range.start += moduleScript.length;
1976
+ script.range.end += moduleScript.length;
1977
+ });
2006
1978
  }
2007
- };
2008
- }
2009
- //#endregion
2010
- //#region src/transformers/babel-transformer.ts
2011
- const { traverse } = babel;
1979
+ placeHeader(root.moduleScript.ast.root);
1980
+ }
1981
+ return warnings;
1982
+ };
1983
+ const { traverse: traverse$1 } = babel;
2012
1984
  const transformBabel = ({ root, originFileName, rawContent, offset }, mutantCollector, { options, mutateDescription }, mutators = allMutators, mutantPlacers = allMutantPlacers) => {
2013
1985
  const file = createBabelFile(originFileName, rawContent, root);
2014
1986
  const placementMap = /* @__PURE__ */ new Map();
2015
1987
  let directiveRule = rootRule;
2016
1988
  const mutatorEntries = Object.entries(mutators);
2017
1989
  const allMutatorNames = mutatorEntries.map(([name]) => name.toLowerCase());
2018
- let ignorerState = createIgnorerBookkeeper(options.ignorers);
2019
1990
  const warnings = [];
2020
- traverse(file.ast, {
1991
+ traverse$1(file.ast, {
2021
1992
  enter(path) {
2022
1993
  const result = processStrykerDirectives(directiveRule, path.node, allMutatorNames, originFileName);
2023
1994
  directiveRule = result.rule;
2024
1995
  warnings.push(...result.warnings);
2025
1996
  if (shouldSkip(path)) path.skip();
2026
1997
  else {
2027
- ignorerState = enterNode(ignorerState, path);
2028
1998
  addToPlacementMapIfPossible(path);
2029
1999
  if (shouldMutate(path)) {
2030
2000
  const mutantsToPlace = collectMutants(path);
@@ -2042,7 +2012,6 @@ const transformBabel = ({ root, originFileName, rawContent, offset }, mutantColl
2042
2012
  },
2043
2013
  exit(path) {
2044
2014
  placeMutantsIfNeeded(path);
2045
- ignorerState = leaveNode(ignorerState, path);
2046
2015
  }
2047
2016
  });
2048
2017
  placeHeaderIfNeeded(mutantCollector, originFileName, options, root);
@@ -2055,10 +2024,14 @@ const transformBabel = ({ root, originFileName, rawContent, offset }, mutantColl
2055
2024
  });
2056
2025
  }
2057
2026
  function shouldSkip(path) {
2058
- return isTypeNode(path) || isImportDeclaration(path) || path.isDecorator() || !mutateDescription || Array.isArray(mutateDescription) && mutateDescription.every((range) => !locationOverlaps(range, getNodeLocation(path.node)));
2027
+ return isTypeNode(path) || isImportDeclaration(path) || path.isDecorator() || mutateDescription === false || Array.isArray(mutateDescription) && mutateDescription.every((range) => {
2028
+ return !locationOverlaps(range, getNodeLocation(path.node));
2029
+ });
2059
2030
  }
2060
2031
  function shouldMutate(path) {
2061
- return mutateDescription === true || Array.isArray(mutateDescription) && mutateDescription.some((range) => locationIncluded(range, getNodeLocation(path.node)));
2032
+ return mutateDescription === true || Array.isArray(mutateDescription) && mutateDescription.some((range) => {
2033
+ return locationIncluded(range, getNodeLocation(path.node));
2034
+ });
2062
2035
  }
2063
2036
  function placeMutantsIfNeeded(path) {
2064
2037
  const mutantsPlacement = placementMap.get(path.node);
@@ -2069,18 +2042,26 @@ const transformBabel = ({ root, originFileName, rawContent, offset }, mutantColl
2069
2042
  throwPlacementError(toError(error), path, mutantsPlacement.placer, [...mutantsPlacement.appliedMutants.keys()], originFileName);
2070
2043
  }
2071
2044
  }
2045
+ function ignoreMessageFor(path) {
2046
+ const view = toIgnorerPath(path);
2047
+ for (const ignorer of options.ignorers) {
2048
+ const result = ignorer.shouldIgnore(view);
2049
+ if (Option.isSome(result)) return result.value;
2050
+ }
2051
+ }
2072
2052
  function collectMutants(path) {
2073
- return [...mutate(path)].map((mutable) => collect(mutantCollector, originFileName, path.node, mutable, offset)).filter((mutant) => !mutant.ignoreReason);
2053
+ return [...mutate(path)].map((mutable) => collect(mutantCollector, originFileName, path.node, mutable, offset)).filter((mutant) => mutant.ignoreReason === void 0);
2074
2054
  }
2075
2055
  function* mutate(path) {
2076
2056
  const context = toMutatorContext(path);
2077
2057
  for (const [mutatorName, mutate] of mutatorEntries) for (const replacement of mutate(path.node, context)) {
2078
- const ignoreReason = findIgnoreReason(directiveRule, mutatorName, getNodeLocation(path.node).start.line) ?? findExcludedMutatorIgnoreReason(mutatorName) ?? currentIgnoreMessage(ignorerState);
2079
- yield {
2058
+ const ignoreReason = findIgnoreReason(directiveRule, mutatorName, getNodeLocation(path.node).start.line) ?? findExcludedMutatorIgnoreReason(mutatorName) ?? ignoreMessageFor(path);
2059
+ const mutableEntry = {
2080
2060
  replacement,
2081
- mutatorName,
2082
- ...ignoreReason === void 0 ? {} : { ignoreReason }
2061
+ mutatorName
2083
2062
  };
2063
+ if (ignoreReason !== void 0) mutableEntry.ignoreReason = ignoreReason;
2064
+ yield mutableEntry;
2084
2065
  }
2085
2066
  function findExcludedMutatorIgnoreReason(mutatorName) {
2086
2067
  if (options.excludedMutations.includes(mutatorName)) return `Ignored because of excluded mutation "${mutatorName}"`;
@@ -2091,7 +2072,7 @@ const transformBabel = ({ root, originFileName, rawContent, offset }, mutantColl
2091
2072
  function toMutatorContext(path) {
2092
2073
  const ancestors = [];
2093
2074
  let current = path.parentPath;
2094
- while (current !== null && current !== void 0) {
2075
+ while (current !== null) {
2095
2076
  ancestors.push(current.node);
2096
2077
  current = current.parentPath;
2097
2078
  }
@@ -2103,91 +2084,110 @@ function toMutatorContext(path) {
2103
2084
  }
2104
2085
  function getNodeLocation(node) {
2105
2086
  const loc = node.loc;
2106
- if (loc === null || loc === void 0 || loc.start === null || loc.start === void 0 || loc.end === null || loc.end === void 0) throw new Error("Babel node without location");
2107
- return loc;
2087
+ if (loc == null) throw new Error("Babel node without location");
2088
+ return {
2089
+ start: {
2090
+ line: loc.start.line,
2091
+ column: loc.start.column
2092
+ },
2093
+ end: {
2094
+ line: loc.end.line,
2095
+ column: loc.end.column
2096
+ }
2097
+ };
2108
2098
  }
2109
2099
  function toError(value) {
2110
2100
  if (value instanceof Error) return value;
2111
2101
  return new Error("Unexpected error", { cause: value });
2112
2102
  }
2113
2103
  //#endregion
2114
- //#region src/transformers/html-transformer.ts
2115
- const transformHtml = ({ root }, mutantCollector, context) => {
2116
- const warnings = [];
2117
- root.scripts.forEach((ast) => {
2118
- warnings.push(...context.transform(ast, mutantCollector, context));
2119
- });
2120
- return warnings;
2121
- };
2122
- //#endregion
2123
- //#region src/transformers/svelte-transformer.ts
2124
- const moduleScript = `<script context="module">
2125
- \n<\/script>\n`;
2126
- const transformSvelte = (svelte, mutantCollector, context) => {
2127
- const warnings = [];
2128
- const { root, originFileName } = svelte;
2129
- [root.moduleScript, ...root.additionalScripts].filter(Predicate.isNotNullish).forEach((script) => {
2130
- warnings.push(...context.transform(script.ast, mutantCollector, {
2131
- ...context,
2132
- options: {
2133
- ...context.options,
2134
- noHeader: true
2135
- }
2136
- }));
2104
+ //#region src/Instrument.ts
2105
+ /**
2106
+ * Instrument — the instrument capability: Cell description, file/mutant types and the instrument entry point.
2107
+ */
2108
+ const { traverse, types: babelTypes } = babel;
2109
+ function resolveGenerate() {
2110
+ if (typeof generator === "function") return generator;
2111
+ return generator.default;
2112
+ }
2113
+ resolveGenerate();
2114
+ function toApiMutant(mutant) {
2115
+ const loc = mutant.original.loc;
2116
+ if (loc === void 0 || loc === null) throw new Error("Babel node without a source location");
2117
+ const base = {
2118
+ fileName: mutant.fileName,
2119
+ id: mutant.id,
2120
+ location: toApiLocation(loc, mutant.offset),
2121
+ mutatorName: mutant.mutatorName,
2122
+ replacement: mutant.replacementCode
2123
+ };
2124
+ if (mutant.ignoreReason === void 0) return Mutant.make(base);
2125
+ return Mutant.make({
2126
+ ...base,
2127
+ statusReason: mutant.ignoreReason,
2128
+ status: "Ignored"
2137
2129
  });
2138
- if (hasPlacedMutants(mutantCollector, originFileName)) {
2139
- if (!root.moduleScript) {
2140
- root.moduleScript = {
2141
- ast: {
2142
- format: "js",
2143
- root: types.file(types.program([])),
2144
- rawContent: "",
2145
- originFileName
2146
- },
2147
- range: {
2148
- start: 26,
2149
- end: 26
2150
- },
2151
- isExpression: false
2152
- };
2153
- svelte.rawContent = `${moduleScript}${svelte.rawContent}`;
2154
- svelte.root.additionalScripts.forEach((script) => {
2155
- script.range.start += moduleScript.length;
2156
- script.range.end += moduleScript.length;
2157
- });
2158
- }
2159
- placeHeader(root.moduleScript.ast.root);
2160
- }
2161
- return warnings;
2162
- };
2163
- //#endregion
2164
- //#region src/transformers/transformer.ts
2165
- function transform(ast, mutantCollector, transformerContext) {
2166
- const context = {
2167
- ...transformerContext,
2168
- transform
2130
+ }
2131
+ function toApiLocation(source, offset) {
2132
+ return {
2133
+ start: toPosition(source.start, offset),
2134
+ end: toPosition(source.end, offset)
2135
+ };
2136
+ }
2137
+ function toPosition(source, offset) {
2138
+ let columnOffset = 0;
2139
+ if (source.line === 1) columnOffset = offset.column;
2140
+ return {
2141
+ column: source.column + columnOffset,
2142
+ line: source.line + offset.line - 1
2143
+ };
2144
+ }
2145
+ const commentDirectiveRegEx = /^(\s*)@(ts-[a-z-]+).*$/;
2146
+ const tsDirectiveLikeRegEx = /@(ts-[a-z-]+)/;
2147
+ const startingCommentRegex = /(^\s*\/\*.*?\*\/)/gs;
2148
+ async function disableTypeChecks(file, options) {
2149
+ const format = getFormat(file.name);
2150
+ if (!format) return file;
2151
+ if (isJSFileWithoutTSDirectives(file, format)) return {
2152
+ ...file,
2153
+ content: prefixWithNoCheck(file.content)
2169
2154
  };
2155
+ const ast = await createParser(options)(file.content, file.name);
2170
2156
  switch (ast.format) {
2171
- case "html": return transformHtml(ast, mutantCollector, context);
2172
2157
  case "js":
2173
2158
  case "ts":
2174
- case "tsx": return transformBabel(ast, mutantCollector, context);
2175
- case "svelte": return transformSvelte(ast, mutantCollector, context);
2159
+ case "tsx": return {
2160
+ ...file,
2161
+ content: disableTypeCheckingInBabelAst(ast)
2162
+ };
2163
+ case "html": return {
2164
+ ...file,
2165
+ content: disableTypeCheckingInHtml(ast)
2166
+ };
2167
+ case "svelte": return {
2168
+ ...file,
2169
+ content: disableTypeCheckingInSvelte(ast)
2170
+ };
2176
2171
  }
2177
2172
  }
2178
- //#endregion
2179
- //#region src/instrument-error.schema.ts
2180
- var InstrumentError = class extends S.TaggedError("@systemfsoftware/stryker-js-instrumenter/InstrumentError")("InstrumentError", {
2181
- message: S.String,
2182
- cause: S.Defect()
2183
- }) {
2184
- get message() {
2185
- if (this.message.length === 0) return "Instrumenter failure";
2186
- return this.message;
2173
+ function isJSFileWithoutTSDirectives(file, format) {
2174
+ return (format === "ts" || format === "js") && !tsDirectiveLikeRegEx.test(file.content);
2175
+ }
2176
+ function disableTypeCheckingInBabelAst(ast) {
2177
+ return prefixWithNoCheck(removeTSDirectives(ast.rawContent, ast.root.comments));
2178
+ }
2179
+ function prefixWithNoCheck(code) {
2180
+ if (code.startsWith("#")) {
2181
+ const newLineIndex = code.indexOf("\n");
2182
+ if (newLineIndex > 0) return `${code.substring(0, newLineIndex)}\n// @ts-nocheck\n${code.substring(newLineIndex + 1)}`;
2183
+ else return code;
2184
+ } else {
2185
+ startingCommentRegex.lastIndex = 0;
2186
+ const leadingComment = startingCommentRegex.exec(code)?.[1];
2187
+ if (leadingComment === void 0) return `// @ts-nocheck\n${code}`;
2188
+ return `${leadingComment.concat("\n")}// @ts-nocheck\n${code.substring(leadingComment.length)}`;
2187
2189
  }
2188
- };
2189
- //#endregion
2190
- //#region src/printers/html-printer.ts
2190
+ }
2191
2191
  function getScriptStart(script) {
2192
2192
  const start = script.root.start;
2193
2193
  if (start === void 0 || start === null) throw new Error("Script AST root without start");
@@ -2198,75 +2198,99 @@ function getScriptEnd(script) {
2198
2198
  if (end === void 0 || end === null) throw new Error("Script AST root without end");
2199
2199
  return end;
2200
2200
  }
2201
- const print$4 = (ast, context) => {
2201
+ function disableTypeCheckingInHtml(ast) {
2202
2202
  const sortedScripts = [...ast.root.scripts].sort((a, b) => getScriptStart(a) - getScriptStart(b));
2203
2203
  let currentIndex = 0;
2204
2204
  let html = "";
2205
2205
  for (const script of sortedScripts) {
2206
2206
  html += ast.rawContent.substring(currentIndex, getScriptStart(script));
2207
2207
  html += "\n";
2208
- html += context.print(script, context);
2208
+ html += prefixWithNoCheck(removeTSDirectives(script.rawContent, script.root.comments));
2209
2209
  html += "\n";
2210
2210
  currentIndex = getScriptEnd(script);
2211
2211
  }
2212
- html += ast.rawContent.substr(currentIndex);
2212
+ html += ast.rawContent.substring(currentIndex);
2213
2213
  return html;
2214
- };
2215
- //#endregion
2216
- //#region src/printers/js-printer.ts
2217
- const print$3 = (file) => {
2218
- return generate(file.root, { sourceMaps: false }).code;
2219
- };
2220
- //#endregion
2221
- //#region src/printers/svelte-printer.ts
2222
- const print$2 = ({ root, rawContent }, context) => {
2214
+ }
2215
+ function disableTypeCheckingInSvelte(ast) {
2216
+ const sortedScripts = [ast.root.moduleScript, ...ast.root.additionalScripts].filter(Predicate.isNotNullish).sort((a, b) => a.range.start - b.range.start);
2223
2217
  let currentIndex = 0;
2224
- let outputText = "";
2225
- const sortedScripts = [root.moduleScript, ...root.additionalScripts].filter(Predicate.isNotNullish).sort((a, b) => a.range.start - b.range.start);
2226
- for (const script of sortedScripts) if (script.isExpression) {
2227
- const codeWithoutSemicolon = context.print(script.ast, context).slice(0, -1);
2228
- outputText += rawContent.substring(currentIndex, script.range.start) + codeWithoutSemicolon;
2229
- currentIndex = script.range.end;
2230
- } else {
2231
- outputText += rawContent.substring(currentIndex, script.range.start);
2232
- outputText += "\n";
2233
- outputText += context.print(script.ast, context);
2234
- outputText += "\n";
2218
+ let html = "";
2219
+ for (const script of sortedScripts) {
2220
+ html += ast.rawContent.substring(currentIndex, script.range.start);
2221
+ html += "\n";
2222
+ html += prefixWithNoCheck(removeTSDirectives(script.ast.rawContent, script.ast.root.comments));
2223
+ html += "\n";
2235
2224
  currentIndex = script.range.end;
2236
2225
  }
2237
- outputText += rawContent.substring(currentIndex);
2238
- return outputText;
2239
- };
2240
- //#endregion
2241
- //#region src/printers/ts-printer.ts
2242
- const print$1 = (file) => {
2243
- return generate(file.root, {
2244
- decoratorsBeforeExport: true,
2245
- sourceMaps: false
2246
- }).code;
2247
- };
2248
- //#endregion
2249
- //#region src/printers/index.ts
2250
- function print(file) {
2251
- const context = { print };
2252
- switch (file.format) {
2253
- case "js": return print$3(file, context);
2254
- case "ts": return print$1(file, context);
2255
- case "tsx": return print$1(file, context);
2256
- case "html": return print$4(file, context);
2257
- case "svelte": return print$2(file, context);
2226
+ html += ast.rawContent.substring(currentIndex);
2227
+ return html;
2228
+ }
2229
+ function removeTSDirectives(text, comments) {
2230
+ if (comments === null || comments === void 0) return text;
2231
+ const directiveRanges = comments.map(tryParseTSDirective).filter(Predicate.isNotNullish).sort((a, b) => a.startPos - b.startPos);
2232
+ if (directiveRanges.length === 0) return text;
2233
+ let currentIndex = 0;
2234
+ let pruned = "";
2235
+ for (const directiveRange of directiveRanges) {
2236
+ pruned += text.substring(currentIndex, directiveRange.startPos);
2237
+ currentIndex = directiveRange.endPos;
2258
2238
  }
2239
+ pruned += text.substring(currentIndex);
2240
+ return pruned;
2259
2241
  }
2260
- //#endregion
2261
- //#region src/instrumenter.ts
2262
- const instrument = Effect.fn("Instrumenter.instrument")(function* (files, options) {
2263
- yield* Effect.annotateCurrentSpan({ "instrumenter.file.count": files.length });
2264
- yield* Effect.logDebug(`Instrumenting ${files.length} source files with mutants`);
2265
- const collector = createMutantCollector();
2266
- const outFiles = [];
2267
- let mutantCount = 0;
2242
+ function tryParseTSDirective(comment) {
2243
+ const match = commentDirectiveRegEx.exec(comment.value);
2244
+ if (match !== null) {
2245
+ const start = comment.start;
2246
+ if (start == null) throw new Error("Comment without start");
2247
+ const directivePrefix = match[1];
2248
+ if (directivePrefix === void 0) throw new Error("TS directive match without prefix");
2249
+ const directiveName = match[2];
2250
+ if (directiveName === void 0) throw new Error("TS directive match without directive name");
2251
+ const directiveStartPos = start + directivePrefix.length + 2;
2252
+ return {
2253
+ startPos: directiveStartPos,
2254
+ endPos: directiveStartPos + directiveName.length + 1
2255
+ };
2256
+ }
2257
+ }
2258
+ function toBabelLineNumber(range) {
2259
+ if (typeof range === "boolean") return range;
2260
+ return range.map(({ start, end }) => ({
2261
+ start: {
2262
+ column: start.column,
2263
+ line: start.line + 1
2264
+ },
2265
+ end: {
2266
+ column: end.column,
2267
+ line: end.line + 1
2268
+ }
2269
+ }));
2270
+ }
2271
+ function isIgnorerService(value) {
2272
+ return typeof value === "object" && value !== null && "shouldIgnore" in value && typeof value.shouldIgnore === "function";
2273
+ }
2274
+ function toTransformerOptions(options) {
2275
+ const base = {
2276
+ excludedMutations: [...options.excludedMutations],
2277
+ ignorers: options.ignorers.filter(isIgnorerService)
2278
+ };
2279
+ if (options.noHeader !== void 0) return {
2280
+ ...base,
2281
+ noHeader: options.noHeader
2282
+ };
2283
+ return base;
2284
+ }
2285
+ function isAst(value) {
2286
+ return typeof value === "object" && value !== null && "format" in value && "root" in value;
2287
+ }
2288
+ const instrumentDescription = pipe(Cell.read((command) => Effect.gen(function* () {
2289
+ const files = command.files;
2290
+ const options = command.options;
2268
2291
  const parse = createParser(options);
2269
- for (const { name, mutate, content } of files) {
2292
+ const asts = [];
2293
+ for (const { name, content } of files) {
2270
2294
  const ast = yield* Effect.tryPromise({
2271
2295
  try: () => parse(content, name),
2272
2296
  catch: (cause) => new InstrumentError({
@@ -2274,54 +2298,85 @@ const instrument = Effect.fn("Instrumenter.instrument")(function* (files, option
2274
2298
  cause
2275
2299
  })
2276
2300
  });
2277
- const warnings = yield* Effect.try({
2301
+ asts.push(ast);
2302
+ }
2303
+ const collector = createMutantCollector();
2304
+ const workAsts = asts.filter(isAst);
2305
+ for (let i = 0; i < workAsts.length; i++) {
2306
+ const ast = workAsts[i];
2307
+ const file = files[i];
2308
+ if (ast === void 0 || file === void 0) continue;
2309
+ yield* Effect.try({
2278
2310
  try: () => transform(ast, collector, {
2279
- options,
2280
- mutateDescription: toBabelLineNumber(mutate)
2311
+ options: toTransformerOptions(options),
2312
+ mutateDescription: toBabelLineNumber(file.mutate)
2281
2313
  }),
2282
2314
  catch: (cause) => new InstrumentError({
2283
- message: `Failed to transform ${name}`,
2315
+ message: `Failed to transform ${file.name}`,
2284
2316
  cause
2285
2317
  })
2286
2318
  });
2287
- for (const warning of warnings) yield* Effect.logWarning(warning);
2288
- const mutatedContent = yield* Effect.try({
2289
- try: () => print(ast),
2319
+ }
2320
+ return {
2321
+ files,
2322
+ options,
2323
+ asts,
2324
+ mutants: yield* Effect.try({
2325
+ try: () => collector.map(toApiMutant),
2290
2326
  catch: (cause) => new InstrumentError({
2291
- message: `Failed to print ${name}`,
2327
+ message: "Failed to instrument",
2292
2328
  cause
2293
2329
  })
2294
- });
2295
- outFiles.push({
2296
- name,
2297
- mutate,
2298
- content: mutatedContent
2299
- });
2300
- const nrOfMutantsInFile = collector.length - mutantCount;
2301
- mutantCount = collector.length;
2302
- yield* Effect.logDebug(`Instrumented ${name} (${nrOfMutantsInFile} mutant(s))`);
2303
- yield* Effect.annotateCurrentSpan({ "instrumenter.mutants.total": collector.length });
2304
- }
2305
- const mutants = collector.map(toApiMutant);
2306
- yield* Effect.logInfo(`Instrumented ${files.length} source file(s) with ${mutants.length} mutant(s)`);
2307
- yield* Effect.annotateCurrentSpan({ "instrumenter.mutants.placed": mutants.length });
2308
- return {
2309
- files: outFiles,
2310
- mutants
2330
+ })
2311
2331
  };
2312
- });
2313
- function toBabelLineNumber(range) {
2314
- if (typeof range === "boolean") return range;
2315
- else return range.map(({ start, end }) => ({
2316
- start: {
2317
- column: start.column,
2318
- line: start.line + 1
2332
+ })), Cell.decode((raw) => Result.succeed(InstrumentDecoded.make({
2333
+ files: raw.files,
2334
+ options: raw.options,
2335
+ asts: raw.asts,
2336
+ mutants: raw.mutants
2337
+ }))), Cell.decide(instrumentWorkflow), Cell.encode((outcome) => outcome), Cell.write((outcome) => Result.match(outcome, {
2338
+ onFailure: (error) => Effect.fail(error),
2339
+ onSuccess: (decision) => Effect.try({
2340
+ try: () => {
2341
+ const files = decision.files;
2342
+ const asts = decision.asts;
2343
+ const outFiles = [];
2344
+ const isAstValue = (value) => typeof value === "object" && value !== null && "format" in value && "root" in value;
2345
+ for (let i = 0; i < asts.length; i++) {
2346
+ const maybeAst = asts[i];
2347
+ const maybeFile = files[i];
2348
+ if (maybeAst === void 0 || maybeFile === void 0) continue;
2349
+ if (!isAstValue(maybeAst)) continue;
2350
+ const ast = maybeAst;
2351
+ const file = maybeFile;
2352
+ const mutatedContent = print(ast);
2353
+ outFiles.push({
2354
+ name: file.name,
2355
+ mutate: file.mutate,
2356
+ content: mutatedContent
2357
+ });
2358
+ }
2359
+ return InstrumentResult.make({
2360
+ files: outFiles,
2361
+ mutants: decision.mutants
2362
+ });
2319
2363
  },
2320
- end: {
2321
- column: end.column,
2322
- line: end.line + 1
2323
- }
2364
+ catch: (cause) => new InstrumentError({
2365
+ message: "Failed to print",
2366
+ cause
2367
+ })
2368
+ })
2369
+ })));
2370
+ const instrument = (files, options) => {
2371
+ const schemaFiles = files.map((file) => ({
2372
+ name: file.name,
2373
+ content: file.content,
2374
+ mutate: file.mutate
2324
2375
  }));
2325
- }
2376
+ return Cell.apply(instrumentDescription, InstrumentCommand.make({
2377
+ files: schemaFiles,
2378
+ options
2379
+ }));
2380
+ };
2326
2381
  //#endregion
2327
2382
  export { angularIgnorer, disableTypeChecks, frameworkPluginsFileUrl, instrument, strykerPlugins };