@systemfsoftware/stryker-js-instrumenter 0.1.0 → 1.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,27 +1,33 @@
1
- import { PluginKind, commonTokens, declareClassPlugin, tokens } from "@systemfsoftware/stryker-js-plugin-api/plugin";
1
+ import * as Predicate from "effect/Predicate";
2
+ import { INSTRUMENTER_CONSTANTS, propertyPath, strykerReportBugUrl } from "@systemfsoftware/stryker-js-plugin-api/core";
2
3
  import path from "path";
3
- import { INSTRUMENTER_CONSTANTS as ID } from "@systemfsoftware/stryker-js-plugin-api/core";
4
+ import * as S from "effect/Schema";
4
5
  import babel, { File, types } from "@babel/core";
5
- import { deepFreeze, notEmpty, propertyPath } from "@systemfsoftware/stryker-js-util";
6
- import { satisfies } from "semver";
7
- import generator from "@babel/generator";
8
- import * as weaponRegex from "weapon-regex";
9
- import "@systemfsoftware/stryker-js-plugin-api/ignore";
10
- import "@systemfsoftware/stryker-js-plugin-api/logging";
11
6
  import { createRequire } from "module";
12
- //#region src/instrumenter-tokens.ts
13
- const instrumenterTokens = Object.freeze({
14
- createParser: "instrumenterCreateParser",
15
- print: "instrumenterPrint",
16
- transform: "instrumenterTransform"
17
- });
18
- //#endregion
19
- //#region src/parsers/parse-error.ts
20
- var ParseError = class extends Error {
21
- constructor(message, fileName, location) {
22
- super(`Parse error in ${fileName} (${location.line}:${location.column}) ${message}`);
7
+ import * as Option from "effect/Option";
8
+ import "@systemfsoftware/stryker-js-plugin-api/ignore";
9
+ import generator from "@babel/generator";
10
+ import { RegExpParser, visitRegExpAST } from "@eslint-community/regexpp";
11
+ import * as Effect from "effect/Effect";
12
+ //#region src/parsers/parse-error.schema.ts
13
+ var ParseFailed = class extends S.TaggedError("@systemfsoftware/stryker-js-instrumenter/ParseFailed")("ParseFailed", {
14
+ fileName: S.String,
15
+ message: S.String,
16
+ location: S.Struct({
17
+ line: S.Finite,
18
+ column: S.Finite
19
+ }),
20
+ cause: S.Defect()
21
+ }) {
22
+ get message() {
23
+ return `Failed to parse ${this.fileName} at ${this.location.line}:${this.location.column}: ${this.message}`;
23
24
  }
24
25
  };
26
+ S.TaggedError("@systemfsoftware/stryker-js-instrumenter/ParserNotFound")("ParserNotFound", {
27
+ fileName: S.String,
28
+ extension: S.String,
29
+ cause: S.Defect()
30
+ });
25
31
  //#endregion
26
32
  //#region src/parsers/html-parser.ts
27
33
  const TSX_SCRIPT_TYPES = Object.freeze(["tsx", "text/tsx"]);
@@ -53,17 +59,43 @@ async function ngHtmlParser(text, fileName, parserContext) {
53
59
  });
54
60
  if (errors.length !== 0) {
55
61
  const firstError = errors[0];
56
- if (firstError === void 0) throw new Error("HTML parser reported errors but first error is missing");
57
- throw new ParseError(firstError.msg, fileName, toSourceLocation(firstError.span.start));
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
+ });
58
77
  }
59
78
  const scriptsAsPromised = [];
60
- ngParser.visitAll(new class extends ngParser.RecursiveVisitor {
61
- visitElement(el, context) {
79
+ const scriptCollector = {
80
+ visitElement: (el, context) => {
62
81
  const scriptFormat = getScriptType(el);
63
82
  if (scriptFormat) scriptsAsPromised.push(parseScript(el, scriptFormat));
64
- super.visitElement(el, context);
65
- }
66
- }(), rootNodes);
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);
67
99
  return { scripts: await Promise.all(scriptsAsPromised) };
68
100
  async function parseScript(el, scriptFormat) {
69
101
  const endSourceSpan = el.endSourceSpan;
@@ -110,7 +142,7 @@ function getScriptType(element) {
110
142
  }
111
143
  //#endregion
112
144
  //#region src/parsers/js-parser.ts
113
- const { types: types$15, parseAsync: parseAsync$1 } = babel;
145
+ const { types: types$20, parseAsync: parseAsync$1 } = babel;
114
146
  function isParserPlugin(value) {
115
147
  return typeof value === "string" || Array.isArray(value);
116
148
  }
@@ -157,7 +189,7 @@ function createParser$1({ plugins: pluginsOverride }) {
157
189
  sourceType: "module"
158
190
  });
159
191
  if (ast === null || ast === void 0) throw new Error(`Expected ${fileName} to contain a babel.types.file, but it yielded null`);
160
- if (types$15.isProgram(ast)) throw new Error(`Expected ${fileName} to contain a babel.types.file, but was a program`);
192
+ if (types$20.isProgram(ast)) throw new Error(`Expected ${fileName} to contain a babel.types.file, but was a program`);
161
193
  return {
162
194
  originFileName: fileName,
163
195
  rawContent: text,
@@ -167,66 +199,50 @@ function createParser$1({ plugins: pluginsOverride }) {
167
199
  };
168
200
  }
169
201
  //#endregion
170
- //#region src/util/position-converter.ts
171
- /**
172
- * A class that can convert a string offset back to line / column.
173
- * Grabbed from TypeScript code base
174
- * @see https://github.com/microsoft/TypeScript/blob/aa9b6953441b53f8b14072c047f0519b611150c4/src/compiler/scanner.ts#L503
175
- */
176
- var PositionConverter = class {
177
- text;
178
- _lineStarts;
179
- get lineStarts() {
180
- if (!this._lineStarts) this._lineStarts = this.computeLineStarts(this.text);
181
- return this._lineStarts;
182
- }
183
- constructor(text) {
184
- this.text = text;
185
- }
186
- positionFromOffset(offset) {
187
- const lineNumber = this.computeLineOfPosition(offset);
188
- const lineStart = this.lineStarts[lineNumber];
189
- if (lineStart === void 0) throw new Error("Line start not found for computed line number");
190
- return {
191
- line: lineNumber,
192
- column: offset - lineStart
193
- };
194
- }
195
- computeLineOfPosition(offset) {
196
- let lineNumber = binarySearch(this.lineStarts, offset);
197
- if (lineNumber < 0) {
198
- lineNumber = ~lineNumber - 1;
199
- if (lineNumber === -1) throw new Error("position cannot precede the beginning of the file");
200
- }
201
- return lineNumber;
202
- }
203
- computeLineStarts(text) {
204
- const result = [];
205
- let pos = 0;
206
- let lineStart = 0;
207
- while (pos < text.length) {
208
- const ch = text.charCodeAt(pos);
209
- pos++;
210
- switch (ch) {
211
- case 13:
212
- if (text.charCodeAt(pos) === 10) pos++;
213
- result.push(lineStart);
214
- lineStart = pos;
215
- break;
216
- case 10:
217
- result.push(lineStart);
218
- lineStart = pos;
219
- break;
220
- default: if (ch > 127 && isLineBreak(ch)) {
221
- result.push(lineStart);
222
- lineStart = pos;
223
- }
202
+ //#region src/syntax/position-converter.ts
203
+ function computeLineStarts(text) {
204
+ const result = [];
205
+ let pos = 0;
206
+ let lineStart = 0;
207
+ while (pos < text.length) {
208
+ const ch = text.charCodeAt(pos);
209
+ pos++;
210
+ switch (ch) {
211
+ case CharacterCodes.carriageReturn:
212
+ if (text.charCodeAt(pos) === CharacterCodes.lineFeed) pos++;
213
+ result.push(lineStart);
214
+ lineStart = pos;
215
+ break;
216
+ case CharacterCodes.lineFeed:
217
+ result.push(lineStart);
218
+ lineStart = pos;
219
+ break;
220
+ default: if (ch > CharacterCodes.maxAsciiCharacter && isLineBreak(ch)) {
221
+ result.push(lineStart);
222
+ lineStart = pos;
224
223
  }
225
224
  }
226
- result.push(lineStart);
227
- return result;
228
225
  }
229
- };
226
+ result.push(lineStart);
227
+ return result;
228
+ }
229
+ function positionFromOffset(lineStarts, offset) {
230
+ const lineNumber = computeLineOfPosition(lineStarts, offset);
231
+ const lineStart = lineStarts[lineNumber];
232
+ if (lineStart === void 0) throw new Error("Line start not found for computed line number");
233
+ return {
234
+ line: lineNumber,
235
+ column: offset - lineStart
236
+ };
237
+ }
238
+ function computeLineOfPosition(lineStarts, offset) {
239
+ let lineNumber = binarySearch(lineStarts, offset);
240
+ if (lineNumber < 0) {
241
+ lineNumber = ~lineNumber - 1;
242
+ if (lineNumber === -1) throw new Error("position cannot precede the beginning of the file");
243
+ }
244
+ return lineNumber;
245
+ }
230
246
  function binarySearch(array, value) {
231
247
  if (!array.length) return -1;
232
248
  let low = 0;
@@ -248,792 +264,643 @@ function binarySearch(array, value) {
248
264
  function compare(a, b) {
249
265
  return a < b ? -1 : a > b ? 1 : 0;
250
266
  }
267
+ const CharacterCodes = {
268
+ lineFeed: 10,
269
+ carriageReturn: 13,
270
+ maxAsciiCharacter: 127,
271
+ lineSeparator: 8232,
272
+ paragraphSeparator: 8233
273
+ };
251
274
  function isLineBreak(ch) {
252
- return ch === 10 || ch === 13 || ch === 8232 || ch === 8233;
275
+ return ch === CharacterCodes.lineFeed || ch === CharacterCodes.carriageReturn || ch === CharacterCodes.lineSeparator || ch === CharacterCodes.paragraphSeparator;
253
276
  }
254
277
  //#endregion
255
- //#region src/util/babel-generator.ts
256
- /**
257
- * `@babel/generator` is CommonJS. Under Node's own ESM interop a default import
258
- * of it is the module's `exports` object, so the code generator sits behind
259
- * `.default` — the shape upstream reaches for, because upstream ships one
260
- * emitted file per source file. This package ships a bundle, where the default
261
- * import is already the function and `.default` is `undefined`, which fails at
262
- * the first mutant with `generator is not a function` rather than at build
263
- * time. Resolving both shapes once keeps the printers and the mutant's
264
- * replacement code identical under either layout.
265
- */
266
- const generate = typeof generator === "function" ? generator : generator.default;
267
- //#endregion
268
- //#region src/mutant.ts
269
- const { traverse: traverse$2 } = babel;
270
- var Mutant$1 = class {
271
- id;
272
- fileName;
273
- original;
274
- offset;
275
- replacementCode;
276
- replacement;
277
- mutatorName;
278
- ignoreReason;
279
- constructor(id, fileName, original, specs, offset = {
280
- column: 0,
281
- line: 0
282
- }) {
283
- this.id = id;
284
- this.fileName = fileName;
285
- this.original = original;
286
- this.offset = offset;
287
- this.replacement = specs.replacement;
288
- this.mutatorName = specs.mutatorName;
289
- this.ignoreReason = specs.ignoreReason;
290
- this.replacementCode = generate(this.replacement).code;
291
- }
292
- toApiMutant() {
293
- const loc = this.original.loc;
294
- if (loc === void 0 || loc === null) throw new Error("Babel node without a source location");
295
- return {
296
- fileName: this.fileName,
297
- id: this.id,
298
- location: toApiLocation(loc, this.offset),
299
- mutatorName: this.mutatorName,
300
- replacement: this.replacementCode,
301
- ...this.ignoreReason === void 0 ? {} : { statusReason: this.ignoreReason },
302
- ...this.ignoreReason === void 0 ? {} : { status: "Ignored" }
303
- };
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)`;
304
286
  }
305
- /**
306
- * Applies the mutant in (a copy of) the AST, without changing provided AST.
307
- * Can the tree itself (in which case the replacement is returned),
308
- * or can be nested in the given tree.
309
- *
310
- * Returns a plain node rather than the argument's own type: whether the
311
- * replacement fits a given position is the placer's claim, and the placer
312
- * checks it with a Babel predicate. A generic return would have to assert it
313
- * here, where nothing can check it.
314
- * @param originalTree The original node, which will be treated as readonly
315
- */
316
- applied(originalTree) {
317
- if (originalTree === this.original) return this.replacement;
318
- else {
319
- const mutatedAst = deepCloneNode(originalTree);
320
- let applied = false;
321
- const { original, replacement } = this;
322
- traverse$2(mutatedAst, {
323
- noScope: true,
324
- enter(path) {
325
- if (eqNode(path.node, original)) {
326
- path.replaceWith(replacement);
327
- path.stop();
328
- applied = true;
329
- }
330
- }
331
- });
332
- if (!applied) throw new Error(`Could not apply mutant ${JSON.stringify(this.replacement)}.`);
333
- return mutatedAst;
334
- }
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}`;
335
294
  }
336
295
  };
337
- function toApiLocation(source, offset) {
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;
338
308
  return {
339
- start: toPosition(source.start, offset),
340
- end: toPosition(source.end, offset)
309
+ major,
310
+ minor
341
311
  };
342
312
  }
343
- function toPosition(source, offset) {
344
- return {
345
- column: source.column + (source.line === 1 ? offset.column : 0),
346
- line: source.line + offset.line - 1
347
- };
313
+ function isSupportedSvelteVersion(version) {
314
+ const parsed = parseVersion(version);
315
+ if (parsed === void 0) return false;
316
+ return parsed.major > 3 || parsed.major === 3 && parsed.minor >= 30;
348
317
  }
349
- //#endregion
350
- //#region src/mutators/arithmetic-operator-mutator.ts
351
- const arithmeticOperatorReplacements = Object.freeze({
352
- "+": "-",
353
- "-": "+",
354
- "*": "/",
355
- "/": "*",
356
- "%": "*"
357
- });
358
- const arithmeticOperatorMutator = {
359
- name: "ArithmeticOperator",
360
- *mutate(path) {
361
- if (path.isBinaryExpression() && isSupported$3(path.node.operator, path.node)) {
362
- const mutatedOperator = arithmeticOperatorReplacements[path.node.operator];
363
- const replacement = deepCloneNode(path.node);
364
- replacement.operator = mutatedOperator;
365
- yield replacement;
366
- }
367
- }
368
- };
369
- function isSupported$3(operator, node) {
370
- if (!Object.keys(arithmeticOperatorReplacements).includes(operator)) return false;
371
- const stringTypes = ["StringLiteral", "TemplateLiteral"];
372
- const leftType = node.left.type === "BinaryExpression" ? node.left.right.type : node.left.type;
373
- if (stringTypes.includes(node.right.type) || stringTypes.includes(leftType)) return false;
374
- return true;
318
+ function isSvelteV5OrLater(version) {
319
+ const parsed = parseVersion(version);
320
+ if (parsed === void 0) return false;
321
+ return parsed.major >= 5;
375
322
  }
376
- //#endregion
377
- //#region src/mutators/array-declaration-mutator.ts
378
- const { types: types$14 } = babel;
379
- const arrayDeclarationMutator = {
380
- name: "ArrayDeclaration",
381
- *mutate(path) {
382
- if (path.isArrayExpression()) yield path.node.elements.length ? types$14.arrayExpression() : types$14.arrayExpression([types$14.stringLiteral("Stryker was here")]);
383
- if ((path.isCallExpression() || path.isNewExpression()) && types$14.isIdentifier(path.node.callee) && path.node.callee.name === "Array") {
384
- const mutatedCallArgs = path.node.arguments.length ? [] : [types$14.arrayExpression()];
385
- yield types$14.isNewExpression(path.node) ? types$14.newExpression(deepCloneNode(path.node.callee), mutatedCallArgs) : types$14.callExpression(deepCloneNode(path.node.callee), mutatedCallArgs);
386
- }
387
- }
388
- };
389
- //#endregion
390
- //#region src/mutators/arrow-function-mutator.ts
391
- const { types: types$13 } = babel;
392
- const arrowFunctionMutator = {
393
- name: "ArrowFunction",
394
- *mutate(path) {
395
- if (path.isArrowFunctionExpression() && !types$13.isBlockStatement(path.node.body) && !(types$13.isIdentifier(path.node.body) && path.node.body.name === "undefined")) yield types$13.arrowFunctionExpression([], types$13.identifier("undefined"));
396
- }
397
- };
398
- //#endregion
399
- //#region src/mutators/assignment-operator-mutator.ts
400
- const assignmentOperatorReplacements = Object.freeze({
401
- "+=": "-=",
402
- "-=": "+=",
403
- "*=": "/=",
404
- "/=": "*=",
405
- "%=": "*=",
406
- "<<=": ">>=",
407
- ">>=": "<<=",
408
- "&=": "|=",
409
- "|=": "&=",
410
- "&&=": "||=",
411
- "||=": "&&=",
412
- "??=": "&&="
413
- });
414
- const stringTypes = Object.freeze(["StringLiteral", "TemplateLiteral"]);
415
- const stringAssignmentTypes = Object.freeze([
416
- "&&=",
417
- "||=",
418
- "??="
419
- ]);
420
- const assignmentOperatorMutator = {
421
- name: "AssignmentOperator",
422
- *mutate(path) {
423
- if (path.isAssignmentExpression() && isSupportedAssignmentOperator(path.node.operator) && isSupported$2(path.node)) {
424
- const mutatedOperator = assignmentOperatorReplacements[path.node.operator];
425
- const replacement = deepCloneNode(path.node);
426
- replacement.operator = mutatedOperator;
427
- yield replacement;
428
- }
429
- }
430
- };
431
- function isSupportedAssignmentOperator(operator) {
432
- return Object.keys(assignmentOperatorReplacements).includes(operator);
323
+ function isPlainRecord(value) {
324
+ return typeof value === "object" && value !== null;
433
325
  }
434
- function isSupported$2(node) {
435
- if (stringTypes.includes(node.right.type) && !stringAssignmentTypes.includes(node.operator)) return false;
436
- return true;
326
+ function isUnknownArray(value) {
327
+ return Array.isArray(value);
437
328
  }
438
- //#endregion
439
- //#region src/mutators/block-statement-mutator.ts
440
- const { types: types$12 } = babel;
441
- const blockStatementMutator = {
442
- name: "BlockStatement",
443
- *mutate(path) {
444
- if (path.isBlockStatement() && isValid(path)) yield types$12.blockStatement([]);
445
- }
446
- };
447
- function isValid(path) {
448
- return !isEmpty(path) && !isInvalidConstructorBody(path);
329
+ function isRangedProgram(value) {
330
+ return isPlainRecord(value) && typeof value["start"] === "number" && typeof value["end"] === "number";
449
331
  }
450
- function isEmpty(path) {
451
- return !path.node.body.length;
332
+ function isWalkFunction(value) {
333
+ return typeof value === "function";
452
334
  }
453
- /**
454
- * Checks to see if a statement is an invalid constructor body
455
- * @example
456
- * // Invalid!
457
- * class Foo extends Bar {
458
- * constructor(public baz: string) {
459
- * super(42);
460
- * }
461
- * }
462
- * @example
463
- * // Invalid!
464
- * class Foo extends Bar {
465
- * public baz = 'string';
466
- * constructor() {
467
- * super(42);
468
- * }
469
- * }
470
- * @see https://github.com/stryker-mutator/stryker-js/issues/2314
471
- * @see https://github.com/stryker-mutator/stryker-js/issues/2474
472
- */
473
- function isInvalidConstructorBody(blockStatement) {
474
- return Boolean(blockStatement.parentPath.isClassMethod() && blockStatement.parentPath.node.kind === "constructor" && (containsTSParameterProperties(blockStatement.parentPath) || containsInitializedClassProperties(blockStatement.parentPath)) && hasSuperExpression(blockStatement));
335
+ function isRangedBaseNode(value) {
336
+ return isPlainRecord(value) && typeof value["type"] === "string" && typeof value["start"] === "number" && typeof value["end"] === "number";
475
337
  }
476
- function containsTSParameterProperties(constructor) {
477
- return constructor.node.params.some((param) => types$12.isTSParameterProperty(param));
338
+ function isTemplateExpressionType(type) {
339
+ return type === "MustacheTag" || type === "RawMustacheTag" || type === "IfBlock" || type === "ConstTag" || type === "EachBlock" || type === "AwaitBlock" || type === "KeyBlock" || type === "EventHandler";
478
340
  }
479
- function containsInitializedClassProperties(constructor) {
480
- return constructor.parentPath.isClassBody() && constructor.parentPath.node.body.some((classMember) => types$12.isClassProperty(classMember) && classMember.value);
341
+ function tryGetScriptRangeFromElement(node) {
342
+ if (!isPlainRecord(node) || node["type"] !== "Element" || node["name"] !== "script") return;
343
+ const children = node["children"];
344
+ if (!isUnknownArray(children) || children.length === 0) return;
345
+ const firstChild = children[0];
346
+ if (!isPlainRecord(firstChild) || firstChild["type"] !== "Text" || typeof firstChild["start"] !== "number" || typeof firstChild["end"] !== "number") return;
347
+ return {
348
+ start: firstChild["start"],
349
+ end: firstChild["end"],
350
+ isExpression: false
351
+ };
481
352
  }
482
- function hasSuperExpression(constructor) {
483
- let hasSuper = false;
484
- constructor.traverse({ Super(path) {
485
- if (path.parentPath.isCallExpression()) {
486
- path.stop();
487
- hasSuper = true;
353
+ async function parse$1(text, fileName, context) {
354
+ const { parse: svelteParse, preprocess, VERSION } = await import("svelte/compiler");
355
+ let walk;
356
+ if (!isSupportedSvelteVersion(VERSION)) throw new SvelteVersionNotSupported({
357
+ version: VERSION,
358
+ fileName,
359
+ cause: `Expected >=3.30`
360
+ });
361
+ if (isSvelteV5OrLater(VERSION)) {
362
+ const walkerModule = await import(import.meta.resolve("estree-walker", import.meta.resolve("svelte")));
363
+ if (!isPlainRecord(walkerModule) || !isWalkFunction(walkerModule["walk"])) throw new SvelteWalkerNotFound({
364
+ fileName,
365
+ cause: "estree-walker module without walk export"
366
+ });
367
+ walk = walkerModule["walk"];
368
+ } else {
369
+ const svelteCompilerModule = await import("svelte/compiler");
370
+ if (!isPlainRecord(svelteCompilerModule) || !isWalkFunction(svelteCompilerModule["walk"])) throw new SvelteWalkerNotFound({
371
+ fileName,
372
+ cause: "svelte/compiler module without walk export"
373
+ });
374
+ walk = svelteCompilerModule["walk"];
375
+ }
376
+ const lineStarts = computeLineStarts(text);
377
+ const { replacedCode, scriptMap } = await replaceScripts(text);
378
+ const svelteAst = svelteParse(replacedCode, { filename: fileName });
379
+ const { remappedModuleScriptRange, remappedScriptRanges } = remapScriptLocations(replacedCode, scriptMap, getModuleScriptRange(svelteAst), getTemplateScriptRanges(svelteAst, walk));
380
+ const [moduleScript, ...additionalScripts] = await Promise.all([parseTemplateScriptIfDefined(remappedModuleScriptRange), ...remappedScriptRanges.map(parseTemplateScript)]);
381
+ return {
382
+ originFileName: fileName,
383
+ rawContent: text,
384
+ format: "svelte",
385
+ root: {
386
+ ...moduleScript === void 0 ? {} : { moduleScript },
387
+ additionalScripts
488
388
  }
489
- } });
490
- return hasSuper;
491
- }
492
- //#endregion
493
- //#region src/mutators/boolean-literal-mutator.ts
494
- const { types: types$11 } = babel;
495
- const booleanLiteralMutator = {
496
- name: "BooleanLiteral",
497
- *mutate(path) {
498
- if (path.isBooleanLiteral()) yield types$11.booleanLiteral(!path.node.value);
499
- if (path.isUnaryExpression() && path.node.operator === "!" && path.node.prefix) yield deepCloneNode(path.node.argument);
389
+ };
390
+ /**
391
+ * Replaces script tags with placeholders.
392
+ * This is needed, because svelte's `parse` doesn't support `lang="ts"`.
393
+ */
394
+ async function replaceScripts(code) {
395
+ const map = /* @__PURE__ */ new Map();
396
+ let scriptIndex = 0;
397
+ return {
398
+ replacedCode: (await preprocess(code, { script(script) {
399
+ const scriptName = `script${scriptIndex++}`;
400
+ map.set(scriptName, script);
401
+ return { code: scriptName };
402
+ } })).code,
403
+ scriptMap: map
404
+ };
500
405
  }
501
- };
502
- //#endregion
503
- //#region src/mutators/conditional-expression-mutator.ts
504
- const booleanOperators = Object.freeze([
505
- "!=",
506
- "!==",
507
- "&&",
508
- "<",
509
- "<=",
510
- "==",
511
- "===",
512
- ">",
513
- ">=",
514
- "||"
515
- ]);
516
- const { types: types$10 } = babel;
517
- const conditionalExpressionMutator = {
518
- name: "ConditionalExpression",
519
- *mutate(path) {
520
- if (isTestOfLoop(path)) yield types$10.booleanLiteral(false);
521
- else if (isTestOfCondition(path)) {
522
- yield types$10.booleanLiteral(true);
523
- yield types$10.booleanLiteral(false);
524
- } else if (isBooleanExpression(path)) {
525
- if (path.parent?.type === "LogicalExpression") {
526
- if (path.parent.operator === "||") {
527
- yield types$10.booleanLiteral(false);
528
- return;
529
- }
530
- if (path.parent.operator === "&&") {
531
- yield types$10.booleanLiteral(true);
532
- return;
533
- }
406
+ function getTemplateScriptRanges(ast, walker) {
407
+ const ranges = [];
408
+ if (isPlainRecord(ast) && ast["instance"] !== null && ast["instance"] !== void 0) {
409
+ const instance = ast["instance"];
410
+ if (isPlainRecord(instance) && "content" in instance) {
411
+ const content = instance["content"];
412
+ if (isRangedProgram(content)) ranges.push({
413
+ start: content.start,
414
+ end: content.end,
415
+ isExpression: false
416
+ });
417
+ else throw new Error("Svelte instance script without a source range");
534
418
  }
535
- yield types$10.booleanLiteral(true);
536
- yield types$10.booleanLiteral(false);
537
- } else if (path.isForStatement() && !path.node.test) {
538
- const replacement = deepCloneNode(path.node);
539
- replacement.test = types$10.booleanLiteral(false);
540
- yield replacement;
541
- } else if (path.isSwitchCase() && path.node.consequent.length > 0) {
542
- const replacement = deepCloneNode(path.node);
543
- replacement.consequent = [];
544
- yield replacement;
545
419
  }
420
+ if (!isPlainRecord(ast) || !("html" in ast)) throw new Error("Svelte AST without html");
421
+ const html = ast["html"];
422
+ walker(html, { enter(node) {
423
+ const scriptRange = tryGetScriptRangeFromElement(node);
424
+ if (scriptRange) ranges.push(scriptRange);
425
+ const templateExpression = collectTemplateExpression(node);
426
+ if (templateExpression) {
427
+ const { start, end } = templateExpression;
428
+ ranges.push({
429
+ start,
430
+ end,
431
+ isExpression: true
432
+ });
433
+ }
434
+ } });
435
+ return ranges;
546
436
  }
547
- };
548
- function isTestOfLoop(path) {
549
- const { parentPath } = path;
550
- if (!parentPath) return false;
551
- return (parentPath.isForStatement() || parentPath.isWhileStatement() || parentPath.isDoWhileStatement()) && parentPath.node.test === path.node;
552
- }
553
- function isTestOfCondition(path) {
554
- const { parentPath } = path;
555
- if (!parentPath) return false;
556
- return parentPath.isIfStatement() && parentPath.node.test === path.node;
557
- }
558
- function isBooleanExpression(path) {
559
- return (path.isBinaryExpression() || path.isLogicalExpression()) && booleanOperators.includes(path.node.operator);
560
- }
561
- //#endregion
562
- //#region src/mutators/equality-operator-mutator.ts
563
- const { types: t$2 } = babel;
564
- const operators = {
565
- "<": ["<=", ">="],
566
- "<=": ["<", ">"],
567
- ">": [">=", "<="],
568
- ">=": [">", "<"],
569
- "==": ["!="],
570
- "!=": ["=="],
571
- "===": ["!=="],
572
- "!==": ["==="]
573
- };
574
- function isEqualityOperator(operator) {
575
- return Object.keys(operators).includes(operator);
576
- }
577
- const equalityOperatorMutator = {
578
- name: "EqualityOperator",
579
- *mutate(path) {
580
- if (path.isBinaryExpression() && isEqualityOperator(path.node.operator)) for (const mutableOperator of operators[path.node.operator]) {
581
- const replacement = t$2.cloneNode(path.node, true);
582
- replacement.operator = mutableOperator;
583
- yield replacement;
584
- }
437
+ async function parseTemplateScriptIfDefined(range) {
438
+ if (range) return parseTemplateScript(range);
585
439
  }
586
- };
587
- //#endregion
588
- //#region src/mutators/logical-operator-mutator.ts
589
- const logicalOperatorReplacements = Object.freeze({
590
- "&&": "||",
591
- "||": "&&",
592
- "??": "&&"
593
- });
594
- const logicalOperatorMutator = {
595
- name: "LogicalOperator",
596
- *mutate(path) {
597
- if (path.isLogicalExpression() && isSupported$1(path.node.operator)) {
598
- const mutatedOperator = logicalOperatorReplacements[path.node.operator];
599
- const replacement = deepCloneNode(path.node);
600
- replacement.operator = mutatedOperator;
601
- yield replacement;
602
- }
440
+ async function parseTemplateScript({ start, end, isExpression, format }) {
441
+ const scriptText = text.slice(start, end);
442
+ return {
443
+ ast: {
444
+ ...await context.parse(scriptText, fileName, format),
445
+ offset: positionFromOffset(lineStarts, start)
446
+ },
447
+ range: {
448
+ start,
449
+ end
450
+ },
451
+ isExpression
452
+ };
603
453
  }
604
- };
605
- function isSupported$1(operator) {
606
- return Object.keys(logicalOperatorReplacements).includes(operator);
607
454
  }
608
- //#endregion
609
- //#region src/mutators/method-expression-mutator.ts
610
- const { types: types$9 } = babel;
611
- const replacements = /* @__PURE__ */ new Map([
612
- ["charAt", null],
613
- ["endsWith", "startsWith"],
614
- ["every", "some"],
615
- ["filter", null],
616
- ["reverse", null],
617
- ["slice", null],
618
- ["sort", null],
619
- ["substr", null],
620
- ["substring", null],
621
- ["toLocaleLowerCase", "toLocaleUpperCase"],
622
- ["toLowerCase", "toUpperCase"],
623
- ["trim", null],
624
- ["trimEnd", "trimStart"],
625
- ["min", "max"],
626
- ["setDate", "setTime"],
627
- ["setFullYear", "setMonth"],
628
- ["setHours", "setMinutes"],
629
- ["setSeconds", "setMilliseconds"],
630
- ["setUTCDate", "setTime"],
631
- ["setUTCFullYear", "setUTCMonth"],
632
- ["setUTCHours", "setUTCMinutes"],
633
- ["setUTCSeconds", "setUTCMilliseconds"]
634
- ]);
635
- const noReverseRemplacements = ["getUTCDate", "setUTCDate"];
636
- for (const [key, value] of Array.from(replacements)) if (value && !noReverseRemplacements.includes(key)) replacements.set(value, key);
637
- const methodExpressionMutator = {
638
- name: "MethodExpression",
639
- *mutate(path) {
640
- if (!(path.isCallExpression() || path.isOptionalCallExpression())) return;
641
- const { callee } = path.node;
642
- if (!(types$9.isMemberExpression(callee) || types$9.isOptionalMemberExpression(callee)) || !types$9.isIdentifier(callee.property)) return;
643
- const newName = replacements.get(callee.property.name);
644
- if (newName === void 0) return;
645
- if (newName === null) {
646
- yield deepCloneNode(callee.object);
647
- return;
648
- }
649
- const nodeArguments = path.node.arguments.map((argumentNode) => deepCloneNode(argumentNode));
650
- const mutatedCallee = types$9.isMemberExpression(callee) ? types$9.memberExpression(deepCloneNode(callee.object), types$9.identifier(newName), false, callee.optional) : types$9.optionalMemberExpression(deepCloneNode(callee.object), types$9.identifier(newName), false, callee.optional);
651
- yield types$9.isCallExpression(path.node) ? types$9.callExpression(mutatedCallee, nodeArguments) : types$9.optionalCallExpression(mutatedCallee, nodeArguments, path.node.optional);
652
- }
653
- };
654
- //#endregion
655
- //#region src/mutators/object-literal-mutator.ts
656
- const { types: types$8 } = babel;
657
- const objectLiteralMutator = {
658
- name: "ObjectLiteral",
659
- *mutate(path) {
660
- if (path.isObjectExpression() && path.node.properties.length > 0) yield types$8.objectExpression([]);
661
- }
662
- };
663
- //#endregion
664
- //#region src/mutators/optional-chaining-mutator.ts
665
- const { types: t$1 } = babel;
455
+ function getModuleScriptRange(svelteAst) {
456
+ if (!isPlainRecord(svelteAst) || !("module" in svelteAst) || svelteAst["module"] === null || svelteAst["module"] === void 0) return;
457
+ const mod = svelteAst["module"];
458
+ if (!isPlainRecord(mod) || !("content" in mod)) throw new Error("Svelte module script without a source range");
459
+ const content = mod["content"];
460
+ if (!isRangedProgram(content)) throw new Error("Svelte module script without a source range");
461
+ return {
462
+ start: content.start,
463
+ end: content.end,
464
+ isExpression: false
465
+ };
466
+ }
666
467
  /**
667
- * Mutates optional chaining operators
668
- * Note that the AST for optional chaining might not be what you expect. Nodes of type `OptionalMemberExpression` can be either optional or not-optional
669
- *
670
- * For example: In this expression: `foo?.bar.baz` the `.baz` member expression is of type `OptionalMemberExpression`, because it is part of an optional chain, but is is _not_ optional.
671
- * Only the `.bar` optional member expression is optional.
672
- *
673
- * @example
674
- * foo?.bar -> foo.bar
675
- * foo?.[1] -> foo[1]
676
- * foo?.() -> foo()
468
+ * Remaps script locations back to the original places using the script map
677
469
  */
678
- const optionalChainingMutator = {
679
- name: "OptionalChaining",
680
- *mutate(path) {
681
- if (path.isOptionalMemberExpression() && path.node.optional) yield t$1.optionalMemberExpression(t$1.cloneNode(path.node.object, true), t$1.cloneNode(path.node.property, true), path.node.computed, false);
682
- if (path.isOptionalCallExpression() && path.node.optional) yield t$1.optionalCallExpression(t$1.cloneNode(path.node.callee, true), path.node.arguments.map((arg) => t$1.cloneNode(arg, true)), false);
683
- }
684
- };
470
+ function remapScriptLocations(code, scriptMap, moduleScriptRange, templateRanges) {
471
+ const scriptRanges = [moduleScriptRange, ...templateRanges].filter(Predicate.isNotNullish).sort((a, b) => a.start - b.start);
472
+ let offset = 0;
473
+ let newModuleScriptRange;
474
+ const newScriptRanges = scriptRanges.map((range) => {
475
+ const script = code.substring(range.start, range.end);
476
+ const actualScript = scriptMap.get(script);
477
+ const start = range.start + offset;
478
+ if (actualScript) {
479
+ const scriptRange = {
480
+ start,
481
+ end: start + actualScript.content.length,
482
+ isExpression: range.isExpression,
483
+ format: actualScript.attributes["lang"] === "ts" ? "ts" : "js"
484
+ };
485
+ offset += actualScript.content.length - script.length;
486
+ if (range === moduleScriptRange) newModuleScriptRange = scriptRange;
487
+ return scriptRange;
488
+ } else return {
489
+ start,
490
+ end: start + script.length,
491
+ isExpression: range.isExpression,
492
+ format: "js"
493
+ };
494
+ });
495
+ return {
496
+ remappedModuleScriptRange: newModuleScriptRange,
497
+ remappedScriptRanges: newScriptRanges.filter((range) => range !== newModuleScriptRange)
498
+ };
499
+ }
500
+ function collectTemplateExpression(node) {
501
+ if (!isPlainRecord(node) || typeof node["type"] !== "string") return;
502
+ if (!isTemplateExpressionType(node["type"])) return;
503
+ if (!("expression" in node)) return;
504
+ const expression = node["expression"];
505
+ if (isRangedBaseNode(expression)) return expression;
506
+ }
685
507
  //#endregion
686
- //#region src/mutators/regex-mutator.ts
687
- const { types: types$7 } = babel;
508
+ //#region src/parsers/ts-parser.ts
509
+ const { types: types$19, parseAsync } = babel;
510
+ const require = createRequire(import.meta.url);
688
511
  /**
689
- * Checks that a string literal is an obvious regex string literal
690
- * @param path The string literal to checks
691
- * @example
692
- * new RegExp("\\d{4}");
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
693
515
  */
694
- function isObviousRegexString(path) {
695
- return path.parentPath.isNewExpression() && types$7.isIdentifier(path.parentPath.node.callee) && path.parentPath.node.callee.name === RegExp.name && path.parentPath.node.arguments[0] === path.node;
696
- }
697
- function getFlags(path) {
698
- if (types$7.isStringLiteral(path.node.arguments[1])) return path.node.arguments[1].value;
699
- }
700
- const weaponRegexOptions = { mutationLevels: [1] };
701
- const regexMutator = {
702
- name: "Regex",
703
- *mutate(path) {
704
- if (path.isRegExpLiteral()) for (const replacementPattern of mutatePattern(path.node.pattern, path.node.flags)) yield types$7.regExpLiteral(replacementPattern, path.node.flags);
705
- else if (path.isStringLiteral() && isObviousRegexString(path)) {
706
- const parentPath = path.parentPath;
707
- if (parentPath.isNewExpression()) {
708
- const flags = getFlags(parentPath);
709
- for (const replacementPattern of mutatePattern(path.node.value, flags)) yield types$7.stringLiteral(replacementPattern);
710
- }
711
- }
712
- }
713
- };
714
- function mutatePattern(pattern, flags) {
715
- if (pattern.length) try {
716
- return weaponRegex.mutate(pattern, flags, weaponRegexOptions).map((mutant) => mutant.pattern);
717
- } catch (err) {
718
- const message = err instanceof Error ? err.message : typeof err === "string" ? err : JSON.stringify(err) ?? "Unknown error";
719
- console.error(`[RegexMutator]: The Regex parser of weapon-regex couldn't parse this regex pattern: "${pattern}". Please report this issue at https://github.com/stryker-mutator/weapon-regex/issues. Inner error: ${message}`);
720
- }
721
- return [];
722
- }
723
- //#endregion
724
- //#region src/mutators/string-literal-mutator.ts
725
- const { types: types$6 } = babel;
726
- const stringLiteralMutator = {
727
- name: "StringLiteral",
728
- *mutate(path) {
729
- if (path.isTemplateLiteral()) {
730
- const firstQuasi = path.node.quasis[0];
731
- if (firstQuasi === void 0) throw new Error("Template literal without quasis");
732
- const replacement = path.node.quasis.length === 1 && firstQuasi.value.raw.length === 0 ? "Stryker was here!" : "";
733
- yield types$6.templateLiteral([types$6.templateElement({ raw: replacement })], []);
734
- }
735
- if (path.isStringLiteral() && isValidParent(path)) yield types$6.stringLiteral(path.node.value.length === 0 ? "Stryker was here!" : "");
736
- }
737
- };
738
- function isValidParent(child) {
739
- const { parent } = child;
740
- return !isImportExportRelated(parent) && !isJsxOrExpressionRelated(parent) && !isObjectOrClassPropertyKey(parent, child) && !isDisallowedCallExpression(parent);
741
- }
742
- function isImportExportRelated(parent) {
743
- return types$6.isImportDeclaration(parent) || types$6.isExportDeclaration(parent) || types$6.isImportOrExportDeclaration(parent) || types$6.isTSExternalModuleReference(parent);
744
- }
745
- function isJsxOrExpressionRelated(parent) {
746
- return types$6.isJSXAttribute(parent) || types$6.isExpressionStatement(parent) || types$6.isTSLiteralType(parent) || types$6.isObjectMethod(parent);
747
- }
748
- function isObjectOrClassPropertyKey(parent, child) {
749
- return types$6.isObjectProperty(parent) && parent.key === child.node || types$6.isClassProperty(parent) && parent.key === child.node;
750
- }
751
- function isDisallowedCallExpression(parent) {
752
- return isRequireCall(parent) || isSymbolCall(parent) || isImportCall(parent);
753
- }
754
- function isRequireCall(parent) {
755
- return types$6.isCallExpression(parent) && types$6.isIdentifier(parent.callee, { name: "require" });
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
+ };
756
523
  }
757
- function isSymbolCall(parent) {
758
- return types$6.isCallExpression(parent) && types$6.isIdentifier(parent.callee, { name: "Symbol" });
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
+ };
759
531
  }
760
- function isImportCall(parent) {
761
- return types$6.isCallExpression(parent) && types$6.isImport(parent.callee);
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;
762
547
  }
763
548
  //#endregion
764
- //#region src/mutators/unary-operator-mutator.ts
765
- const { types: types$5 } = babel;
766
- var UnaryOperator = /* @__PURE__ */ function(UnaryOperator) {
767
- UnaryOperator["+"] = "-";
768
- UnaryOperator["-"] = "+";
769
- UnaryOperator["~"] = "";
770
- return UnaryOperator;
771
- }(UnaryOperator || {});
772
- const unaryOperatorMutator = {
773
- name: "UnaryOperator",
774
- *mutate(path) {
775
- if (path.isUnaryExpression() && isSupported(path.node.operator) && path.node.prefix) {
776
- const mutatedOperator = UnaryOperator[path.node.operator];
777
- yield isPlusOrMinus(mutatedOperator) ? types$5.unaryExpression(mutatedOperator, deepCloneNode(path.node.argument)) : deepCloneNode(path.node.argument);
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}!`);
778
557
  }
779
- }
780
- };
781
- function isSupported(operator) {
782
- return Object.keys(UnaryOperator).includes(operator);
783
- }
784
- function isPlusOrMinus(operator) {
785
- return operator === "-" || operator === "+";
786
- }
787
- //#endregion
788
- //#region src/mutators/update-operator-mutator.ts
789
- const { types: types$4 } = babel;
790
- var UpdateOperators = /* @__PURE__ */ function(UpdateOperators) {
791
- UpdateOperators["++"] = "--";
792
- UpdateOperators["--"] = "++";
793
- return UpdateOperators;
794
- }(UpdateOperators || {});
795
- //#endregion
796
- //#region src/mutators/mutate.ts
797
- const allMutators = [
798
- arithmeticOperatorMutator,
799
- arrayDeclarationMutator,
800
- arrowFunctionMutator,
801
- blockStatementMutator,
802
- booleanLiteralMutator,
803
- conditionalExpressionMutator,
804
- equalityOperatorMutator,
805
- logicalOperatorMutator,
806
- methodExpressionMutator,
807
- objectLiteralMutator,
808
- stringLiteralMutator,
809
- unaryOperatorMutator,
810
- {
811
- name: "UpdateOperator",
812
- *mutate(path) {
813
- if (path.isUpdateExpression()) yield types$4.updateExpression(UpdateOperators[path.node.operator], deepCloneNode(path.node.argument), path.node.prefix);
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)}`);
814
565
  }
815
- },
816
- regexMutator,
817
- optionalChainingMutator,
818
- assignmentOperatorMutator
819
- ];
820
- //#endregion
821
- //#region src/transformers/mutant-collector.ts
822
- var MutantCollector = class {
823
- _mutants = [];
824
- get mutants() {
825
- return this._mutants;
826
566
  }
827
- /**
828
- * Adds mutants to the internal mutant list.
829
- * @param fileName file name that houses the mutant
830
- * @param original The node to mutate
831
- * @param mutables the named node mutation to be added
832
- * @param contextPath the context where these mutants are found and should be placed as close by as possible
833
- * @param offset offset of mutant nodes
834
- * @returns The mutant (for testability)
835
- */
836
- collect(fileName, original, mutable, offset = {
837
- line: 0,
838
- column: 0
839
- }) {
840
- const mutant = new Mutant$1(this._mutants.length.toString(), fileName, original, mutable, offset);
841
- this._mutants.push(mutant);
842
- return mutant;
843
- }
844
- hasPlacedMutants(fileName) {
845
- return this.mutants.some((mutant) => mutant.fileName === fileName && !mutant.ignoreReason);
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;
846
585
  }
847
- };
848
- //#endregion
849
- //#region src/mutant-placers/mutant-placer.ts
850
- /**
851
- * Narrows an applied mutant to the node kind a placer emits. `applied()` hands
852
- * back a plain node — whether it fits this position is the placer's claim, and
853
- * `canPlace` is what established it, so a mismatch here means the placer was
854
- * handed a mutant it never accepted.
855
- */
856
- function nodeOfKind(mutant, node, isKind, kind) {
857
- if (!isKind(node)) throw new Error(`Cannot place mutant ${mutant.id}: expected ${kind}, got ${node.type}`);
858
- return node;
859
586
  }
860
587
  //#endregion
861
- //#region src/mutant-placers/expression-mutant-placer.ts
862
- const { types: types$3 } = babel;
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;
863
592
  /**
864
- * Will set the identifier of anonymous function expressions if is located in a variable declaration.
865
- * Will treat input as readonly. Returns undefined if not needed.
866
- * @example
867
- * const a = function() { }
868
- * becomes
869
- * const a = function a() {}
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
870
598
  */
871
- function classOrFunctionExpressionNamedIfNeeded(path) {
872
- if ((path.isFunctionExpression() || path.isClassExpression()) && !path.node.id) {
873
- if (path.parentPath.isVariableDeclarator() && types$3.isIdentifier(path.parentPath.node.id)) {
874
- path.node.id = path.parentPath.node.id;
875
- return path.node;
876
- } else if (path.parentPath.isObjectProperty() && types$3.isIdentifier(path.parentPath.node.key) && path.getStatementParent()?.isVariableDeclaration()) {
877
- path.node.id = path.parentPath.node.key;
878
- return path.node;
879
- }
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
+ };
880
622
  }
881
623
  }
882
- /**
883
- * Will set the identifier of anonymous arrow function expressions if is located in a variable declaration.
884
- * Will treat input as readonly. Returns undefined if not needed.
885
- * @example
886
- * const a = () => { }
887
- * becomes
888
- * const a = (() => { const a = () => {}; return a; })()
889
- */
890
- function arrowFunctionExpressionNamedIfNeeded(path) {
891
- if (path.isArrowFunctionExpression() && path.parentPath.isVariableDeclarator() && types$3.isIdentifier(path.parentPath.node.id)) return types$3.callExpression(types$3.arrowFunctionExpression([], types$3.blockStatement([types$3.variableDeclaration("const", [types$3.variableDeclarator(path.parentPath.node.id, path.node)]), types$3.returnStatement(path.parentPath.node.id)])), []);
892
- }
893
- function nameIfAnonymous(path) {
894
- return classOrFunctionExpressionNamedIfNeeded(path) ?? arrowFunctionExpressionNamedIfNeeded(path) ?? path.node;
624
+ function isJSFileWithoutTSDirectives(file, format) {
625
+ return (format === "ts" || format === "js") && !tsDirectiveLikeRegEx.test(file.content);
895
626
  }
896
- function isMemberOrCallOrNonNullExpression(path) {
897
- return isCallExpression(path) || isMemberOrNonNullExpression(path);
627
+ function disableTypeCheckingInBabelAst(ast) {
628
+ return prefixWithNoCheck(removeTSDirectives(ast.rawContent, ast.root.comments));
898
629
  }
899
- function isMemberOrNonNullExpression(path) {
900
- return isMemberExpression(path) || path.isTSNonNullExpression();
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
+ }
901
641
  }
902
- function isMemberExpression(path) {
903
- return path.isMemberExpression() || path.isOptionalMemberExpression();
642
+ function getScriptStart$1(script) {
643
+ const start = script.root.start;
644
+ if (start === void 0 || start === null) throw new Error("Script AST root without start");
645
+ return start;
904
646
  }
905
- function isCallExpression(path) {
906
- return path.isCallExpression() || path.isOptionalCallExpression();
647
+ function getScriptEnd$1(script) {
648
+ const end = script.root.end;
649
+ if (end === void 0 || end === null) throw new Error("Script AST root without end");
650
+ return end;
907
651
  }
908
- function isValidExpression(path) {
909
- const parent = path.parentPath;
910
- return !isObjectPropertyKey() && !isPartOfChain() && !parent.isTaggedTemplateExpression() && !isPartOfDeleteExpression() && !isPartOfAssignmentExpression();
911
- /**
912
- * Determines if the expression is property of an object.
913
- * @example
914
- * const a = {
915
- * 'foo': 'bar' // 'foo' here is an object property
916
- * };
917
- */
918
- function isObjectPropertyKey() {
919
- return parent.isObjectProperty() && parent.node.key === path.node;
920
- }
921
- /**
922
- * Determines if the expression is part of a call/member chain.
923
- * @example
924
- * // bar is part of chain, foo is NOT part of the chain:
925
- * foo.bar.baz();
926
- * foo.bar?.baz()
927
- * foo.bar;
928
- * foo.bar!;
929
- * foo.bar();
930
- * foo?.bar();
931
- * baz[foo.bar()]
932
- * bar?.baz[0]
933
- */
934
- function isPartOfChain() {
935
- return isMemberOrCallOrNonNullExpression(path) && (isMemberExpression(parent) && !(parent.node.computed && parent.node.property === path.node) || parent.isTSNonNullExpression() || isCallExpression(parent) && parent.node.callee === path.node);
652
+ function disableTypeCheckingInHtml(ast) {
653
+ const sortedScripts = [...ast.root.scripts].sort((a, b) => getScriptStart$1(a) - getScriptStart$1(b));
654
+ let currentIndex = 0;
655
+ let html = "";
656
+ for (const script of sortedScripts) {
657
+ html += ast.rawContent.substring(currentIndex, getScriptStart$1(script));
658
+ html += "\n";
659
+ html += prefixWithNoCheck(removeTSDirectives(script.rawContent, script.root.comments));
660
+ html += "\n";
661
+ currentIndex = getScriptEnd$1(script);
936
662
  }
937
- /**
938
- * Determines if the expression is part of a delete expression.
939
- * @returns true if the expression is part of a delete expression
940
- * @example
941
- * delete foo.bar;
942
- */
943
- function isPartOfDeleteExpression() {
944
- return parent.isUnaryExpression() && parent.node.operator === "delete";
663
+ html += ast.rawContent.substring(currentIndex);
664
+ 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);
668
+ 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";
675
+ currentIndex = script.range.end;
945
676
  }
946
- /**
947
- * Determines if the expression is part of an assignment expression.
948
- * @returns true if the expression is part of an assignment expression
949
- * @example
950
- * foo.bar = 42;
951
- * initialNodes.filter((n) => n.id === 'tiptilt')[0].className = tiptiltState;
952
- */
953
- function isPartOfAssignmentExpression() {
954
- return parent.isAssignmentExpression() && parent.node.left === path.node;
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
+ };
955
707
  }
956
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
957
782
  /**
958
- * Places the mutants with a conditional expression: `global.activeMutant === 1? mutatedCode : originalCode`;
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.
959
791
  */
960
- const expressionMutantPlacer = {
961
- name: "expressionMutantPlacer",
962
- canPlace(path) {
963
- return path.isExpression() && isValidExpression(path);
964
- },
965
- place(path, appliedMutants) {
966
- let expression = nameIfAnonymous(path);
967
- expression = mutationCoverageSequenceExpression(appliedMutants.keys(), expression);
968
- for (const [mutant, appliedMutant] of appliedMutants) expression = types$3.conditionalExpression(mutantTestExpression(mutant.id), nodeOfKind(mutant, appliedMutant, types$3.isExpression, "an expression"), expression);
969
- path.replaceWith(expression);
970
- }
971
- };
792
+ const generate = typeof generator === "function" ? generator : generator.default;
972
793
  //#endregion
973
- //#region src/mutant-placers/statement-mutant-placer.ts
974
- const { types: t } = babel;
794
+ //#region src/babel/clone.ts
795
+ const { types: types$18 } = babel;
975
796
  /**
976
- * Mutant placer that places mutants in statements that allow it.
977
- * It uses an `if` statement to do so
797
+ * Helper for `types.cloneNode(node, deep: true, withoutLocations: false);`
978
798
  */
979
- const statementMutantPlacer = {
980
- name: "statementMutantPlacer",
981
- canPlace(path) {
982
- return path.isStatement();
983
- },
984
- place(path, appliedMutants) {
985
- let statement = t.blockStatement([t.expressionStatement(mutationCoverageSequenceExpression(appliedMutants.keys())), ...path.isBlockStatement() ? path.node.body : [path.node]]);
986
- for (const [mutant, appliedMutant] of appliedMutants) statement = t.ifStatement(mutantTestExpression(mutant.id), t.blockStatement([nodeOfKind(mutant, appliedMutant, t.isStatement, "a statement")]), statement);
987
- path.replaceWith(path.isBlockStatement() ? t.blockStatement([statement]) : statement);
988
- }
989
- };
799
+ function deepCloneNode(node) {
800
+ return types$18.cloneNode(node, true, false);
801
+ }
990
802
  //#endregion
991
- //#region src/mutant-placers/switch-case-mutant-placer.ts
803
+ //#region src/babel/equality.ts
804
+ function eqPosition(a, b) {
805
+ return a.line === b.line && a.column === b.column;
806
+ }
807
+ function eqLocation(a, b) {
808
+ return eqPosition(a.start, b.start) && eqPosition(a.end, b.end);
809
+ }
810
+ function eqNode(a, b) {
811
+ return a.type === b.type && !!a.loc && !!b.loc && eqLocation(a.loc, b.loc);
812
+ }
813
+ //#endregion
814
+ //#region src/mutant.ts
815
+ const { traverse: traverse$1 } = babel;
816
+ function createMutant(id, fileName, original, specs, offset = {
817
+ column: 0,
818
+ line: 0
819
+ }) {
820
+ return {
821
+ id,
822
+ fileName,
823
+ original,
824
+ offset,
825
+ replacement: specs.replacement,
826
+ mutatorName: specs.mutatorName,
827
+ 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" }
842
+ };
843
+ }
992
844
  /**
993
- * Places the mutants with consequent of a SwitchCase node. Uses an if-statement to do so.
994
- * @example
995
- * case 'foo':
996
- * if (stryMutAct_9fa48(0)) {} else {
997
- * stryCov_9fa48(0);
998
- * console.log('bar');
999
- * break;
1000
- * }
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
1001
855
  */
1002
- const switchCaseMutantPlacer = {
1003
- name: "switchCaseMutantPlacer",
1004
- canPlace(path) {
1005
- return path.isSwitchCase();
1006
- },
1007
- place(path, appliedMutants) {
1008
- let consequence = babel.types.blockStatement([babel.types.expressionStatement(mutationCoverageSequenceExpression(appliedMutants.keys())), ...path.node.consequent]);
1009
- for (const [mutant, appliedMutant] of appliedMutants) {
1010
- const switchCase = nodeOfKind(mutant, appliedMutant, babel.types.isSwitchCase, "a switch case");
1011
- consequence = babel.types.ifStatement(mutantTestExpression(mutant.id), babel.types.blockStatement(switchCase.consequent), consequence);
856
+ function applyMutant(mutant, originalTree) {
857
+ if (originalTree === mutant.original) return mutant.replacement;
858
+ const mutatedAst = deepCloneNode(originalTree);
859
+ let applied = false;
860
+ const { original, replacement } = mutant;
861
+ traverse$1(mutatedAst, {
862
+ noScope: true,
863
+ enter(path) {
864
+ if (eqNode(path.node, original)) {
865
+ path.replaceWith(replacement);
866
+ path.stop();
867
+ applied = true;
868
+ }
1012
869
  }
1013
- path.replaceWith(babel.types.switchCase(path.node.test, [consequence]));
1014
- }
1015
- };
1016
- //#endregion
1017
- //#region src/mutant-placers/throw-placement-error.ts
1018
- function throwPlacementError(error, nodePath, placer, mutants, fileName) {
1019
- const location = `${path.relative(process.cwd(), fileName)}:${nodePath.node.loc?.start.line}:${nodePath.node.loc?.start.column}`;
1020
- const message = `${placer.name} could not place mutants with type(s): "${new Intl.ListFormat("en").format(mutants.map((mutant) => mutant.mutatorName))}"`;
1021
- 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 https://github.com/stryker-mutator/stryker-js/issues/new?assignees=&labels=%F0%9F%90%9B+Bug&template=bug_report.md&title=${encodeURIComponent(message)}. Original error: ${error.stack}`;
1022
- let builtError = new Error(errorMessage);
1023
- try {
1024
- builtError = nodePath.buildCodeFrameError(errorMessage);
1025
- } catch {}
1026
- throw builtError;
870
+ });
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
+ };
1027
885
  }
1028
886
  //#endregion
1029
- //#region src/mutant-placers/index.ts
1030
- const allMutantPlacers = Object.freeze([
1031
- expressionMutantPlacer,
1032
- statementMutantPlacer,
1033
- switchCaseMutantPlacer
1034
- ]);
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
+ }
1035
902
  //#endregion
1036
- //#region src/util/babel-file.ts
903
+ //#region src/babel/babel-file.ts
1037
904
  /**
1038
905
  * Wraps a parsed AST the way Babel's own pipeline does, so
1039
906
  * `NodePath#buildCodeFrameError` can render a code frame
@@ -1047,327 +914,569 @@ function createBabelFile(filename, code, ast) {
1047
914
  });
1048
915
  }
1049
916
  //#endregion
1050
- //#region src/transformers/directive-bookkeeper.ts
1051
- const WILDCARD = "all";
1052
- const DEFAULT_REASON = "Ignored using a comment";
1053
- var IgnoreRule = class {
1054
- mutatorNames;
1055
- line;
1056
- ignoreReason;
1057
- previousRule;
1058
- constructor(mutatorNames, line, ignoreReason, previousRule) {
1059
- this.mutatorNames = mutatorNames;
1060
- this.line = line;
1061
- this.ignoreReason = ignoreReason;
1062
- this.previousRule = previousRule;
1063
- }
1064
- matches(mutatorName, line) {
1065
- const lineMatches = () => this.line === void 0 || this.line === line;
1066
- const mutatorMatches = () => this.mutatorNames.includes(mutatorName) || this.mutatorNames.includes(WILDCARD);
1067
- return lineMatches() && mutatorMatches();
1068
- }
1069
- findIgnoreReason(mutatorName, line) {
1070
- if (this.matches(mutatorName, line)) return this.ignoreReason;
1071
- return this.previousRule.findIgnoreReason(mutatorName, line);
917
+ //#region src/mutators/arithmetic-operator-mutator.ts
918
+ const { types: t$3 } = babel;
919
+ const arithmeticOperatorReplacements = Object.freeze({
920
+ "+": "-",
921
+ "-": "+",
922
+ "*": "/",
923
+ "/": "*",
924
+ "%": "*"
925
+ });
926
+ const arithmeticOperatorMutator = function* (node, _context) {
927
+ if (t$3.isBinaryExpression(node) && isSupported$3(node.operator, node)) {
928
+ const mutatedOperator = arithmeticOperatorReplacements[node.operator];
929
+ const replacement = deepCloneNode(node);
930
+ replacement.operator = mutatedOperator;
931
+ yield replacement;
1072
932
  }
1073
933
  };
1074
- var RestoreRule = class extends IgnoreRule {
1075
- constructor(mutatorNames, line, previousRule) {
1076
- super(mutatorNames, line, void 0, previousRule);
934
+ function isSupported$3(operator, node) {
935
+ if (!Object.keys(arithmeticOperatorReplacements).includes(operator)) return false;
936
+ const stringTypes = ["StringLiteral", "TemplateLiteral"];
937
+ const leftType = t$3.isBinaryExpression(node.left) ? node.left.right.type : node.left.type;
938
+ if (stringTypes.includes(node.right.type) || stringTypes.includes(leftType)) return false;
939
+ return true;
940
+ }
941
+ //#endregion
942
+ //#region src/mutators/array-declaration-mutator.ts
943
+ const { types: types$17 } = babel;
944
+ 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);
1077
949
  }
1078
950
  };
1079
- const rootRule = { findIgnoreReason() {} };
1080
- /**
1081
- * Responsible for the bookkeeping of "// Stryker" directives like "disable" and "restore".
1082
- */
1083
- var DirectiveBookkeeper = class {
1084
- logger;
1085
- allMutators;
1086
- originFileName;
1087
- strykerCommentDirectiveRegex = /^\s?Stryker (disable|restore)(?: (next-line))? ([a-zA-Z, ]+)(?::(.+)?)?/;
1088
- currentIgnoreRule = rootRule;
1089
- allMutatorNames;
1090
- constructor(logger, allMutators, originFileName) {
1091
- this.logger = logger;
1092
- this.allMutators = allMutators;
1093
- this.originFileName = originFileName;
1094
- this.allMutatorNames = this.allMutators.map((x) => x.name.toLowerCase());
1095
- }
1096
- processStrykerDirectives({ loc, leadingComments }) {
1097
- if (!leadingComments) return;
1098
- for (const comment of leadingComments) {
1099
- const matchResult = this.strykerCommentDirectiveRegex.exec(comment.value);
1100
- if (!matchResult) continue;
1101
- const directiveType = matchResult[1];
1102
- const scope = matchResult[2];
1103
- const mutators = matchResult[3];
1104
- const optionalReason = matchResult[4];
1105
- if (directiveType === void 0 || mutators === void 0) throw new Error("Stryker directive without directive type or mutators");
1106
- let mutatorNames = mutators.split(",").map((mutator) => mutator.trim());
1107
- this.warnAboutUnusedDirective(mutatorNames, directiveType, scope, comment);
1108
- mutatorNames = mutatorNames.map((mutator) => mutator.toLowerCase());
1109
- const reason = (optionalReason ?? DEFAULT_REASON).trim();
1110
- this.applyDirective(directiveType, scope, mutatorNames, reason, loc);
1111
- }
1112
- }
1113
- applyDirective(directiveType, scope, mutatorNames, reason, loc) {
1114
- switch (directiveType) {
1115
- case "disable":
1116
- this.applyDisable(scope, mutatorNames, reason, loc);
1117
- break;
1118
- case "restore": this.applyRestore(scope, mutatorNames, loc);
1119
- }
951
+ //#endregion
952
+ //#region src/mutators/arrow-function-mutator.ts
953
+ const { types: types$16 } = babel;
954
+ 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"));
956
+ };
957
+ //#endregion
958
+ //#region src/mutators/assignment-operator-mutator.ts
959
+ const { types: types$15 } = babel;
960
+ const assignmentOperatorReplacements = Object.freeze({
961
+ "+=": "-=",
962
+ "-=": "+=",
963
+ "*=": "/=",
964
+ "/=": "*=",
965
+ "%=": "*=",
966
+ "<<=": ">>=",
967
+ ">>=": "<<=",
968
+ "&=": "|=",
969
+ "|=": "&=",
970
+ "&&=": "||=",
971
+ "||=": "&&=",
972
+ "??=": "&&="
973
+ });
974
+ const stringTypes = Object.freeze(["StringLiteral", "TemplateLiteral"]);
975
+ const stringAssignmentTypes = Object.freeze([
976
+ "&&=",
977
+ "||=",
978
+ "??="
979
+ ]);
980
+ const assignmentOperatorMutator = function* (node, _context) {
981
+ if (types$15.isAssignmentExpression(node) && isSupportedAssignmentOperator(node.operator) && isSupported$2(node)) {
982
+ const mutatedOperator = assignmentOperatorReplacements[node.operator];
983
+ const replacement = deepCloneNode(node);
984
+ replacement.operator = mutatedOperator;
985
+ yield replacement;
1120
986
  }
1121
- applyDisable(scope, mutatorNames, reason, loc) {
1122
- switch (scope) {
1123
- case "next-line":
1124
- this.currentIgnoreRule = new IgnoreRule(mutatorNames, this.getLine(loc), reason, this.currentIgnoreRule);
1125
- break;
1126
- case void 0:
1127
- default: this.currentIgnoreRule = new IgnoreRule(mutatorNames, void 0, reason, this.currentIgnoreRule);
1128
- }
987
+ };
988
+ function isSupportedAssignmentOperator(operator) {
989
+ return Object.keys(assignmentOperatorReplacements).includes(operator);
990
+ }
991
+ function isSupported$2(node) {
992
+ if (stringTypes.includes(node.right.type) && !stringAssignmentTypes.includes(node.operator)) return false;
993
+ return true;
994
+ }
995
+ //#endregion
996
+ //#region src/mutators/block-statement-mutator.ts
997
+ const { types: types$14 } = babel;
998
+ const blockStatementMutator = function* (node, context) {
999
+ if (types$14.isBlockStatement(node) && isValid(node, context)) yield types$14.blockStatement([]);
1000
+ };
1001
+ function isValid(node, context) {
1002
+ return !isEmpty(node) && !isInvalidConstructorBody(node, context);
1003
+ }
1004
+ function isEmpty(node) {
1005
+ return node.body.length === 0;
1006
+ }
1007
+ function isInvalidConstructorBody(node, context) {
1008
+ 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));
1011
+ const hasInitProps = containsInitializedClassProperties(parent, context);
1012
+ const hasSuper = hasSuperExpression(node);
1013
+ return (hasParamProps || hasInitProps) && hasSuper;
1014
+ }
1015
+ function containsInitializedClassProperties(constructor, context) {
1016
+ 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);
1019
+ }
1020
+ function hasSuperExpression(block) {
1021
+ return containsSuperCall(block);
1022
+ }
1023
+ function isSuperType(node) {
1024
+ return Predicate.hasProperty(node, "type") && node["type"] === "Super";
1025
+ }
1026
+ function isSuperCallExpression(node) {
1027
+ return Predicate.hasProperty(node, "type") && node["type"] === "CallExpression" && Predicate.hasProperty(node, "callee") && isSuperType(node["callee"]);
1028
+ }
1029
+ function containsSuperCall(node) {
1030
+ if (typeof node !== "object" || node === null) return false;
1031
+ if (isSuperType(node) || isSuperCallExpression(node)) return true;
1032
+ return hasSuperInChildren(node);
1033
+ }
1034
+ function hasSuperInChildren(node) {
1035
+ for (const key of Object.keys(node)) {
1036
+ if (!Predicate.hasProperty(node, key)) continue;
1037
+ const value = node[key];
1038
+ if (Array.isArray(value)) {
1039
+ for (const element of value) if (containsSuperCall(element)) return true;
1040
+ } else if (typeof value === "object" && value !== null && containsSuperCall(value)) return true;
1129
1041
  }
1130
- applyRestore(scope, mutatorNames, loc) {
1131
- switch (scope) {
1132
- case "next-line":
1133
- this.currentIgnoreRule = new RestoreRule(mutatorNames, this.getLine(loc), this.currentIgnoreRule);
1134
- break;
1135
- case void 0:
1136
- default: this.currentIgnoreRule = new RestoreRule(mutatorNames, void 0, this.currentIgnoreRule);
1042
+ return false;
1043
+ }
1044
+ //#endregion
1045
+ //#region src/mutators/boolean-literal-mutator.ts
1046
+ const { types: types$13 } = babel;
1047
+ 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);
1050
+ };
1051
+ //#endregion
1052
+ //#region src/mutators/conditional-expression-mutator.ts
1053
+ const booleanOperators = Object.freeze([
1054
+ "!=",
1055
+ "!==",
1056
+ "&&",
1057
+ "<",
1058
+ "<=",
1059
+ "==",
1060
+ "===",
1061
+ ">",
1062
+ ">=",
1063
+ "||"
1064
+ ]);
1065
+ const { types: types$12 } = babel;
1066
+ const conditionalExpressionMutator = function* (node, context) {
1067
+ if (isTestOfLoop(node, context)) yield types$12.booleanLiteral(false);
1068
+ else if (isTestOfCondition(node, context)) {
1069
+ yield types$12.booleanLiteral(true);
1070
+ yield types$12.booleanLiteral(false);
1071
+ } else if (isBooleanExpression(node)) {
1072
+ const parent = context.parent;
1073
+ if (parent !== void 0 && types$12.isLogicalExpression(parent)) {
1074
+ if (parent.operator === "||") {
1075
+ yield types$12.booleanLiteral(false);
1076
+ return;
1077
+ }
1078
+ if (parent.operator === "&&") {
1079
+ yield types$12.booleanLiteral(true);
1080
+ return;
1081
+ }
1137
1082
  }
1083
+ yield types$12.booleanLiteral(true);
1084
+ yield types$12.booleanLiteral(false);
1085
+ } else if (types$12.isForStatement(node) && node.test === null) {
1086
+ const replacement = deepCloneNode(node);
1087
+ replacement.test = types$12.booleanLiteral(false);
1088
+ yield replacement;
1089
+ } else if (types$12.isSwitchCase(node) && node.consequent.length > 0) {
1090
+ const replacement = deepCloneNode(node);
1091
+ replacement.consequent = [];
1092
+ yield replacement;
1138
1093
  }
1139
- getLine(loc) {
1140
- if (loc === void 0 || loc === null || loc.start === null || loc.start === void 0) throw new Error("Babel node without location");
1141
- return loc.start.line;
1142
- }
1143
- findIgnoreReason(line, mutatorName) {
1144
- mutatorName = mutatorName.toLowerCase();
1145
- return this.currentIgnoreRule.findIgnoreReason(mutatorName, line);
1094
+ };
1095
+ function isTestOfLoop(node, context) {
1096
+ const parent = context.parent;
1097
+ 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;
1101
+ return false;
1102
+ }
1103
+ function isTestOfCondition(node, context) {
1104
+ const parent = context.parent;
1105
+ if (parent === void 0) return false;
1106
+ return types$12.isIfStatement(parent) && parent.test === node;
1107
+ }
1108
+ 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);
1111
+ return false;
1112
+ }
1113
+ //#endregion
1114
+ //#region src/mutators/equality-operator-mutator.ts
1115
+ const { types: t$2 } = babel;
1116
+ const operators = {
1117
+ "<": ["<=", ">="],
1118
+ "<=": ["<", ">"],
1119
+ ">": [">=", "<="],
1120
+ ">=": [">", "<"],
1121
+ "==": ["!="],
1122
+ "!=": ["=="],
1123
+ "===": ["!=="],
1124
+ "!==": ["==="]
1125
+ };
1126
+ function isEqualityOperator(operator) {
1127
+ return Object.keys(operators).includes(operator);
1128
+ }
1129
+ 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);
1132
+ replacement.operator = mutableOperator;
1133
+ yield replacement;
1146
1134
  }
1147
- warnAboutUnusedDirective(mutators, directiveType, scope, comment) {
1148
- for (const mutator of mutators) {
1149
- if (mutator === WILDCARD) continue;
1150
- if (!this.allMutatorNames.includes(mutator.toLowerCase())) {
1151
- const commentLoc = comment.loc;
1152
- if (commentLoc === void 0 || commentLoc === null || commentLoc.start === null || commentLoc.start === void 0) throw new Error("Comment without location");
1153
- this.logger.warn(`Unused 'Stryker ${scope ? directiveType + " " + scope : directiveType}' directive. Mutator with name '${mutator}' not found. Directive found at: ${this.originFileName}:${commentLoc.start.line}:${commentLoc.start.column}.`);
1154
- }
1155
- }
1135
+ };
1136
+ //#endregion
1137
+ //#region src/mutators/logical-operator-mutator.ts
1138
+ const { types: types$11 } = babel;
1139
+ const logicalOperatorReplacements = Object.freeze({
1140
+ "&&": "||",
1141
+ "||": "&&",
1142
+ "??": "&&"
1143
+ });
1144
+ const logicalOperatorMutator = function* (node, _context) {
1145
+ if (types$11.isLogicalExpression(node) && isSupported$1(node.operator)) {
1146
+ const mutatedOperator = logicalOperatorReplacements[node.operator];
1147
+ const replacement = deepCloneNode(node);
1148
+ replacement.operator = mutatedOperator;
1149
+ yield replacement;
1156
1150
  }
1157
1151
  };
1152
+ function isSupported$1(operator) {
1153
+ return Object.keys(logicalOperatorReplacements).includes(operator);
1154
+ }
1158
1155
  //#endregion
1159
- //#region src/transformers/ignorer-bookkeeper.ts
1156
+ //#region src/mutators/method-expression-mutator.ts
1157
+ const { types: types$10 } = babel;
1158
+ const baseReplacements = {
1159
+ charAt: null,
1160
+ endsWith: "startsWith",
1161
+ every: "some",
1162
+ filter: null,
1163
+ reverse: null,
1164
+ slice: null,
1165
+ sort: null,
1166
+ substr: null,
1167
+ substring: null,
1168
+ toLocaleLowerCase: "toLocaleUpperCase",
1169
+ toLowerCase: "toUpperCase",
1170
+ trim: null,
1171
+ trimEnd: "trimStart",
1172
+ min: "max",
1173
+ setDate: "setTime",
1174
+ setFullYear: "setMonth",
1175
+ setHours: "setMinutes",
1176
+ setSeconds: "setMilliseconds",
1177
+ setUTCDate: "setTime",
1178
+ setUTCFullYear: "setUTCMonth",
1179
+ setUTCHours: "setUTCMinutes",
1180
+ setUTCSeconds: "setUTCMilliseconds"
1181
+ };
1182
+ 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
+ }
1188
+ const methodExpressionMutator = function* (node, _context) {
1189
+ if (!(types$10.isCallExpression(node) || types$10.isOptionalCallExpression(node))) return;
1190
+ 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];
1193
+ if (newName === void 0) return;
1194
+ if (newName === null) {
1195
+ yield deepCloneNode(callee.object);
1196
+ return;
1197
+ }
1198
+ 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);
1201
+ };
1202
+ //#endregion
1203
+ //#region src/mutators/object-literal-mutator.ts
1204
+ const { types: types$9 } = babel;
1205
+ const objectLiteralMutator = function* (node, _context) {
1206
+ if (types$9.isObjectExpression(node) && node.properties.length > 0) yield types$9.objectExpression([]);
1207
+ };
1208
+ //#endregion
1209
+ //#region src/mutators/optional-chaining-mutator.ts
1210
+ const { types: t$1 } = babel;
1211
+ const optionalChainingMutator = function* (node, _context) {
1212
+ 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
+ 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
+ };
1215
+ //#endregion
1216
+ //#region src/mutators/regex-mutation.ts
1160
1217
  /**
1161
- * Responsible for keeping track of the active ignore message and node using the configured ignore-plugins.
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)` |
1234
+ *
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.
1238
+ *
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.
1162
1242
  */
1163
- var IgnorerBookkeeper = class {
1164
- ignorers;
1165
- activeIgnored;
1166
- get currentIgnoreMessage() {
1167
- return this.activeIgnored?.message;
1168
- }
1169
- constructor(ignorers) {
1170
- this.ignorers = ignorers;
1171
- }
1172
- enterNode(path) {
1173
- if (!this.activeIgnored) this.ignorers.forEach((ignorer) => {
1174
- const message = ignorer.shouldIgnore(path);
1175
- if (message) this.activeIgnored = {
1176
- node: path.node,
1177
- message
1178
- };
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")
1179
1250
  });
1180
- }
1181
- leaveNode(path) {
1182
- if (this.activeIgnored?.node === path.node) this.activeIgnored = void 0;
1183
- }
1184
- };
1185
- //#endregion
1186
- //#region src/transformers/babel-transformer.ts
1187
- const { traverse: traverse$1 } = babel;
1188
- const transformBabel = ({ root, originFileName, rawContent, offset }, mutantCollector, { options, mutateDescription, logger }, mutators = allMutators, mutantPlacers = allMutantPlacers) => {
1189
- const file = createBabelFile(originFileName, rawContent, root);
1190
- const placementMap = /* @__PURE__ */ new Map();
1191
- const directiveBookkeeper = new DirectiveBookkeeper(logger, mutators, originFileName);
1192
- const ignorerBookkeeper = new IgnorerBookkeeper(options.ignorers);
1193
- traverse$1(file.ast, {
1194
- enter(path) {
1195
- directiveBookkeeper.processStrykerDirectives(path.node);
1196
- if (shouldSkip(path)) path.skip();
1197
- else {
1198
- ignorerBookkeeper.enterNode(path);
1199
- addToPlacementMapIfPossible(path);
1200
- if (shouldMutate(path)) {
1201
- const mutantsToPlace = collectMutants(path);
1202
- if (mutantsToPlace.length) {
1203
- const placementPath = path.find((ancestor) => placementMap.has(ancestor.node));
1204
- if (placementPath) {
1205
- const placement = placementMap.get(placementPath.node);
1206
- if (placement === void 0) throw new Error("Placement not found for node");
1207
- const { appliedMutants } = placement;
1208
- mutantsToPlace.forEach((mutant) => appliedMutants.set(mutant, mutant.applied(placementPath.node)));
1209
- } else throw new Error(`Mutants cannot be placed. This shouldn't happen! Unplaced mutants: ${JSON.stringify(mutantsToPlace, null, 2)}`);
1210
- }
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
+ });
1211
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
+ });
1212
1328
  }
1213
- },
1214
- exit(path) {
1215
- placeMutantsIfNeeded(path);
1216
- ignorerBookkeeper.leaveNode(path);
1217
- }
1218
- });
1219
- placeHeaderIfNeeded(mutantCollector, originFileName, options, root);
1220
- /**
1221
- * If this node can be used to place mutants on, add to the placement map
1222
- */
1223
- function addToPlacementMapIfPossible(path) {
1224
- const placer = mutantPlacers.find((p) => p.canPlace(path));
1225
- if (placer) placementMap.set(path.node, {
1226
- appliedMutants: /* @__PURE__ */ new Map(),
1227
- placer
1228
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 [];
1229
1338
  }
1230
- /**
1231
- * Don't traverse import declarations, decorators and nodes that don't have overlap with the selected mutation ranges
1232
- */
1233
- function shouldSkip(path) {
1234
- return isTypeNode(path) || isImportDeclaration(path) || path.isDecorator() || !mutateDescription || Array.isArray(mutateDescription) && mutateDescription.every((range) => !locationOverlaps(range, getNodeLocation(path.node)));
1235
- }
1236
- function shouldMutate(path) {
1237
- return mutateDescription === true || Array.isArray(mutateDescription) && mutateDescription.some((range) => locationIncluded(range, getNodeLocation(path.node)));
1238
- }
1239
- /**
1240
- * Place mutants that are assigned to the current node path (on exit)
1241
- */
1242
- function placeMutantsIfNeeded(path) {
1243
- const mutantsPlacement = placementMap.get(path.node);
1244
- if (mutantsPlacement?.appliedMutants.size) try {
1245
- mutantsPlacement.placer.place(path, mutantsPlacement.appliedMutants);
1246
- path.skip();
1247
- } catch (error) {
1248
- throwPlacementError(toError(error), path, mutantsPlacement.placer, [...mutantsPlacement.appliedMutants.keys()], originFileName);
1249
- }
1250
- }
1251
- /**
1252
- * Collect the mutants for the current node and return the non-ignored.
1253
- */
1254
- function collectMutants(path) {
1255
- return [...mutate(path)].map((mutable) => mutantCollector.collect(originFileName, path.node, mutable, offset)).filter((mutant) => !mutant.ignoreReason);
1256
- }
1257
- /**
1258
- * Generate mutants for the current node.
1259
- * @yields {Mutable} A mutable describing the mutant to be placed
1260
- */
1261
- function* mutate(node) {
1262
- for (const mutator of mutators) for (const replacement of mutator.mutate(node)) {
1263
- const ignoreReason = directiveBookkeeper.findIgnoreReason(getNodeLocation(node.node).start.line, mutator.name) ?? findExcludedMutatorIgnoreReason(mutator.name) ?? ignorerBookkeeper.currentIgnoreMessage;
1264
- yield {
1265
- replacement,
1266
- mutatorName: mutator.name,
1267
- ...ignoreReason === void 0 ? {} : { ignoreReason }
1268
- };
1269
- }
1270
- function findExcludedMutatorIgnoreReason(mutatorName) {
1271
- if (options.excludedMutations.includes(mutatorName)) return `Ignored because of excluded mutation "${mutatorName}"`;
1272
- else return;
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);
1273
1350
  }
1274
1351
  }
1275
1352
  };
1276
- function getNodeLocation(node) {
1277
- const loc = node.loc;
1278
- if (loc === void 0 || loc === null || loc.start === null || loc.start === void 0 || loc.end === null || loc.end === void 0) throw new Error("Babel node without location");
1279
- return loc;
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;
1280
1357
  }
1281
- function toError(value) {
1282
- if (value instanceof Error) return value;
1283
- return new Error("Unexpected error", { cause: value });
1358
+ function getFlags(node) {
1359
+ const secondArg = node.arguments[1];
1360
+ if (secondArg !== void 0 && types$8.isStringLiteral(secondArg)) return secondArg.value;
1284
1361
  }
1285
1362
  //#endregion
1286
- //#region src/transformers/html-transformer.ts
1287
- const transformHtml = ({ root }, mutantCollector, context) => {
1288
- root.scripts.forEach((ast) => {
1289
- context.transform(ast, mutantCollector, context);
1290
- });
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!" : "");
1291
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
+ }
1292
1400
  //#endregion
1293
- //#region src/transformers/svelte-transformer.ts
1294
- const moduleScript = `<script context="module">
1295
- \n<\/script>\n`;
1296
- const transformSvelte = (svelte, mutantCollector, context) => {
1297
- const { root, originFileName } = svelte;
1298
- [root.moduleScript, ...root.additionalScripts].filter(notEmpty).forEach((script) => {
1299
- context.transform(script.ast, mutantCollector, {
1300
- ...context,
1301
- options: {
1302
- ...context.options,
1303
- noHeader: true
1304
- }
1305
- });
1306
- });
1307
- if (mutantCollector.hasPlacedMutants(originFileName)) {
1308
- if (!root.moduleScript) {
1309
- root.moduleScript = {
1310
- ast: {
1311
- format: "js",
1312
- root: types.file(types.program([])),
1313
- rawContent: "",
1314
- originFileName
1315
- },
1316
- range: {
1317
- start: 26,
1318
- end: 26
1319
- },
1320
- isExpression: false
1321
- };
1322
- svelte.rawContent = `${moduleScript}${svelte.rawContent}`;
1323
- svelte.root.additionalScripts.forEach((script) => {
1324
- script.range.start += moduleScript.length;
1325
- script.range.end += moduleScript.length;
1326
- });
1327
- }
1328
- placeHeader(root.moduleScript.ast.root);
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);
1329
1412
  }
1330
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
+ };
1331
1430
  //#endregion
1332
- //#region src/transformers/transformer.ts
1431
+ //#region src/mutators/registry.ts
1333
1432
  /**
1334
- * Transform the AST by generating mutants and placing them in the AST.
1335
- * Supports all AST formats supported by Stryker.
1336
- * @param ast The Abstract Syntax Tree
1337
- * @param mutantCollector the mutant collector that will be used to register and administer mutants
1338
- * @param transformerContext the options used during transforming
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.
1339
1447
  */
1340
- function transform(ast, mutantCollector, transformerContext) {
1341
- const context = {
1342
- ...transformerContext,
1343
- transform
1344
- };
1345
- switch (ast.format) {
1346
- case "html":
1347
- transformHtml(ast, mutantCollector, context);
1348
- break;
1349
- case "js":
1350
- case "ts":
1351
- case "tsx":
1352
- transformBabel(ast, mutantCollector, context);
1353
- break;
1354
- case "svelte": transformSvelte(ast, mutantCollector, context);
1355
- }
1356
- }
1448
+ const allMutators = Object.freeze({
1449
+ ArithmeticOperator: arithmeticOperatorMutator,
1450
+ ArrayDeclaration: arrayDeclarationMutator,
1451
+ ArrowFunction: arrowFunctionMutator,
1452
+ AssignmentOperator: assignmentOperatorMutator,
1453
+ BlockStatement: blockStatementMutator,
1454
+ BooleanLiteral: booleanLiteralMutator,
1455
+ ConditionalExpression: conditionalExpressionMutator,
1456
+ EqualityOperator: equalityOperatorMutator,
1457
+ LogicalOperator: logicalOperatorMutator,
1458
+ MethodExpression: methodExpressionMutator,
1459
+ ObjectLiteral: objectLiteralMutator,
1460
+ OptionalChaining: optionalChainingMutator,
1461
+ Regex: regexMutator,
1462
+ StringLiteral: stringLiteralMutator,
1463
+ UnaryOperator: unaryOperatorMutator,
1464
+ UpdateOperator: updateOperatorMutator
1465
+ });
1357
1466
  //#endregion
1358
- //#region src/util/syntax-helpers.ts
1467
+ //#region src/babel/instrumentation-header.ts
1359
1468
  const STRYKER_NAMESPACE_HELPER = "stryNS_9fa48";
1360
- const COVER_MUTANT_HELPER = "stryCov_9fa48";
1361
- const IS_MUTANT_ACTIVE_HELPER = "stryMutAct_9fa48";
1362
- const { types: types$2, traverse } = babel;
1469
+ const COVER_MUTANT_HELPER$1 = "stryCov_9fa48";
1470
+ const IS_MUTANT_ACTIVE_HELPER$1 = "stryMutAct_9fa48";
1471
+ const { types: types$4 } = babel;
1363
1472
  /**
1364
1473
  * Returns syntax for the header if JS/TS files
1365
1474
  */
1366
1475
  const parsedInstrumentationHeader = babel.parse(`function ${STRYKER_NAMESPACE_HELPER}(){
1367
1476
  var g = typeof globalThis === 'object' && globalThis && globalThis.Math === Math && globalThis || new Function("return this")();
1368
- var ns = g.${ID.NAMESPACE} || (g.${ID.NAMESPACE} = {});
1369
- if (ns.${ID.ACTIVE_MUTANT} === undefined && g.process && g.process.env && g.process.env.${ID.ACTIVE_MUTANT_ENV_VARIABLE}) {
1370
- ns.${ID.ACTIVE_MUTANT} = g.process.env.${ID.ACTIVE_MUTANT_ENV_VARIABLE};
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};
1371
1480
  }
1372
1481
  function retrieveNS(){
1373
1482
  return ns;
@@ -1377,72 +1486,84 @@ const parsedInstrumentationHeader = babel.parse(`function ${STRYKER_NAMESPACE_HE
1377
1486
  }
1378
1487
  ${STRYKER_NAMESPACE_HELPER}();
1379
1488
 
1380
- function ${COVER_MUTANT_HELPER}() {
1489
+ function ${COVER_MUTANT_HELPER$1}() {
1381
1490
  var ns = ${STRYKER_NAMESPACE_HELPER}();
1382
- var cov = ns.${ID.MUTATION_COVERAGE_OBJECT} || (ns.${ID.MUTATION_COVERAGE_OBJECT} = { static: {}, perTest: {} });
1491
+ var cov = ns.${INSTRUMENTER_CONSTANTS.MUTATION_COVERAGE_OBJECT} || (ns.${INSTRUMENTER_CONSTANTS.MUTATION_COVERAGE_OBJECT} = { static: {}, perTest: {} });
1383
1492
  function cover() {
1384
1493
  var c = cov.static;
1385
- if (ns.${ID.CURRENT_TEST_ID}) {
1386
- c = cov.perTest[ns.${ID.CURRENT_TEST_ID}] = cov.perTest[ns.${ID.CURRENT_TEST_ID}] || {};
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}] || {};
1387
1496
  }
1388
1497
  var a = arguments;
1389
1498
  for(var i=0; i < a.length; i++){
1390
1499
  c[a[i]] = (c[a[i]] || 0) + 1;
1391
1500
  }
1392
1501
  }
1393
- ${COVER_MUTANT_HELPER} = cover;
1502
+ ${COVER_MUTANT_HELPER$1} = cover;
1394
1503
  cover.apply(null, arguments);
1395
1504
  }
1396
- function ${IS_MUTANT_ACTIVE_HELPER}(id) {
1505
+ function ${IS_MUTANT_ACTIVE_HELPER$1}(id) {
1397
1506
  var ns = ${STRYKER_NAMESPACE_HELPER}();
1398
1507
  function isActive(id) {
1399
- if (ns.${ID.ACTIVE_MUTANT} === id) {
1400
- if (ns.${ID.HIT_COUNT} !== void 0 && ++ns.${ID.HIT_COUNT} > ns.${ID.HIT_LIMIT}) {
1401
- throw new Error('Stryker: Hit count limit reached (' + ns.${ID.HIT_COUNT} + ')');
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} + ')');
1402
1511
  }
1403
1512
  return true;
1404
1513
  }
1405
1514
  return false;
1406
1515
  }
1407
- ${IS_MUTANT_ACTIVE_HELPER} = isActive;
1516
+ ${IS_MUTANT_ACTIVE_HELPER$1} = isActive;
1408
1517
  return isActive(id);
1409
1518
  }`, {
1410
1519
  configFile: false,
1411
1520
  browserslistConfigFile: false,
1412
1521
  env: { targets: {} }
1413
1522
  });
1414
- if (!types$2.isFile(parsedInstrumentationHeader)) throw new Error("Instrumentation header parsed as non-File");
1523
+ if (!types$4.isFile(parsedInstrumentationHeader)) throw new Error("Instrumentation header parsed as non-File");
1415
1524
  const instrumentationBabelHeader = parsedInstrumentationHeader.program.body;
1416
1525
  deepFreeze(instrumentationBabelHeader);
1417
- /**
1418
- * returns syntax for `global.activeMutant === $mutantId`
1419
- * @param mutantId The id of the mutant to switch
1420
- */
1421
- function mutantTestExpression(mutantId) {
1422
- return types$2.callExpression(types$2.identifier(IS_MUTANT_ACTIVE_HELPER), [types$2.stringLiteral(mutantId)]);
1526
+ function placeHeaderIfNeeded(mutantCollector, originFileName, options, root) {
1527
+ if (hasPlacedMutants(mutantCollector, originFileName) && !options.noHeader) placeHeader(root);
1423
1528
  }
1424
- function eqLocation(a, b) {
1425
- function eqPosition(start, end) {
1426
- return start.column === end.column && start.line === end.line;
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)];
1427
1538
  }
1428
- return eqPosition(a.start, b.start) && eqPosition(a.end, b.end);
1429
- }
1430
- function eqNode(a, b) {
1431
- return a.type === b.type && !!a.loc && !!b.loc && eqLocation(a.loc, b.loc);
1539
+ root.program.body.unshift(...header);
1432
1540
  }
1433
- /**
1434
- * Returns a sequence of mutation coverage counters with an optional last expression.
1435
- *
1436
- * @example (global.__coverMutant__(0, 1), 40 + 2)
1437
- * @param mutants The mutants for which covering syntax needs to be generated
1438
- * @param targetExpression The original expression
1439
- */
1440
- function mutationCoverageSequenceExpression(mutants, targetExpression) {
1441
- const mutantIds = [...mutants].map((mutant) => types$2.stringLiteral(mutant.id));
1442
- const sequence = [types$2.callExpression(types$2.identifier(COVER_MUTANT_HELPER), mutantIds)];
1443
- if (targetExpression) sequence.push(targetExpression);
1444
- return types$2.sequenceExpression(sequence);
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);
1558
+ }
1559
+ for (const v of Object.values(value)) deepFreeze(v);
1560
+ return Object.freeze(value);
1561
+ }
1562
+ return value;
1445
1563
  }
1564
+ //#endregion
1565
+ //#region src/babel/type-guards.ts
1566
+ const { types: types$3 } = babel;
1446
1567
  function isTypeNode(path) {
1447
1568
  return path.isTypeAnnotation() || flowTypeAnnotationNodeTypes.includes(path.node.type) || tsTypeAnnotationNodeTypes.includes(path.node.type) || isDeclareVariableStatement(path) || isDeclareModule(path);
1448
1569
  }
@@ -1489,326 +1610,604 @@ const flowTypeAnnotationNodeTypes = Object.freeze([
1489
1610
  "InterfaceDeclaration"
1490
1611
  ]);
1491
1612
  function isImportDeclaration(path) {
1492
- return types$2.isTSImportEqualsDeclaration(path.node) || path.isImportDeclaration();
1613
+ return types$3.isTSImportEqualsDeclaration(path.node) || path.isImportDeclaration();
1493
1614
  }
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";
1494
1620
  /**
1495
- * Determines if a location (needle) is included in an other location (haystack)
1496
- * @param haystack The range to look in
1497
- * @param needle the range to search for
1621
+ * returns syntax for `global.activeMutant === $mutantId`
1622
+ * @param mutantId The id of the mutant to switch
1498
1623
  */
1499
- function locationIncluded(haystack, needle) {
1500
- const startIncluded = haystack.start.line < needle.start.line || haystack.start.line === needle.start.line && haystack.start.column <= needle.start.column;
1501
- const endIncluded = haystack.end.line > needle.end.line || haystack.end.line === needle.end.line && haystack.end.column >= needle.end.column;
1502
- return startIncluded && endIncluded;
1624
+ function mutantTestExpression(mutantId) {
1625
+ return types$2.callExpression(types$2.identifier(IS_MUTANT_ACTIVE_HELPER), [types$2.stringLiteral(mutantId)]);
1503
1626
  }
1504
1627
  /**
1505
- * Determines if two locations overlap with each other
1628
+ * Returns a sequence of mutation coverage counters with an optional last expression.
1629
+ *
1630
+ * @example (global.__coverMutant__(0, 1), 40 + 2)
1631
+ * @param mutants The mutants for which covering syntax needs to be generated
1632
+ * @param targetExpression The original expression
1506
1633
  */
1507
- function locationOverlaps(a, b) {
1508
- const startIncluded = a.start.line < b.end.line || a.start.line === b.end.line && a.start.column <= b.end.column;
1509
- const endIncluded = a.end.line > b.start.line || a.end.line === b.start.line && a.end.column >= b.start.column;
1510
- return startIncluded && endIncluded;
1634
+ 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)];
1637
+ if (targetExpression) sequence.push(targetExpression);
1638
+ return types$2.sequenceExpression(sequence);
1511
1639
  }
1640
+ //#endregion
1641
+ //#region src/mutant-placers/mutant-placer.ts
1512
1642
  /**
1513
- * Helper for `types.cloneNode(node, deep: true, withoutLocations: false);`
1643
+ * Narrows an applied mutant to the node kind a placer emits. `applied()` hands
1644
+ * back a plain node — whether it fits this position is the placer's claim, and
1645
+ * `canPlace` is what established it, so a mismatch here means the placer was
1646
+ * handed a mutant it never accepted.
1514
1647
  */
1515
- function deepCloneNode(node) {
1516
- return types$2.cloneNode(node, true, false);
1517
- }
1518
- function placeHeaderIfNeeded(mutantCollector, originFileName, options, root) {
1519
- if (mutantCollector.hasPlacedMutants(originFileName) && !options.noHeader) placeHeader(root);
1648
+ function nodeOfKind(mutant, node, isKind, kind) {
1649
+ if (!isKind(node)) throw new Error(`Cannot place mutant ${mutant.id}: expected ${kind}, got ${node.type}`);
1650
+ return node;
1520
1651
  }
1521
- function placeHeader(root) {
1522
- let header = instrumentationBabelHeader;
1523
- const leadingComments = root.program.body[0]?.leadingComments;
1524
- if (Array.isArray(leadingComments)) {
1525
- const firstHeader = instrumentationBabelHeader[0];
1526
- if (firstHeader === void 0) throw new Error("Instrumentation header is empty");
1527
- const cloned = types$2.cloneNode(firstHeader, true, false);
1528
- cloned.leadingComments = leadingComments;
1529
- header = [cloned, ...instrumentationBabelHeader.slice(1)];
1652
+ //#endregion
1653
+ //#region src/mutant-placers/expression-mutant-placer.ts
1654
+ const { types: types$1 } = babel;
1655
+ /**
1656
+ * Will set the identifier of anonymous function expressions if is located in a variable declaration.
1657
+ * Will treat input as readonly. Returns undefined if not needed.
1658
+ * @example
1659
+ * const a = function() { }
1660
+ * becomes
1661
+ * const a = function a() {}
1662
+ */
1663
+ function classOrFunctionExpressionNamedIfNeeded(path) {
1664
+ if ((path.isFunctionExpression() || path.isClassExpression()) && !path.node.id) {
1665
+ if (path.parentPath.isVariableDeclarator() && types$1.isIdentifier(path.parentPath.node.id)) {
1666
+ path.node.id = path.parentPath.node.id;
1667
+ 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
+ }
1530
1672
  }
1531
- root.program.body.unshift(...header);
1532
1673
  }
1533
- //#endregion
1534
- //#region src/parsers/svelte-parser.ts
1535
- const MIN_SVELTE_VERSION = ">=3.30";
1536
- function isPlainRecord(value) {
1537
- return typeof value === "object" && value !== null;
1674
+ /**
1675
+ * Will set the identifier of anonymous arrow function expressions if is located in a variable declaration.
1676
+ * Will treat input as readonly. Returns undefined if not needed.
1677
+ * @example
1678
+ * const a = () => { }
1679
+ * becomes
1680
+ * const a = (() => { const a = () => {}; return a; })()
1681
+ */
1682
+ 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)])), []);
1538
1684
  }
1539
- function isUnknownArray(value) {
1540
- return Array.isArray(value);
1685
+ function nameIfAnonymous(path) {
1686
+ return classOrFunctionExpressionNamedIfNeeded(path) ?? arrowFunctionExpressionNamedIfNeeded(path) ?? path.node;
1541
1687
  }
1542
- function isRangedProgram(value) {
1543
- return isPlainRecord(value) && typeof value["start"] === "number" && typeof value["end"] === "number";
1688
+ function isMemberOrCallOrNonNullExpression(path) {
1689
+ return isCallExpression(path) || isMemberOrNonNullExpression(path);
1544
1690
  }
1545
- function isWalkFunction(value) {
1546
- return typeof value === "function";
1691
+ function isMemberOrNonNullExpression(path) {
1692
+ return isMemberExpression(path) || path.isTSNonNullExpression();
1547
1693
  }
1548
- function isRangedBaseNode(value) {
1549
- return isPlainRecord(value) && typeof value["type"] === "string" && typeof value["start"] === "number" && typeof value["end"] === "number";
1694
+ function isMemberExpression(path) {
1695
+ return path.isMemberExpression() || path.isOptionalMemberExpression();
1550
1696
  }
1551
- function isTemplateExpressionType(type) {
1552
- return type === "MustacheTag" || type === "RawMustacheTag" || type === "IfBlock" || type === "ConstTag" || type === "EachBlock" || type === "AwaitBlock" || type === "KeyBlock" || type === "EventHandler";
1697
+ function isCallExpression(path) {
1698
+ return path.isCallExpression() || path.isOptionalCallExpression();
1553
1699
  }
1554
- function tryGetScriptRangeFromElement(node) {
1555
- if (!isPlainRecord(node) || node["type"] !== "Element" || node["name"] !== "script") return;
1556
- const children = node["children"];
1557
- if (!isUnknownArray(children) || children.length === 0) return;
1558
- const firstChild = children[0];
1559
- if (!isPlainRecord(firstChild) || firstChild["type"] !== "Text" || typeof firstChild["start"] !== "number" || typeof firstChild["end"] !== "number") return;
1560
- return {
1561
- start: firstChild["start"],
1562
- end: firstChild["end"],
1563
- isExpression: false
1564
- };
1700
+ function isValidExpression(path) {
1701
+ const parent = path.parentPath;
1702
+ return !isObjectPropertyKey() && !isPartOfChain() && !parent.isTaggedTemplateExpression() && !isPartOfDeleteExpression() && !isPartOfAssignmentExpression();
1703
+ /**
1704
+ * Determines if the expression is property of an object.
1705
+ * @example
1706
+ * const a = {
1707
+ * 'foo': 'bar' // 'foo' here is an object property
1708
+ * };
1709
+ */
1710
+ function isObjectPropertyKey() {
1711
+ return parent.isObjectProperty() && parent.node.key === path.node;
1712
+ }
1713
+ /**
1714
+ * Determines if the expression is part of a call/member chain.
1715
+ * @example
1716
+ * // bar is part of chain, foo is NOT part of the chain:
1717
+ * foo.bar.baz();
1718
+ * foo.bar?.baz()
1719
+ * foo.bar;
1720
+ * foo.bar!;
1721
+ * foo.bar();
1722
+ * foo?.bar();
1723
+ * baz[foo.bar()]
1724
+ * bar?.baz[0]
1725
+ */
1726
+ function isPartOfChain() {
1727
+ return isMemberOrCallOrNonNullExpression(path) && (isMemberExpression(parent) && !(parent.node.computed && parent.node.property === path.node) || parent.isTSNonNullExpression() || isCallExpression(parent) && parent.node.callee === path.node);
1728
+ }
1729
+ /**
1730
+ * Determines if the expression is part of a delete expression.
1731
+ * @returns true if the expression is part of a delete expression
1732
+ * @example
1733
+ * delete foo.bar;
1734
+ */
1735
+ function isPartOfDeleteExpression() {
1736
+ return parent.isUnaryExpression() && parent.node.operator === "delete";
1737
+ }
1738
+ /**
1739
+ * Determines if the expression is part of an assignment expression.
1740
+ * @returns true if the expression is part of an assignment expression
1741
+ * @example
1742
+ * foo.bar = 42;
1743
+ * initialNodes.filter((n) => n.id === 'tiptilt')[0].className = tiptiltState;
1744
+ */
1745
+ function isPartOfAssignmentExpression() {
1746
+ return parent.isAssignmentExpression() && parent.node.left === path.node;
1747
+ }
1748
+ }
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();
1775
+ },
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();
1798
+ },
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);
1804
+ }
1805
+ path.replaceWith(babel.types.switchCase(path.node.test, [consequence]));
1806
+ }
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
+ ]);
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;
1838
+ }
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;
1565
1846
  }
1566
- async function parse$1(text, fileName, context) {
1567
- const { parse: svelteParse, preprocess, VERSION } = await import("./compiler-Ck-td5Ds.mjs");
1568
- let walk;
1569
- if (!satisfies(VERSION, MIN_SVELTE_VERSION)) throw new Error(`Svelte version ${VERSION} not supported. Expected: ${MIN_SVELTE_VERSION} (processing file ${fileName})`);
1570
- if (satisfies(VERSION, ">=5")) {
1571
- const walkerModule = await import(import.meta.resolve("estree-walker", import.meta.resolve("svelte")));
1572
- if (!isPlainRecord(walkerModule) || !isWalkFunction(walkerModule["walk"])) throw new Error("estree-walker module without walk export");
1573
- walk = walkerModule["walk"];
1574
- } else {
1575
- const svelteCompilerModule = await import("./compiler-Ck-td5Ds.mjs");
1576
- if (!isPlainRecord(svelteCompilerModule) || !isWalkFunction(svelteCompilerModule["walk"])) throw new Error("svelte/compiler module without walk export");
1577
- walk = svelteCompilerModule["walk"];
1578
- }
1579
- const positionConverter = new PositionConverter(text);
1580
- const { replacedCode, scriptMap } = await replaceScripts(text);
1581
- const svelteAst = svelteParse(replacedCode, { filename: fileName });
1582
- const { remappedModuleScriptRange, remappedScriptRanges } = remapScriptLocations(replacedCode, scriptMap, getModuleScriptRange(svelteAst), getTemplateScriptRanges(svelteAst, walk));
1583
- const [moduleScript, ...additionalScripts] = await Promise.all([parseTemplateScriptIfDefined(remappedModuleScriptRange), ...remappedScriptRanges.map(parseTemplateScript)]);
1584
- return {
1585
- originFileName: fileName,
1586
- rawContent: text,
1587
- format: "svelte",
1588
- root: {
1589
- ...moduleScript === void 0 ? {} : { moduleScript },
1590
- additionalScripts
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;
1591
1871
  }
1592
- };
1593
- /**
1594
- * Replaces script tags with placeholders.
1595
- * This is needed, because svelte's `parse` doesn't support `lang="ts"`.
1596
- */
1597
- async function replaceScripts(code) {
1598
- const map = /* @__PURE__ */ new Map();
1599
- let scriptIndex = 0;
1600
- return {
1601
- replacedCode: (await preprocess(code, { script(script) {
1602
- const scriptName = `script${scriptIndex++}`;
1603
- map.set(scriptName, script);
1604
- return { code: scriptName };
1605
- } })).code,
1606
- scriptMap: map
1607
- };
1608
1872
  }
1609
- function getTemplateScriptRanges(ast, walker) {
1610
- const ranges = [];
1611
- if (isPlainRecord(ast) && ast["instance"] !== null && ast["instance"] !== void 0) {
1612
- const instance = ast["instance"];
1613
- if (isPlainRecord(instance) && "content" in instance) {
1614
- const content = instance["content"];
1615
- if (isRangedProgram(content)) ranges.push({
1616
- start: content.start,
1617
- end: content.end,
1618
- isExpression: false
1619
- });
1620
- else throw new Error("Svelte instance script without a source range");
1873
+ }
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}.`);
1621
1897
  }
1622
1898
  }
1623
- if (!isPlainRecord(ast) || !("html" in ast)) throw new Error("Svelte AST without html");
1624
- const html = ast["html"];
1625
- walker(html, { enter(node) {
1626
- const scriptRange = tryGetScriptRangeFromElement(node);
1627
- if (scriptRange) ranges.push(scriptRange);
1628
- const templateExpression = collectTemplateExpression(node);
1629
- if (templateExpression) {
1630
- const { start, end } = templateExpression;
1631
- ranges.push({
1632
- start,
1633
- end,
1634
- isExpression: true
1635
- });
1636
- }
1637
- } });
1638
- return ranges;
1899
+ mutatorNames = mutatorNames.map((mutator) => mutator.toLowerCase());
1900
+ const reason = (optionalReason ?? DEFAULT_REASON).trim();
1901
+ current = applyDirective(current, directiveType, scope, mutatorNames, reason, node.loc);
1639
1902
  }
1640
- async function parseTemplateScriptIfDefined(range) {
1641
- if (range) return parseTemplateScript(range);
1903
+ return {
1904
+ rule: current,
1905
+ warnings
1906
+ };
1907
+ }
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
+ }
1914
+ }
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
+ };
1642
1932
  }
1643
- async function parseTemplateScript({ start, end, isExpression, format }) {
1644
- const scriptText = text.slice(start, end);
1645
- return {
1646
- ast: {
1647
- ...await context.parse(scriptText, fileName, format),
1648
- offset: positionConverter.positionFromOffset(start)
1649
- },
1650
- range: {
1651
- start,
1652
- end
1653
- },
1654
- isExpression
1933
+ }
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
1655
1948
  };
1656
1949
  }
1657
1950
  }
1658
- function getModuleScriptRange(svelteAst) {
1659
- if (!isPlainRecord(svelteAst) || !("module" in svelteAst) || svelteAst["module"] === null || svelteAst["module"] === void 0) return;
1660
- const mod = svelteAst["module"];
1661
- if (!isPlainRecord(mod) || !("content" in mod)) throw new Error("Svelte module script without a source range");
1662
- const content = mod["content"];
1663
- if (!isRangedProgram(content)) throw new Error("Svelte module script without a source range");
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;
1954
+ }
1955
+ //#endregion
1956
+ //#region src/transformers/ignorer-bookkeeper.ts
1957
+ function createIgnorerBookkeeper(ignorers) {
1664
1958
  return {
1665
- start: content.start,
1666
- end: content.end,
1667
- isExpression: false
1959
+ ignorers,
1960
+ activeIgnored: void 0
1668
1961
  };
1669
1962
  }
1670
- /**
1671
- * Remaps script locations back to the original places using the script map
1672
- */
1673
- function remapScriptLocations(code, scriptMap, moduleScriptRange, templateRanges) {
1674
- const scriptRanges = [moduleScriptRange, ...templateRanges].filter(notEmpty).sort((a, b) => a.start - b.start);
1675
- let offset = 0;
1676
- let newModuleScriptRange;
1677
- const newScriptRanges = scriptRanges.map((range) => {
1678
- const script = code.substring(range.start, range.end);
1679
- const actualScript = scriptMap.get(script);
1680
- const start = range.start + offset;
1681
- if (actualScript) {
1682
- const scriptRange = {
1683
- start,
1684
- end: start + actualScript.content.length,
1685
- isExpression: range.isExpression,
1686
- format: actualScript.attributes["lang"] === "ts" ? "ts" : "js"
1687
- };
1688
- offset += actualScript.content.length - script.length;
1689
- if (range === moduleScriptRange) newModuleScriptRange = scriptRange;
1690
- return scriptRange;
1691
- } else return {
1692
- start,
1693
- end: start + script.length,
1694
- isExpression: range.isExpression,
1695
- format: "js"
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
1976
+ }
1696
1977
  };
1697
- });
1698
- return {
1699
- remappedModuleScriptRange: newModuleScriptRange,
1700
- remappedScriptRanges: newScriptRanges.filter((range) => range !== newModuleScriptRange)
1701
- };
1978
+ }
1979
+ return bookkeeper;
1702
1980
  }
1703
- function collectTemplateExpression(node) {
1704
- if (!isPlainRecord(node) || typeof node["type"] !== "string") return;
1705
- if (!isTemplateExpressionType(node["type"])) return;
1706
- if (!("expression" in node)) return;
1707
- const expression = node["expression"];
1708
- if (isRangedBaseNode(expression)) return expression;
1981
+ function leaveNode(bookkeeper, path) {
1982
+ if (bookkeeper.activeIgnored?.node === path.node) return {
1983
+ ...bookkeeper,
1984
+ activeIgnored: void 0
1985
+ };
1986
+ return bookkeeper;
1709
1987
  }
1710
- //#endregion
1711
- //#region src/parsers/ts-parser.ts
1712
- const { types: types$1, parseAsync } = babel;
1713
- const require = createRequire(import.meta.url);
1714
- /**
1715
- * See https://babeljs.io/docs/en/babel-preset-typescript
1716
- * @param text The text to parse
1717
- * @param fileName The name of the file
1718
- */
1719
- async function parseTS(text, fileName) {
1988
+ function toIgnorerPath(path) {
1720
1989
  return {
1721
- originFileName: fileName,
1722
- rawContent: text,
1723
- format: "ts",
1724
- root: await parse(text, fileName, false)
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();
2006
+ }
1725
2007
  };
1726
2008
  }
1727
- async function parseTsx(text, fileName) {
2009
+ //#endregion
2010
+ //#region src/transformers/babel-transformer.ts
2011
+ const { traverse } = babel;
2012
+ const transformBabel = ({ root, originFileName, rawContent, offset }, mutantCollector, { options, mutateDescription }, mutators = allMutators, mutantPlacers = allMutantPlacers) => {
2013
+ const file = createBabelFile(originFileName, rawContent, root);
2014
+ const placementMap = /* @__PURE__ */ new Map();
2015
+ let directiveRule = rootRule;
2016
+ const mutatorEntries = Object.entries(mutators);
2017
+ const allMutatorNames = mutatorEntries.map(([name]) => name.toLowerCase());
2018
+ let ignorerState = createIgnorerBookkeeper(options.ignorers);
2019
+ const warnings = [];
2020
+ traverse(file.ast, {
2021
+ enter(path) {
2022
+ const result = processStrykerDirectives(directiveRule, path.node, allMutatorNames, originFileName);
2023
+ directiveRule = result.rule;
2024
+ warnings.push(...result.warnings);
2025
+ if (shouldSkip(path)) path.skip();
2026
+ else {
2027
+ ignorerState = enterNode(ignorerState, path);
2028
+ addToPlacementMapIfPossible(path);
2029
+ if (shouldMutate(path)) {
2030
+ const mutantsToPlace = collectMutants(path);
2031
+ if (mutantsToPlace.length > 0) {
2032
+ const placementPath = path.find((ancestor) => placementMap.has(ancestor.node));
2033
+ if (placementPath) {
2034
+ const placement = placementMap.get(placementPath.node);
2035
+ if (placement === void 0) throw new Error("Placement not found for node");
2036
+ const { appliedMutants } = placement;
2037
+ mutantsToPlace.forEach((mutant) => appliedMutants.set(mutant, applyMutant(mutant, placementPath.node)));
2038
+ } else throw new Error(`Mutants cannot be placed. This shouldn't happen! Unplaced mutants: ${JSON.stringify(mutantsToPlace, null, 2)}`);
2039
+ }
2040
+ }
2041
+ }
2042
+ },
2043
+ exit(path) {
2044
+ placeMutantsIfNeeded(path);
2045
+ ignorerState = leaveNode(ignorerState, path);
2046
+ }
2047
+ });
2048
+ placeHeaderIfNeeded(mutantCollector, originFileName, options, root);
2049
+ return warnings;
2050
+ function addToPlacementMapIfPossible(path) {
2051
+ const placer = mutantPlacers.find((p) => p.canPlace(path));
2052
+ if (placer !== void 0) placementMap.set(path.node, {
2053
+ appliedMutants: /* @__PURE__ */ new Map(),
2054
+ placer
2055
+ });
2056
+ }
2057
+ function shouldSkip(path) {
2058
+ return isTypeNode(path) || isImportDeclaration(path) || path.isDecorator() || !mutateDescription || Array.isArray(mutateDescription) && mutateDescription.every((range) => !locationOverlaps(range, getNodeLocation(path.node)));
2059
+ }
2060
+ function shouldMutate(path) {
2061
+ return mutateDescription === true || Array.isArray(mutateDescription) && mutateDescription.some((range) => locationIncluded(range, getNodeLocation(path.node)));
2062
+ }
2063
+ function placeMutantsIfNeeded(path) {
2064
+ const mutantsPlacement = placementMap.get(path.node);
2065
+ if (mutantsPlacement !== void 0 && mutantsPlacement.appliedMutants.size > 0) try {
2066
+ mutantsPlacement.placer.place(path, mutantsPlacement.appliedMutants);
2067
+ path.skip();
2068
+ } catch (error) {
2069
+ throwPlacementError(toError(error), path, mutantsPlacement.placer, [...mutantsPlacement.appliedMutants.keys()], originFileName);
2070
+ }
2071
+ }
2072
+ function collectMutants(path) {
2073
+ return [...mutate(path)].map((mutable) => collect(mutantCollector, originFileName, path.node, mutable, offset)).filter((mutant) => !mutant.ignoreReason);
2074
+ }
2075
+ function* mutate(path) {
2076
+ const context = toMutatorContext(path);
2077
+ 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 {
2080
+ replacement,
2081
+ mutatorName,
2082
+ ...ignoreReason === void 0 ? {} : { ignoreReason }
2083
+ };
2084
+ }
2085
+ function findExcludedMutatorIgnoreReason(mutatorName) {
2086
+ if (options.excludedMutations.includes(mutatorName)) return `Ignored because of excluded mutation "${mutatorName}"`;
2087
+ else return;
2088
+ }
2089
+ }
2090
+ };
2091
+ function toMutatorContext(path) {
2092
+ const ancestors = [];
2093
+ let current = path.parentPath;
2094
+ while (current !== null && current !== void 0) {
2095
+ ancestors.push(current.node);
2096
+ current = current.parentPath;
2097
+ }
1728
2098
  return {
1729
- root: await parse(text, fileName, true),
1730
- format: "tsx",
1731
- originFileName: fileName,
1732
- rawContent: text
2099
+ parent: ancestors[0],
2100
+ grandParent: ancestors[1],
2101
+ ancestors
1733
2102
  };
1734
2103
  }
1735
- async function parse(text, fileName, isTSX) {
1736
- const ast = await parseAsync(text, {
1737
- filename: fileName,
1738
- parserOpts: { ranges: true },
1739
- configFile: false,
1740
- babelrc: false,
1741
- presets: [[require.resolve("@babel/preset-typescript"), {
1742
- isTSX,
1743
- allExtensions: true
1744
- }]],
1745
- plugins: [[require.resolve("@babel/plugin-proposal-decorators"), { legacy: true }], [require.resolve("@babel/plugin-transform-explicit-resource-management")]]
1746
- });
1747
- if (ast === null) throw new Error(`Expected ${fileName} to contain a babel.types.file, but it yielded null`);
1748
- if (types$1.isProgram(ast)) throw new Error(`Expected ${fileName} to contain a babel.types.file, but was a program`);
1749
- return ast;
2104
+ function getNodeLocation(node) {
2105
+ 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;
2108
+ }
2109
+ function toError(value) {
2110
+ if (value instanceof Error) return value;
2111
+ return new Error("Unexpected error", { cause: value });
1750
2112
  }
1751
2113
  //#endregion
1752
- //#region src/parsers/create-parser.ts
1753
- function createParser(parserOptions) {
1754
- const jsParse = createParser$1(parserOptions);
1755
- async function parse(code, fileName, formatOverride) {
1756
- const format = getFormat(fileName, formatOverride);
1757
- if (!format) {
1758
- const ext = path.extname(fileName).toLowerCase();
1759
- throw new Error(`Unable to parse ${fileName}. No parser registered for ${ext}!`);
1760
- }
1761
- switch (format) {
1762
- case "js": return jsParse(code, fileName);
1763
- case "tsx": return parseTsx(code, fileName);
1764
- case "ts": return parseTS(code, fileName);
1765
- case "html": return parse$2(code, fileName, { parse });
1766
- case "svelte": return parse$1(code, fileName, { parse });
1767
- default: throw new Error(`Unsupported format: ${String(format)}`);
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
+ }));
2137
+ });
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
+ });
1768
2158
  }
2159
+ placeHeader(root.moduleScript.ast.root);
1769
2160
  }
1770
- return parse;
1771
- }
1772
- function getFormat(fileName, override) {
1773
- if (override) return override;
1774
- else switch (path.extname(fileName).toLowerCase()) {
1775
- case ".js":
1776
- case ".jsx":
1777
- case ".mjs":
1778
- case ".cjs": return "js";
1779
- case ".mts":
1780
- case ".cts":
1781
- case ".ts": return "ts";
1782
- case ".tsx": return "tsx";
1783
- case ".vue":
1784
- case ".html":
1785
- case ".htm": return "html";
1786
- case ".svelte": return "svelte";
1787
- default: return;
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
2169
+ };
2170
+ switch (ast.format) {
2171
+ case "html": return transformHtml(ast, mutantCollector, context);
2172
+ case "js":
2173
+ case "ts":
2174
+ case "tsx": return transformBabel(ast, mutantCollector, context);
2175
+ case "svelte": return transformSvelte(ast, mutantCollector, context);
1788
2176
  }
1789
2177
  }
1790
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;
2187
+ }
2188
+ };
2189
+ //#endregion
1791
2190
  //#region src/printers/html-printer.ts
1792
- function getScriptStart$1(script) {
2191
+ function getScriptStart(script) {
1793
2192
  const start = script.root.start;
1794
2193
  if (start === void 0 || start === null) throw new Error("Script AST root without start");
1795
2194
  return start;
1796
2195
  }
1797
- function getScriptEnd$1(script) {
2196
+ function getScriptEnd(script) {
1798
2197
  const end = script.root.end;
1799
2198
  if (end === void 0 || end === null) throw new Error("Script AST root without end");
1800
2199
  return end;
1801
2200
  }
1802
2201
  const print$4 = (ast, context) => {
1803
- const sortedScripts = [...ast.root.scripts].sort((a, b) => getScriptStart$1(a) - getScriptStart$1(b));
2202
+ const sortedScripts = [...ast.root.scripts].sort((a, b) => getScriptStart(a) - getScriptStart(b));
1804
2203
  let currentIndex = 0;
1805
2204
  let html = "";
1806
2205
  for (const script of sortedScripts) {
1807
- html += ast.rawContent.substring(currentIndex, getScriptStart$1(script));
2206
+ html += ast.rawContent.substring(currentIndex, getScriptStart(script));
1808
2207
  html += "\n";
1809
2208
  html += context.print(script, context);
1810
2209
  html += "\n";
1811
- currentIndex = getScriptEnd$1(script);
2210
+ currentIndex = getScriptEnd(script);
1812
2211
  }
1813
2212
  html += ast.rawContent.substr(currentIndex);
1814
2213
  return html;
@@ -1823,7 +2222,7 @@ const print$3 = (file) => {
1823
2222
  const print$2 = ({ root, rawContent }, context) => {
1824
2223
  let currentIndex = 0;
1825
2224
  let outputText = "";
1826
- const sortedScripts = [root.moduleScript, ...root.additionalScripts].filter(notEmpty).sort((a, b) => a.range.start - b.range.start);
2225
+ const sortedScripts = [root.moduleScript, ...root.additionalScripts].filter(Predicate.isNotNullish).sort((a, b) => a.range.start - b.range.start);
1827
2226
  for (const script of sortedScripts) if (script.isExpression) {
1828
2227
  const codeWithoutSemicolon = context.print(script.ast, context).slice(0, -1);
1829
2228
  outputText += rawContent.substring(currentIndex, script.range.start) + codeWithoutSemicolon;
@@ -1859,244 +2258,58 @@ function print(file) {
1859
2258
  }
1860
2259
  }
1861
2260
  //#endregion
1862
- //#region src/create-instrumenter.ts
1863
- createInstrumenter.inject = tokens(commonTokens.injector);
1864
- function createInstrumenter(injector) {
1865
- return injector.provideValue(instrumenterTokens.print, print).provideValue(instrumenterTokens.createParser, createParser).provideValue(instrumenterTokens.transform, transform).injectClass(Instrumenter);
1866
- }
1867
- //#endregion
1868
- //#region src/disable-type-checks.ts
1869
- const commentDirectiveRegEx = /^(\s*)@(ts-[a-z-]+).*$/;
1870
- const tsDirectiveLikeRegEx = /@(ts-[a-z-]+)/;
1871
- const startingCommentRegex = /(^\s*\/\*.*?\*\/)/gs;
1872
- /**
1873
- * Disables TypeScript type checking for a single file by inserting `// @ts-nocheck` commands.
1874
- * It also does this for *.js files, as they can be type checked by typescript as well.
1875
- * Other file types are silently ignored
1876
- *
1877
- * @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#-ts-nocheck-in-typescript-files
1878
- */
1879
- async function disableTypeChecks(file, options) {
1880
- const format = getFormat(file.name);
1881
- if (!format) return file;
1882
- if (isJSFileWithoutTSDirectives(file, format)) return {
1883
- ...file,
1884
- content: prefixWithNoCheck(file.content)
1885
- };
1886
- const ast = await createParser(options)(file.content, file.name);
1887
- switch (ast.format) {
1888
- case "js":
1889
- case "ts":
1890
- case "tsx": return {
1891
- ...file,
1892
- content: disableTypeCheckingInBabelAst(ast)
1893
- };
1894
- case "html": return {
1895
- ...file,
1896
- content: disableTypeCheckingInHtml(ast)
1897
- };
1898
- case "svelte": return {
1899
- ...file,
1900
- content: disableTypeCheckingInSvelte(ast)
1901
- };
1902
- }
1903
- }
1904
- function isJSFileWithoutTSDirectives(file, format) {
1905
- return (format === "ts" || format === "js") && !tsDirectiveLikeRegEx.test(file.content);
1906
- }
1907
- function disableTypeCheckingInBabelAst(ast) {
1908
- return prefixWithNoCheck(removeTSDirectives(ast.rawContent, ast.root.comments));
1909
- }
1910
- function prefixWithNoCheck(code) {
1911
- if (code.startsWith("#")) {
1912
- const newLineIndex = code.indexOf("\n");
1913
- if (newLineIndex > 0) return `${code.substring(0, newLineIndex)}\n// @ts-nocheck\n${code.substring(newLineIndex + 1)}`;
1914
- else return code;
1915
- } else {
1916
- startingCommentRegex.lastIndex = 0;
1917
- const leadingComment = startingCommentRegex.exec(code)?.[1];
1918
- if (leadingComment === void 0) return `// @ts-nocheck\n${code}`;
1919
- return `${leadingComment.concat("\n")}// @ts-nocheck\n${code.substring(leadingComment.length)}`;
1920
- }
1921
- }
1922
- function getScriptStart(script) {
1923
- const start = script.root.start;
1924
- if (start === void 0 || start === null) throw new Error("Script AST root without start");
1925
- return start;
1926
- }
1927
- function getScriptEnd(script) {
1928
- const end = script.root.end;
1929
- if (end === void 0 || end === null) throw new Error("Script AST root without end");
1930
- return end;
1931
- }
1932
- function disableTypeCheckingInHtml(ast) {
1933
- const sortedScripts = [...ast.root.scripts].sort((a, b) => getScriptStart(a) - getScriptStart(b));
1934
- let currentIndex = 0;
1935
- let html = "";
1936
- for (const script of sortedScripts) {
1937
- html += ast.rawContent.substring(currentIndex, getScriptStart(script));
1938
- html += "\n";
1939
- html += prefixWithNoCheck(removeTSDirectives(script.rawContent, script.root.comments));
1940
- html += "\n";
1941
- currentIndex = getScriptEnd(script);
1942
- }
1943
- html += ast.rawContent.substring(currentIndex);
1944
- return html;
1945
- }
1946
- function disableTypeCheckingInSvelte(ast) {
1947
- const sortedScripts = [ast.root.moduleScript, ...ast.root.additionalScripts].filter(notEmpty).sort((a, b) => a.range.start - b.range.start);
1948
- let currentIndex = 0;
1949
- let html = "";
1950
- for (const script of sortedScripts) {
1951
- html += ast.rawContent.substring(currentIndex, script.range.start);
1952
- html += "\n";
1953
- html += prefixWithNoCheck(removeTSDirectives(script.ast.rawContent, script.ast.root.comments));
1954
- html += "\n";
1955
- currentIndex = script.range.end;
1956
- }
1957
- html += ast.rawContent.substring(currentIndex);
1958
- return html;
1959
- }
1960
- function removeTSDirectives(text, comments) {
1961
- const directiveRanges = comments?.map(tryParseTSDirective).filter(notEmpty).sort((a, b) => a.startPos - b.startPos);
1962
- if (directiveRanges) {
1963
- let currentIndex = 0;
1964
- let pruned = "";
1965
- for (const directiveRange of directiveRanges) {
1966
- pruned += text.substring(currentIndex, directiveRange.startPos);
1967
- currentIndex = directiveRange.endPos;
1968
- }
1969
- pruned += text.substring(currentIndex);
1970
- return pruned;
1971
- } else return text;
1972
- }
1973
- function tryParseTSDirective(comment) {
1974
- const match = commentDirectiveRegEx.exec(comment.value);
1975
- if (match) {
1976
- const start = comment.start;
1977
- if (start === void 0 || start === null) throw new Error("Comment without start");
1978
- const directivePrefix = match[1];
1979
- if (directivePrefix === void 0) throw new Error("TS directive match without prefix");
1980
- const directiveName = match[2];
1981
- if (directiveName === void 0) throw new Error("TS directive match without directive name");
1982
- const directiveStartPos = start + directivePrefix.length + 2;
1983
- return {
1984
- startPos: directiveStartPos,
1985
- endPos: directiveStartPos + directiveName.length + 1
1986
- };
1987
- }
1988
- }
1989
- //#endregion
1990
- //#region src/frameworks/angular-ignorer.ts
1991
- const ANGULAR_SIGNAL_IO_FUNCTIONS = Object.freeze([
1992
- "input",
1993
- "model",
1994
- "output"
1995
- ]);
1996
- const ANGULAR_SIGNAL_QUERY_FUNCTIONS = Object.freeze([
1997
- "contentChild",
1998
- "contentChildren",
1999
- "viewChild",
2000
- "viewChildren"
2001
- ]);
2002
- 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.";
2003
- const SIGNAL_QUERY_OPTIONS_MSG = "Angular signal query options object cannot be mutated as that causes issues with the Angular compiler.";
2004
- var AngularIgnorer = class {
2005
- shouldIgnore(path) {
2006
- if (this.isInputModelOrOutputConfigurationObject(path)) return INPUT_MODEL_OUTPUT_CONFIG_MSG;
2007
- if (this.isSignalQueryOptionsObject(path)) return SIGNAL_QUERY_OPTIONS_MSG;
2008
- }
2009
- #isClassFieldLike(path) {
2010
- return path.isClassProperty() || path.isClassPrivateProperty() || path.isClassAccessorProperty();
2011
- }
2012
- /**
2013
- * Determines if the given path is a configuration object for an Angular input, model or output function.
2014
- * This solves the "Argument needs to be statically analyzable." error
2015
- */
2016
- isInputModelOrOutputConfigurationObject(path) {
2017
- if (!path.isObjectExpression() || !path.parentPath.isCallExpression() || !path.parentPath.parentPath.isClassProperty()) return false;
2018
- const callExpression = path.parentPath;
2019
- const objectExpression = path;
2020
- const isRequiredSignalIOFunction = callExpression.node.callee.type === "MemberExpression" && callExpression.node.callee.object.type === "Identifier" && ANGULAR_SIGNAL_IO_FUNCTIONS.includes(callExpression.node.callee.object.name) && callExpression.node.callee.property.type === "Identifier" && callExpression.node.callee.property.name === "required";
2021
- const isSignalIOFunction = callExpression.node.callee.type === "Identifier" && ANGULAR_SIGNAL_IO_FUNCTIONS.includes(callExpression.node.callee.name);
2022
- const isOutput = callExpression.node.callee.type === "Identifier" && callExpression.node.callee.name === "output";
2023
- if (isRequiredSignalIOFunction || isOutput) return callExpression.node.arguments.length >= 1 && callExpression.node.arguments[0] === objectExpression.node;
2024
- if (isSignalIOFunction) return callExpression.node.arguments.length >= 2 && callExpression.node.arguments[1] === objectExpression.node;
2025
- return false;
2026
- }
2027
- /**
2028
- * Determines if the given path is a configuration object for an Angular signal query function.
2029
- * This solves the "Argument needs to be statically analyzable." error
2030
- */
2031
- isSignalQueryOptionsObject(path) {
2032
- if (!path.isObjectExpression() || !path.parentPath.isCallExpression() || !this.#isClassFieldLike(path.parentPath.parentPath)) return false;
2033
- const callExpression = path.parentPath;
2034
- const objectExpression = path;
2035
- const callee = callExpression.node.callee;
2036
- const isQueryFn = callee.type === "Identifier" && ANGULAR_SIGNAL_QUERY_FUNCTIONS.includes(callee.name);
2037
- const isRequiredQueryFn = callee.type === "MemberExpression" && callee.object.type === "Identifier" && ANGULAR_SIGNAL_QUERY_FUNCTIONS.includes(callee.object.name) && callee.property.type === "Identifier" && callee.property.name === "required";
2038
- if (!isQueryFn && !isRequiredQueryFn) return false;
2039
- return callExpression.node.arguments.length >= 2 && callExpression.node.arguments[1] === objectExpression.node;
2040
- }
2041
- };
2042
- //#endregion
2043
- //#region src/frameworks/index.ts
2044
- const strykerPlugins = [declareClassPlugin(PluginKind.Ignore, "angular", AngularIgnorer)];
2045
- const frameworkPluginsFileUrl = import.meta.url;
2046
- //#endregion
2047
2261
  //#region src/instrumenter.ts
2048
- /**
2049
- * The instrumenter is responsible for
2050
- * * Generating mutants based on source files
2051
- * * Instrumenting the source code with the mutants placed in `mutant switches`.
2052
- * * Adding mutant coverage expressions in the source code.
2053
- * @see https://github.com/stryker-mutator/stryker-js/issues/1514
2054
- */
2055
- var Instrumenter = class {
2056
- logger;
2057
- _createParser;
2058
- _print;
2059
- _transform;
2060
- static inject = tokens(commonTokens.logger, instrumenterTokens.createParser, instrumenterTokens.print, instrumenterTokens.transform);
2061
- constructor(logger, _createParser = createParser, _print = print, _transform = transform) {
2062
- this.logger = logger;
2063
- this._createParser = _createParser;
2064
- this._print = _print;
2065
- this._transform = _transform;
2066
- }
2067
- async instrument(files, options) {
2068
- this.logger.debug("Instrumenting %d source files with mutants", files.length);
2069
- const mutantCollector = new MutantCollector();
2070
- const outFiles = [];
2071
- let mutantCount = 0;
2072
- const parse = this._createParser(options);
2073
- for (const { name, mutate, content } of files) {
2074
- const ast = await parse(content, name);
2075
- this._transform(ast, mutantCollector, {
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;
2268
+ const parse = createParser(options);
2269
+ for (const { name, mutate, content } of files) {
2270
+ const ast = yield* Effect.tryPromise({
2271
+ try: () => parse(content, name),
2272
+ catch: (cause) => new InstrumentError({
2273
+ message: `Failed to parse ${name}`,
2274
+ cause
2275
+ })
2276
+ });
2277
+ const warnings = yield* Effect.try({
2278
+ try: () => transform(ast, collector, {
2076
2279
  options,
2077
- mutateDescription: toBabelLineNumber(mutate),
2078
- logger: this.logger
2079
- });
2080
- const mutatedContent = this._print(ast);
2081
- outFiles.push({
2082
- name,
2083
- mutate,
2084
- content: mutatedContent
2085
- });
2086
- if (this.logger.isDebugEnabled()) {
2087
- const nrOfMutantsInFile = mutantCollector.mutants.length - mutantCount;
2088
- mutantCount = mutantCollector.mutants.length;
2089
- this.logger.debug(`Instrumented ${path.relative(process.cwd(), name)} (${nrOfMutantsInFile} mutant(s))`);
2090
- }
2091
- }
2092
- const mutants = mutantCollector.mutants.map((mutant) => mutant.toApiMutant());
2093
- this.logger.info("Instrumented %d source file(s) with %d mutant(s)", files.length, mutants.length);
2094
- return {
2095
- files: outFiles,
2096
- mutants
2097
- };
2098
- }
2099
- };
2280
+ mutateDescription: toBabelLineNumber(mutate)
2281
+ }),
2282
+ catch: (cause) => new InstrumentError({
2283
+ message: `Failed to transform ${name}`,
2284
+ cause
2285
+ })
2286
+ });
2287
+ for (const warning of warnings) yield* Effect.logWarning(warning);
2288
+ const mutatedContent = yield* Effect.try({
2289
+ try: () => print(ast),
2290
+ catch: (cause) => new InstrumentError({
2291
+ message: `Failed to print ${name}`,
2292
+ cause
2293
+ })
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
2311
+ };
2312
+ });
2100
2313
  function toBabelLineNumber(range) {
2101
2314
  if (typeof range === "boolean") return range;
2102
2315
  else return range.map(({ start, end }) => ({
@@ -2111,4 +2324,4 @@ function toBabelLineNumber(range) {
2111
2324
  }));
2112
2325
  }
2113
2326
  //#endregion
2114
- export { Instrumenter, createInstrumenter, disableTypeChecks, frameworkPluginsFileUrl, strykerPlugins };
2327
+ export { angularIgnorer, disableTypeChecks, frameworkPluginsFileUrl, instrument, strykerPlugins };